diff --git a/app/actions.ts b/app/actions.ts index 9fe805e..3c6967c 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,12 +1,12 @@ "use server"; -import { getServerSession } from "next-auth"; import { prisma } from "@/lib/prisma"; +import { getServerSession } from "@/lib/session/get-server-session"; import { revalidatePath } from "next/cache"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; export async function removeChat({ id, path }: { id: string; path: string }) { - const session = await getServerSession(authOptions); + const session = await getServerSession(); + const userId = session?.user?.email; if (!userId || !id) { throw new Error("Unauthorized"); diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index dea6129..0000000 --- a/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import NextAuth, { NextAuthOptions } from "next-auth"; -import { PrismaAdapter } from "@next-auth/prisma-adapter"; -import { prisma } from "@/lib/prisma"; -import GoogleProvider from "next-auth/providers/google"; - -export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(prisma), - providers: [ - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID as string, - clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, - }), - ], -}; - -const handler = NextAuth(authOptions); - -export { handler as GET, handler as POST }; diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts new file mode 100644 index 0000000..a235634 --- /dev/null +++ b/app/api/auth/callback/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { serialize } from 'cookie'; +import { createSession } from '@/lib/session/create'; +import { encryptJWECookie } from '@/lib/cookies/encrypt-jwe'; +import ms from 'ms'; +import { userSessionCookieName } from '@/lib/session/constants'; + +export const runtime = 'edge'; + +const sharedTokenSecret = process.env.SHARE_TOKEN_ENDPOINT_SECRET; +const cookieTTL = ms('1y'); + +export async function GET(req: NextRequest) { + const { origin, searchParams } = req.nextUrl; + const shareableToken = searchParams.get('shareableToken'); + + if (!sharedTokenSecret || !shareableToken) { + // TODO: Make an error page + return NextResponse.json({ error: 'Invalid request' }, { status: 500 }); + } + + const query = new URLSearchParams({ + sharedTokenSecret, + }); + + const response = await fetch( + `https://api.vercel.com/user/tokens/shared/${shareableToken}?${query.toString()}` + ); + + const { token } = (await response.json()) as { token: string }; + + const session = await createSession(token); + + const cookieValue = await encryptJWECookie(session, '1y'); + const next = req.cookies.get('auth-next')?.value; + + return NextResponse.redirect(new URL(next ?? '/', origin), { + headers: [ + [ + 'set-cookie', + serialize(userSessionCookieName, cookieValue, { + path: '/', + maxAge: cookieTTL / 1000, + expires: new Date(Date.now() + cookieTTL), + httpOnly: true, + secure: process.env.NODE_ENV !== 'development', + }), + ], + // There's currently an issue with adding more than one cookie to a response: https://github.com/vercel/next.js/issues/46579 + // the `auth-next` cookie will expire in 10 min anyway + // [ + // 'set-cookie', + // serialize('auth-next', '', { + // path: '/api/auth/callback', + // maxAge: 0, + // expires: new Date(), + // httpOnly: true, + // }) + // ] + ], + }); +} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..1bd23cc --- /dev/null +++ b/app/api/auth/login/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { serialize } from 'cookie'; + +export const runtime = 'edge'; + +export async function GET(req: NextRequest) { + const { origin } = req.nextUrl; + + const next = req.nextUrl.searchParams.get('next'); + + const params = new URLSearchParams({ + redirectUrl: new URL('/api/auth/callback', origin).toString(), + }); + + return NextResponse.redirect( + `https://vercel.com/api/vercel-auth?${params.toString()}`, + { + headers: next + ? { + 'Set-Cookie': serialize('auth-next', next, { + path: '/api/auth/callback', + maxAge: 60 * 60 * 10, + httpOnly: true, + }), + } + : undefined, + } + ); +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..a8b2768 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,25 @@ +import { userSessionCookieName } from '@/lib/session/constants'; +import { serialize } from 'cookie'; +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'edge'; + +export async function GET(req: NextRequest) { + const { origin, searchParams } = req.nextUrl; + const next = searchParams.get('next'); + + return NextResponse.redirect(new URL(next ?? '/', origin), { + headers: [ + [ + 'set-cookie', + serialize(userSessionCookieName, '', { + path: '/', + maxAge: 0, + expires: new Date(), + httpOnly: true, + secure: process.env.NODE_ENV !== 'development', + }), + ], + ], + }); +} diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index 80ae6aa..a0a9677 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -1,5 +1,5 @@ import { OpenAIStream, openai } from "@/lib/openai"; -import { getServerSession } from "next-auth"; +import { getServerSession } from "@/lib/session/get-server-session"; export const runtime = "edge"; diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 10c6517..4129e8c 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -3,8 +3,7 @@ import { prisma } from "@/lib/prisma"; import { Chat } from "../../chat"; import { type Metadata } from "next"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { getServerSession } from "@/lib/session/get-server-session"; export interface ChatPageProps { params: { @@ -30,7 +29,7 @@ export const runtime = "nodejs"; // default export const preferredRegion = "home"; export const dynamic = "force-dynamic"; export default async function ChatPage({ params }: ChatPageProps) { - const session = await getServerSession(authOptions); + const session = await getServerSession(); const chat = await prisma.chat.findFirst({ where: { id: params.id, diff --git a/app/page.tsx b/app/page.tsx index e7e6f0b..a3ca75a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,6 @@ -import { getServerSession } from "next-auth"; +import { getServerSession } from "@/lib/session/get-server-session"; import { Chat } from "./chat"; import { Sidebar } from "./sidebar"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; // Prisma does not support Edge without the Data Proxy currently export const runtime = "nodejs"; // default @@ -9,7 +8,7 @@ export const preferredRegion = "home"; export const dynamic = "force-dynamic"; export default async function IndexPage() { - const session = await getServerSession(authOptions); + const session = await getServerSession(); return (
diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 9db8003..013ae22 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -1,16 +1,15 @@ -import { Plus } from "lucide-react"; -import Link from "next/link"; +import { DevGPTLogo } from "@/components/ui/devgpt-logo"; import { Login } from "@/components/ui/login"; import { UserMenu } from "@/components/ui/user-menu"; import { prisma } from "@/lib/prisma"; -import { VercelLogo } from "@/components/ui/vercel-logo"; -import { SidebarItem } from "./sidebar-item"; +import { type Session } from "@/lib/session/types"; import { cn } from "@/lib/utils"; -import { type Session } from "next-auth"; -import { DevGPTLogo } from "@/components/ui/devgpt-logo"; +import { Plus } from "lucide-react"; +import Link from "next/link"; +import { SidebarItem } from "./sidebar-item"; export interface SidebarProps { - session: Session | null; + session?: Session; newChat?: boolean; } diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index f3207da..93847ec 100644 --- a/components/theme-toggle.tsx +++ b/components/theme-toggle.tsx @@ -4,16 +4,21 @@ import { useTheme } from "next-themes"; import { Button } from "@/components/ui/button"; import { Moon, Sun } from "lucide-react"; +import { useTransition } from "react"; export function ThemeToggle() { const { setTheme, theme } = useTheme(); - + const [_, startTransition] = useTransition(); return (