feat: support guest session (#919)
This commit is contained in:
parent
24cb2ce19b
commit
9279135355
34 changed files with 741 additions and 288 deletions
21
app/(auth)/api/auth/guest/route.ts
Normal file
21
app/(auth)/api/auth/guest/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { signIn } from '@/app/(auth)/auth';
|
||||
import { isDevelopmentEnvironment } from '@/lib/constants';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const redirectUrl = searchParams.get('redirectUrl') || '/';
|
||||
|
||||
const token = await getToken({
|
||||
req: request,
|
||||
secret: process.env.AUTH_SECRET,
|
||||
secureCookie: !isDevelopmentEnvironment,
|
||||
});
|
||||
|
||||
if (token) {
|
||||
return NextResponse.redirect(new URL('/', request.url));
|
||||
}
|
||||
|
||||
return signIn('guest', { redirect: true, redirectTo: redirectUrl });
|
||||
}
|
||||
|
|
@ -9,31 +9,5 @@ export const authConfig = {
|
|||
// added later in auth.ts since it requires bcrypt which is only compatible with Node.js
|
||||
// while this file is also used in non-Node.js environments
|
||||
],
|
||||
callbacks: {
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
const isLoggedIn = !!auth?.user;
|
||||
const isOnChat = nextUrl.pathname.startsWith('/');
|
||||
const isOnRegister = nextUrl.pathname.startsWith('/register');
|
||||
const isOnLogin = nextUrl.pathname.startsWith('/login');
|
||||
|
||||
if (isLoggedIn && (isOnLogin || isOnRegister)) {
|
||||
return Response.redirect(new URL('/', nextUrl as unknown as URL));
|
||||
}
|
||||
|
||||
if (isOnRegister || isOnLogin) {
|
||||
return true; // Always allow access to register and login pages
|
||||
}
|
||||
|
||||
if (isOnChat) {
|
||||
if (isLoggedIn) return true;
|
||||
return false; // Redirect unauthenticated users to login page
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
return Response.redirect(new URL('/', nextUrl as unknown as URL));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
callbacks: {},
|
||||
} satisfies NextAuthConfig;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,33 @@
|
|||
import { compare } from 'bcrypt-ts';
|
||||
import NextAuth, { type User, type Session } from 'next-auth';
|
||||
import NextAuth, { type DefaultSession } from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
|
||||
import { getUser } from '@/lib/db/queries';
|
||||
|
||||
import { createGuestUser, getUser } from '@/lib/db/queries';
|
||||
import { authConfig } from './auth.config';
|
||||
import { DUMMY_PASSWORD } from '@/lib/constants';
|
||||
import type { DefaultJWT } from 'next-auth/jwt';
|
||||
|
||||
interface ExtendedSession extends Session {
|
||||
user: User;
|
||||
export type UserType = 'guest' | 'regular';
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface Session extends DefaultSession {
|
||||
user: {
|
||||
id: string;
|
||||
type: UserType;
|
||||
} & DefaultSession['user'];
|
||||
}
|
||||
|
||||
interface User {
|
||||
id?: string;
|
||||
email?: string | null;
|
||||
type: UserType;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'next-auth/jwt' {
|
||||
interface JWT extends DefaultJWT {
|
||||
id: string;
|
||||
type: UserType;
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
|
|
@ -40,27 +59,31 @@ export const {
|
|||
|
||||
if (!passwordsMatch) return null;
|
||||
|
||||
return user as any;
|
||||
return { ...user, type: 'regular' };
|
||||
},
|
||||
}),
|
||||
Credentials({
|
||||
id: 'guest',
|
||||
credentials: {},
|
||||
async authorize() {
|
||||
const [guestUser] = await createGuestUser();
|
||||
return { ...guestUser, type: 'guest' };
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.id = user.id as string;
|
||||
token.type = user.type;
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({
|
||||
session,
|
||||
token,
|
||||
}: {
|
||||
session: ExtendedSession;
|
||||
token: any;
|
||||
}) {
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.id = token.id;
|
||||
session.user.type = token.type;
|
||||
}
|
||||
|
||||
return session;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
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]);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
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]);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import {
|
|||
smoothStream,
|
||||
streamText,
|
||||
} from 'ai';
|
||||
import { auth } from '@/app/(auth)/auth';
|
||||
import { auth, type UserType } from '@/app/(auth)/auth';
|
||||
import { systemPrompt } from '@/lib/ai/prompts';
|
||||
import {
|
||||
deleteChatById,
|
||||
getChatById,
|
||||
getMessageCountByUserId,
|
||||
saveChat,
|
||||
saveMessages,
|
||||
} from '@/lib/db/queries';
|
||||
|
|
@ -25,6 +26,7 @@ import { requestSuggestions } from '@/lib/ai/tools/request-suggestions';
|
|||
import { getWeather } from '@/lib/ai/tools/get-weather';
|
||||
import { isProductionEnvironment } from '@/lib/constants';
|
||||
import { myProvider } from '@/lib/ai/providers';
|
||||
import { entitlementsByUserType } from '@/lib/ai/entitlements';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
|
|
@ -46,6 +48,22 @@ export async function POST(request: Request) {
|
|||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const userType: UserType = session.user.type;
|
||||
|
||||
const messageCount = await getMessageCountByUserId({
|
||||
id: session.user.id,
|
||||
differenceInHours: 24,
|
||||
});
|
||||
|
||||
if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) {
|
||||
return new Response(
|
||||
'You have exceeded your maximum number of messages for the day! Please try again later.',
|
||||
{
|
||||
status: 429,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const userMessage = getMostRecentUserMessage(messages);
|
||||
|
||||
if (!userMessage) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
|
||||
import { auth } from '@/app/(auth)/auth';
|
||||
import { Chat } from '@/components/chat';
|
||||
import { getChatById, getMessagesByChatId } from '@/lib/db/queries';
|
||||
import { DataStreamHandler } from '@/components/data-stream-handler';
|
||||
import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
|
||||
import { DBMessage } from '@/lib/db/schema';
|
||||
import { Attachment, UIMessage } from 'ai';
|
||||
import type { DBMessage } from '@/lib/db/schema';
|
||||
import type { Attachment, UIMessage } from 'ai';
|
||||
|
||||
export default async function Page(props: { params: Promise<{ id: string }> }) {
|
||||
const params = await props.params;
|
||||
|
|
@ -20,8 +20,12 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
redirect('/api/auth/guest');
|
||||
}
|
||||
|
||||
if (chat.visibility === 'private') {
|
||||
if (!session || !session.user) {
|
||||
if (!session.user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +63,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
selectedChatModel={DEFAULT_CHAT_MODEL}
|
||||
selectedVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
session={session}
|
||||
/>
|
||||
<DataStreamHandler id={id} />
|
||||
</>
|
||||
|
|
@ -73,6 +78,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
selectedChatModel={chatModelFromCookie.value}
|
||||
selectedVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
session={session}
|
||||
/>
|
||||
<DataStreamHandler id={id} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,16 @@ import { Chat } from '@/components/chat';
|
|||
import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
|
||||
import { generateUUID } from '@/lib/utils';
|
||||
import { DataStreamHandler } from '@/components/data-stream-handler';
|
||||
import { auth } from '../(auth)/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Page() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
redirect('/api/auth/guest');
|
||||
}
|
||||
|
||||
const id = generateUUID();
|
||||
|
||||
const cookieStore = await cookies();
|
||||
|
|
@ -21,6 +29,7 @@ export default async function Page() {
|
|||
selectedChatModel={DEFAULT_CHAT_MODEL}
|
||||
selectedVisibilityType="private"
|
||||
isReadonly={false}
|
||||
session={session}
|
||||
/>
|
||||
<DataStreamHandler id={id} />
|
||||
</>
|
||||
|
|
@ -36,6 +45,7 @@ export default async function Page() {
|
|||
selectedChatModel={modelIdFromCookie.value}
|
||||
selectedVisibilityType="private"
|
||||
isReadonly={false}
|
||||
session={session}
|
||||
/>
|
||||
<DataStreamHandler id={id} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Geist, Geist_Mono } from 'next/font/google';
|
|||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
|
||||
import './globals.css';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://chat.vercel.ai'),
|
||||
|
|
@ -77,7 +78,7 @@ export default async function RootLayout({
|
|||
disableTransitionOnChange
|
||||
>
|
||||
<Toaster position="top-center" />
|
||||
{children}
|
||||
<SessionProvider>{children}</SessionProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue