import NextAuth, { type DefaultSession } from "next-auth"; import "next-auth/jwt"; import GitHubProvider from "next-auth/providers/github"; import CredentialsProvider from "next-auth/providers/credentials"; import { client as db } from "@/lib/db"; import { randomUUID } from "crypto"; import type { Adapter, AdapterUser, AdapterAccount, AdapterSession, VerificationToken } from "@auth/core/adapters"; import { AUTH_CONFIG, ROUTES } from "@/constants"; /** * 🛰️ CodeVerse Authentication Core * Final Production-Grade Implementation (April 2026). * Enforces 100% strict typing, JWT strategy, and local bypass resilience. */ // --- 🧩 Type Augmentations --- declare module "next-auth" { interface Session { user: { id: string; github_username?: string | null; } & DefaultSession["user"]; } interface User { github_username?: string | null; } } declare module "next-auth/jwt" { interface JWT { id?: string; github_username?: string | null; } } /** * 🛠️ Extended Types for Adapter Internal Use */ interface CodeVerseAdapterUser extends AdapterUser { github_username?: string | null; } // LibSQL input value type for explicit casting type LibSQLValue = string | number | bigint | boolean | Uint8Array | null; // --- 🛠️ Custom Adapter Implementation --- const TursoAdapter: Adapter = { async createUser(user: AdapterUser): Promise { const id = randomUUID(); const github_username = (user as CodeVerseAdapterUser).github_username ?? null; await db.execute({ sql: "INSERT INTO users (id, name, email, image, github_username) VALUES (?, ?, ?, ?, ?)", args: [id, user.name ?? null, user.email, user.image ?? null, github_username] as LibSQLValue[], }); return { ...user, id }; }, async getUser(id: string): Promise { const res = await db.execute({ sql: "SELECT * FROM users WHERE id = ?", args: [id] as LibSQLValue[], }); if (res.rows.length === 0) return null; const row = res.rows[0]; return { id: row.id as string, email: row.email as string, emailVerified: null, name: row.name as string | null, image: row.image as string | null, }; }, async getUserByEmail(email: string): Promise { const res = await db.execute({ sql: "SELECT * FROM users WHERE email = ?", args: [email] as LibSQLValue[], }); if (res.rows.length === 0) return null; const row = res.rows[0]; return { id: row.id as string, email: row.email as string, emailVerified: null, name: row.name as string | null, image: row.image as string | null, }; }, async getUserByAccount({ providerAccountId, provider }: { providerAccountId: string; provider: string }): Promise { const res = await db.execute({ sql: `SELECT u.* FROM users u JOIN accounts a ON u.id = a.userId WHERE a.providerAccountId = ? AND a.provider = ?`, args: [providerAccountId, provider] as LibSQLValue[], }); if (res.rows.length === 0) return null; const row = res.rows[0]; return { id: row.id as string, email: row.email as string, emailVerified: null, name: row.name as string | null, image: row.image as string | null, }; }, async updateUser(user: Partial & { id: string }): Promise { const res = await db.execute({ sql: "SELECT name, email, image FROM users WHERE id = ?", args: [user.id] as LibSQLValue[], }); if (res.rows.length === 0) throw new Error("User not found"); const existing = res.rows[0]; const name = user.name ?? (existing.name as string | null); const email = user.email ?? (existing.email as string); const image = user.image ?? (existing.image as string | null); await db.execute({ sql: "UPDATE users SET name = ?, email = ?, image = ? WHERE id = ?", args: [name, email, image, user.id] as LibSQLValue[], }); return { id: user.id, email: email, emailVerified: null, name: name, image: image, }; }, async deleteUser(userId: string): Promise { await db.execute({ sql: "DELETE FROM users WHERE id = ?", args: [userId] as LibSQLValue[] }); }, async linkAccount(account: AdapterAccount): Promise { await db.execute({ sql: `INSERT INTO accounts (id, userId, provider, providerAccountId, refresh_token, access_token, expires_at, token_type, scope, id_token, session_state) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: [ randomUUID(), account.userId, account.provider, account.providerAccountId, account.refresh_token ?? null, account.access_token ?? null, account.expires_at ?? null, account.token_type ?? null, account.scope ?? null, account.id_token ?? null, account.session_state ?? null ] as LibSQLValue[], }); }, async unlinkAccount({ providerAccountId, provider }: { providerAccountId: string; provider: string }): Promise { await db.execute({ sql: "DELETE FROM accounts WHERE providerAccountId = ? AND provider = ?", args: [providerAccountId, provider] as LibSQLValue[], }); }, async createSession({ sessionToken, userId, expires }: { sessionToken: string; userId: string; expires: Date }): Promise { const id = randomUUID(); await db.execute({ sql: "INSERT INTO sessions (id, sessionToken, userId, expires) VALUES (?, ?, ?, ?)", args: [id, sessionToken, userId, expires.toISOString()] as LibSQLValue[], }); return { sessionToken, userId, expires }; }, async getSessionAndUser(sessionToken: string): Promise<{ session: AdapterSession; user: AdapterUser } | null> { const res = await db.execute({ sql: `SELECT s.sessionToken, s.userId, s.expires, u.id as u_id, u.name, u.email, u.image FROM sessions s JOIN users u ON s.userId = u.id WHERE s.sessionToken = ?`, args: [sessionToken] as LibSQLValue[], }); if (res.rows.length === 0) return null; const row = res.rows[0]; return { session: { sessionToken: row.sessionToken as string, userId: row.userId as string, expires: new Date(row.expires as string) }, user: { id: row.u_id as string, name: row.name as string | null, email: row.email as string, image: row.image as string | null, emailVerified: null }, }; }, async updateSession(session: Partial & { sessionToken: string }): Promise { const res = await db.execute({ sql: "SELECT userId, expires FROM sessions WHERE sessionToken = ?", args: [session.sessionToken] as LibSQLValue[], }); if (res.rows.length === 0) return null; const current = res.rows[0]; const expires = session.expires?.toISOString() ?? (current.expires as string); await db.execute({ sql: "UPDATE sessions SET expires = ? WHERE sessionToken = ?", args: [expires, session.sessionToken] as LibSQLValue[], }); return { sessionToken: session.sessionToken, userId: current.userId as string, expires: new Date(expires) }; }, async deleteSession(sessionToken: string): Promise { await db.execute({ sql: "DELETE FROM sessions WHERE sessionToken = ?", args: [sessionToken] as LibSQLValue[] }); }, async createVerificationToken({ identifier, expires, token }: { identifier: string; expires: Date; token: string }): Promise { await db.execute({ sql: "INSERT INTO verification_tokens (identifier, token, expires) VALUES (?, ?, ?)", args: [identifier, token, expires.toISOString()] as LibSQLValue[], }); return { identifier, expires, token }; }, async useVerificationToken({ identifier, token }: { identifier: string; token: string }): Promise { const res = await db.execute({ sql: "SELECT * FROM verification_tokens WHERE identifier = ? AND token = ?", args: [identifier, token] as LibSQLValue[], }); if (res.rows.length === 0) return null; await db.execute({ sql: "DELETE FROM verification_tokens WHERE identifier = ? AND token = ?", args: [identifier, token] as LibSQLValue[], }); const row = res.rows[0]; return { identifier: row.identifier as string, token: row.token as string, expires: new Date(row.expires as string) }; } }; // --- 🚀 Auth Configuration --- export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: TursoAdapter, trustHost: true, secret: process.env.AUTH_SECRET, providers: [ GitHubProvider({ clientId: process.env.GITHUB_ID ?? "", clientSecret: process.env.GITHUB_SECRET ?? "", }), ...(process.env.NODE_ENV === "development" || process.env.NEXT_PUBLIC_NODE_ENV === "development" ? [ CredentialsProvider({ id: "credentials", name: "Developer Bypass", credentials: { username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" } }, async authorize(credentials) { if (credentials?.username === "dev" || credentials?.username === "guest") { return { id: AUTH_CONFIG.DEV_USER_ID, name: AUTH_CONFIG.DEV_USER_NAME, email: AUTH_CONFIG.DEV_USER_EMAIL, image: AUTH_CONFIG.DEV_AVATAR, }; } return null; } }) ] : []), ], session: { strategy: AUTH_CONFIG.SESSION_STRATEGY as "jwt" | "database" }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.github_username = user.github_username; } return token; }, async session({ session, token }) { if (session.user && token.id) { session.user.id = token.id; session.user.github_username = token.github_username; } return session; }, }, pages: { signIn: ROUTES.LOGIN, }, debug: process.env.NODE_ENV === "development", });