From 50e85e499afe2cda3b2126ed8154953875936bf0 Mon Sep 17 00:00:00 2001 From: Marko Djordjevic Date: Fri, 20 Feb 2026 09:55:04 +0100 Subject: [PATCH] Add src/auth.ts with Auth.js v5 config (task 3.1) JWT session strategy, Credentials provider with bcryptjs password verification, and Google OAuth provider using AUTH_GOOGLE_ID/SECRET. Co-Authored-By: Claude Sonnet 4.6 --- openspec/changes/user-accounts/tasks.md | 2 +- src/auth.ts | 72 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/auth.ts diff --git a/openspec/changes/user-accounts/tasks.md b/openspec/changes/user-accounts/tasks.md index 8fe4a0d..dd7a952 100644 --- a/openspec/changes/user-accounts/tasks.md +++ b/openspec/changes/user-accounts/tasks.md @@ -14,7 +14,7 @@ ## 3. Auth.js Configuration -- [ ] 3.1 `[sonnet]` Create `src/auth.ts` with Auth.js v5 config: JWT strategy, Credentials provider (email/password with bcryptjs verify), Google OAuth provider +- [x] 3.1 `[sonnet]` Create `src/auth.ts` with Auth.js v5 config: JWT strategy, Credentials provider (email/password with bcryptjs verify), Google OAuth provider - [ ] 3.2 `[sonnet]` Add JWT callback to embed `user.id` in token and session callback to expose `session.user.id` - [ ] 3.3 `[sonnet]` Handle Google OAuth sign-in callback: create user on first sign-in, find existing user on returning sign-in - [ ] 3.4 `[haiku]` Create `src/app/api/auth/[...nextauth]/route.ts` exporting GET/POST handlers diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..580834e --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,72 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import Google from "next-auth/providers/google"; +import bcryptjs from "bcryptjs"; +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { users } from "@/lib/db/schema"; + +export const { handlers, auth, signIn, signOut } = NextAuth({ + secret: process.env.AUTH_SECRET, + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + providers: [ + Credentials({ + name: "Credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + const email = credentials?.email as string | undefined; + const password = credentials?.password as string | undefined; + + if (!email || !password) { + return null; + } + + const [user] = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (!user || !user.password_hash) { + return null; + } + + const passwordValid = await bcryptjs.compare(password, user.password_hash); + if (!passwordValid) { + return null; + } + + return { + id: user.id, + email: user.email, + name: user.name, + image: user.image, + }; + }, + }), + Google({ + clientId: process.env.AUTH_GOOGLE_ID, + clientSecret: process.env.AUTH_GOOGLE_SECRET, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + } + return token; + }, + async session({ session, token }) { + if (token.id) { + session.user.id = token.id as string; + } + return session; + }, + }, +});