-
Notifications
You must be signed in to change notification settings - Fork 0
feat: support guest session #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { redirect } from 'next/navigation'; | ||
import { auth, signIn } from '@/app/(auth)/auth'; | ||
|
||
export async function GET() { | ||
const session = await auth(); | ||
|
||
if (!session?.user?.id) { | ||
await signIn('guest', { redirect: false }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improvement: Add error handling for the case where |
||
redirect('/'); | ||
} | ||
|
||
return new Response('Unauthorized', { status: 401 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic Issue: This response will never be reached because of the redirect above. If the user is not authenticated, they'll be redirected, and if they are authenticated, this endpoint shouldn't return an error. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ import { compare } from 'bcrypt-ts'; | |
import NextAuth, { type User, type Session } from 'next-auth'; | ||
import Credentials from 'next-auth/providers/credentials'; | ||
|
||
import { getUser } from '@/lib/db/queries'; | ||
import { createAnonymousUser, getUser } from '@/lib/db/queries'; | ||
|
||
import { authConfig } from './auth.config'; | ||
|
||
|
@@ -21,12 +21,24 @@ export const { | |
Credentials({ | ||
credentials: {}, | ||
async authorize({ email, password }: any) { | ||
const users = await getUser(email); | ||
if (users.length === 0) return null; | ||
// biome-ignore lint: Forbidden non-null assertion. | ||
const passwordsMatch = await compare(password, users[0].password!); | ||
const [user] = await getUser(email); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improvement: Consider destructuring the user object directly from the result of |
||
|
||
if (!user) return null; | ||
if (!user.password) return null; | ||
|
||
const passwordsMatch = await compare(password, user.password); | ||
|
||
if (!passwordsMatch) return null; | ||
return users[0] as any; | ||
|
||
return user; | ||
}, | ||
}), | ||
Credentials({ | ||
id: 'guest', | ||
credentials: {}, | ||
async authorize() { | ||
const [anonymousUser] = await createAnonymousUser(); | ||
return anonymousUser; | ||
}, | ||
}), | ||
], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import { AuthForm } from '@/components/auth-form'; | |
import { SubmitButton } from '@/components/submit-button'; | ||
|
||
import { login, type LoginActionState } from '../actions'; | ||
import { useSession } from 'next-auth/react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Import: The |
||
|
||
export default function Page() { | ||
const router = useRouter(); | ||
|
@@ -23,6 +24,8 @@ export default function Page() { | |
}, | ||
); | ||
|
||
const { update: updateSession } = useSession(); | ||
|
||
useEffect(() => { | ||
if (state.status === 'failed') { | ||
toast({ | ||
|
@@ -36,6 +39,7 @@ export default function Page() { | |
}); | ||
} else if (state.status === 'success') { | ||
setIsSuccessful(true); | ||
updateSession(); | ||
router.refresh(); | ||
} | ||
}, [state.status]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dependency Warning: You're missing |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import { SubmitButton } from '@/components/submit-button'; | |
|
||
import { register, type RegisterActionState } from '../actions'; | ||
import { toast } from '@/components/toast'; | ||
import { useSession } from 'next-auth/react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Import: The |
||
|
||
export default function Page() { | ||
const router = useRouter(); | ||
|
@@ -23,6 +24,8 @@ export default function Page() { | |
}, | ||
); | ||
|
||
const { update: updateSession } = useSession(); | ||
|
||
useEffect(() => { | ||
if (state.status === 'user_exists') { | ||
toast({ type: 'error', description: 'Account already exists!' }); | ||
|
@@ -37,6 +40,7 @@ export default function Page() { | |
toast({ type: 'success', description: 'Account created successfully!' }); | ||
|
||
setIsSuccessful(true); | ||
updateSession(); | ||
router.refresh(); | ||
} | ||
}, [state]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dependency Warning: You're missing |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { | ||
UIMessage, | ||
type UIMessage, | ||
appendResponseMessages, | ||
createDataStreamResponse, | ||
smoothStream, | ||
|
@@ -10,6 +10,7 @@ import { systemPrompt } from '@/lib/ai/prompts'; | |
import { | ||
deleteChatById, | ||
getChatById, | ||
getMessageCountByUserId, | ||
saveChat, | ||
saveMessages, | ||
} from '@/lib/db/queries'; | ||
|
@@ -23,8 +24,12 @@ import { createDocument } from '@/lib/ai/tools/create-document'; | |
import { updateDocument } from '@/lib/ai/tools/update-document'; | ||
import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; | ||
import { getWeather } from '@/lib/ai/tools/get-weather'; | ||
import { isProductionEnvironment } from '@/lib/constants'; | ||
import { anonymousRegex, isProductionEnvironment } from '@/lib/constants'; | ||
import { myProvider } from '@/lib/ai/providers'; | ||
import { | ||
entitlementsByMembershipTier, | ||
type MembershipTier, | ||
} from '@/lib/ai/capabilities'; | ||
|
||
export const maxDuration = 60; | ||
|
||
|
@@ -42,10 +47,33 @@ export async function POST(request: Request) { | |
|
||
const session = await auth(); | ||
|
||
if (!session || !session.user || !session.user.id) { | ||
if (!session?.user?.id) { | ||
return new Response('Unauthorized', { status: 401 }); | ||
} | ||
|
||
const membershipTier: MembershipTier = anonymousRegex.test( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improvement: Consider extracting the membership tier determination logic to a separate function for better reusability. |
||
session.user.email ?? '', | ||
) | ||
? 'guest' | ||
: 'free'; | ||
|
||
const messageCount = await getMessageCountByUserId({ | ||
id: session.user.id, | ||
differenceInHours: 24, | ||
}); | ||
|
||
if ( | ||
messageCount > | ||
entitlementsByMembershipTier[membershipTier].maxMessagesPerDay | ||
) { | ||
return new Response( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UX Suggestion: Consider providing more detailed error information in the 429 response, such as when the user can try again or how many messages they've used. |
||
'You have exceeded your maximum number of messages for the day', | ||
{ | ||
status: 429, | ||
}, | ||
); | ||
} | ||
|
||
const userMessage = getMostRecentUserMessage(messages); | ||
|
||
if (!userMessage) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
'use client'; | ||
|
||
import { ChevronUp } from 'lucide-react'; | ||
import Image from 'next/image'; | ||
import type { User } from 'next-auth'; | ||
import { signOut } from 'next-auth/react'; | ||
import { signOut, useSession } from 'next-auth/react'; | ||
import { useTheme } from 'next-themes'; | ||
|
||
import { | ||
|
@@ -17,26 +18,50 @@ import { | |
SidebarMenuButton, | ||
SidebarMenuItem, | ||
} from '@/components/ui/sidebar'; | ||
import { anonymousRegex } from '@/lib/constants'; | ||
import { useRouter } from 'next/navigation'; | ||
import { toast } from './toast'; | ||
import { LoaderIcon } from './icons'; | ||
|
||
export function SidebarUserNav({ user }: { user: User }) { | ||
const router = useRouter(); | ||
const { data, status } = useSession(); | ||
const { setTheme, theme } = useTheme(); | ||
|
||
const isGuest = anonymousRegex.test(data?.user?.email ?? ''); | ||
|
||
return ( | ||
<SidebarMenu> | ||
<SidebarMenuItem> | ||
<DropdownMenu> | ||
<DropdownMenuTrigger asChild> | ||
<SidebarMenuButton className="data-[state=open]:bg-sidebar-accent bg-background data-[state=open]:text-sidebar-accent-foreground h-10"> | ||
<Image | ||
src={`https://avatar.vercel.sh/${user.email}`} | ||
alt={user.email ?? 'User Avatar'} | ||
width={24} | ||
height={24} | ||
className="rounded-full" | ||
/> | ||
<span className="truncate">{user?.email}</span> | ||
<ChevronUp className="ml-auto" /> | ||
</SidebarMenuButton> | ||
{status === 'loading' ? ( | ||
<SidebarMenuButton className="data-[state=open]:bg-sidebar-accent bg-background data-[state=open]:text-sidebar-accent-foreground h-10 justify-between"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: The loading state could be optimized by using a skeleton component instead of animating multiple elements. |
||
<div className="flex flex-row gap-2"> | ||
<div className="size-6 bg-zinc-500/30 rounded-full animate-pulse" /> | ||
<span className="bg-zinc-500/30 text-transparent rounded-md animate-pulse"> | ||
Loading auth status | ||
</span> | ||
</div> | ||
<div className="animate-spin text-zinc-500"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessibility: Consider adding an aria-label to the loading spinner for better accessibility. |
||
<LoaderIcon /> | ||
</div> | ||
</SidebarMenuButton> | ||
) : ( | ||
<SidebarMenuButton className="data-[state=open]:bg-sidebar-accent bg-background data-[state=open]:text-sidebar-accent-foreground h-10"> | ||
<Image | ||
src={`https://avatar.vercel.sh/${user.email}`} | ||
alt={user.email ?? 'User Avatar'} | ||
width={24} | ||
height={24} | ||
className="rounded-full" | ||
/> | ||
<span className="truncate"> | ||
{isGuest ? 'Guest' : user?.email} | ||
</span> | ||
<ChevronUp className="ml-auto" /> | ||
</SidebarMenuButton> | ||
)} | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent | ||
side="top" | ||
|
@@ -54,12 +79,26 @@ export function SidebarUserNav({ user }: { user: User }) { | |
type="button" | ||
className="w-full cursor-pointer" | ||
onClick={() => { | ||
signOut({ | ||
redirectTo: '/', | ||
}); | ||
if (status === 'loading') { | ||
toast({ | ||
type: 'error', | ||
description: | ||
'Checking authentication status, please try again!', | ||
}); | ||
|
||
return; | ||
} | ||
|
||
if (isGuest) { | ||
router.push('/login'); | ||
} else { | ||
signOut({ | ||
redirectTo: '/', | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dependency Warning: You should include |
||
} | ||
}} | ||
> | ||
Sign out | ||
{isGuest ? 'Login to your account' : 'Sign out'} | ||
</button> | ||
</DropdownMenuItem> | ||
</DropdownMenuContent> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { ChatModel } from './models'; | ||
|
||
export type MembershipTier = 'guest' | 'free'; | ||
|
||
interface Entitlements { | ||
maxMessagesPerDay: number; | ||
chatModelsAvailable: Array<ChatModel['id']>; | ||
} | ||
|
||
export const entitlementsByMembershipTier: Record< | ||
MembershipTier, | ||
Entitlements | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation: Consider adding JSDoc comments to explain the purpose and constraints of each membership tier. |
||
> = { | ||
/* | ||
* For users without an account | ||
*/ | ||
guest: { | ||
maxMessagesPerDay: 20, | ||
chatModelsAvailable: ['chat-model', 'chat-model-reasoning'], | ||
}, | ||
|
||
/* | ||
* For user with an account | ||
*/ | ||
free: { | ||
maxMessagesPerDay: 100, | ||
chatModelsAvailable: ['chat-model', 'chat-model-reasoning'], | ||
}, | ||
|
||
/* | ||
* TODO: For users with an account and a paid membership | ||
*/ | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,5 @@ export const isTestEnvironment = Boolean( | |
process.env.PLAYWRIGHT || | ||
process.env.CI_PLAYWRIGHT, | ||
); | ||
|
||
export const anonymousRegex = /^anonymous-\d+$/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: The anonymous regex pattern might be too simple. Consider a more robust pattern to ensure it only matches the expected format. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Consider adding a rate limit for guest sign-ins to prevent abuse of the anonymous user creation feature.