From 1d2b63ae7049d31c1ee058031f99ed5c6396ec0a Mon Sep 17 00:00:00 2001 From: Leon Serfaty G Date: Thu, 17 Jul 2025 11:34:27 +0000 Subject: [PATCH] change our current auth system for NextAuth.js (now Auth.js): This is th --- .env | 3 +- middleware.ts | 27 ++++--- package-lock.json | 103 ++++++++++++++++++++++++ package.json | 1 + src/app/admin/layout.tsx | 19 ++--- src/app/admin/settings/user/page.tsx | 1 - src/app/api/auth/[...nextauth]/route.ts | 68 ++++++++++++++++ src/app/login/page.tsx | 25 +++--- src/lib/actions/user.ts | 23 +++--- src/lib/auth.ts | 76 +---------------- src/lib/types.ts | 13 +++ 11 files changed, 240 insertions(+), 119 deletions(-) create mode 100644 src/app/api/auth/[...nextauth]/route.ts diff --git a/.env b/.env index 418813a..148673b 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -GEMINI_API_KEY= \ No newline at end of file + +AUTH_SECRET="your-super-secret-auth-secret-change-me" diff --git a/middleware.ts b/middleware.ts index 5c9b80e..bb437d8 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,18 +1,25 @@ -import {NextResponse} from 'next/server'; -import type {NextRequest} from 'next/server'; +import { auth } from '@/app/api/auth/[...nextauth]/route'; +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; -export function middleware(request: NextRequest) { - const session = request.cookies.get('session'); +export async function middleware(request: NextRequest) { + const session = await auth(); + const { pathname } = request.nextUrl; - // Redirect to login if trying to access /admin without a session - if (request.nextUrl.pathname.startsWith('/admin') && !session) { - return NextResponse.redirect(new URL('/login', request.url)); + const isAuthPage = pathname === '/login'; + + if (isAuthPage) { + if (session) { + return NextResponse.redirect(new URL('/admin', request.url)); + } + return null; } - // Redirect to admin if trying to access /login with a session - if (request.nextUrl.pathname === '/login' && session) { - return NextResponse.redirect(new URL('/admin', request.url)); + if (!session && pathname.startsWith('/admin')) { + const signInUrl = new URL('/login', request.url); + signInUrl.searchParams.set('callbackUrl', pathname); + return NextResponse.redirect(signInUrl); } return NextResponse.next(); diff --git a/package-lock.json b/package-lock.json index e565af8..c0910db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "genkit": "^1.14.1", "lucide-react": "^0.475.0", "next": "15.3.3", + "next-auth": "^5.0.0-beta.19", "nodemailer": "^6.9.14", "patch-package": "^8.0.0", "react": "^18.3.1", @@ -90,6 +91,35 @@ "zod": "^3.20.2" } }, + "node_modules/@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/runtime": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", @@ -2671,6 +2701,15 @@ "node": ">=14" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7268,6 +7307,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7811,6 +7859,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.29.tgz", + "integrity": "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.40.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0-0", + "nodemailer": "^6.6.5", + "react": "^18.2.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -7902,6 +7977,15 @@ "node": ">=0.10.0" } }, + "node_modules/oauth4webapi": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.0.tgz", + "integrity": "sha512-OwXPTXjKPOldTpAa19oksrX9TYHA0rt+VcUFTkJ7QKwgmevPpNm9Cn5vFZUtIo96FiU6AfPuUUGzoXqgOzibWg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8359,6 +8443,25 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", diff --git a/package.json b/package.json index 9cc3384..c41ebf1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "genkit": "^1.14.1", "lucide-react": "^0.475.0", "next": "15.3.3", + "next-auth": "^5.0.0-beta.19", "nodemailer": "^6.9.14", "patch-package": "^8.0.0", "react": "^18.3.1", diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 69e2eaf..986697f 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -17,7 +17,7 @@ import { SidebarFooter, } from '@/components/ui/sidebar'; import { LayoutDashboard, LogOut, Settings, Mail, User as UserIcon } from 'lucide-react'; -import { signOut, getSession } from '@/lib/auth'; +import { signOut } from '@/lib/auth'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -28,17 +28,18 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import type { User } from '@/lib/types'; - +import { auth } from '@/app/api/auth/[...nextauth]/route'; export default async function AdminLayout({ children, }: { children: React.ReactNode; }) { - const session = (await getSession()) as User | null; - const userInitials = session?.name - ? session.name + const session = await auth(); + const user = session?.user; + + const userInitials = user?.name + ? user.name .split(' ') .map((n) => n[0]) .join('') @@ -108,14 +109,14 @@ export default async function AdminLayout({ - My Account + {user?.name} Settings @@ -123,7 +124,7 @@ export default async function AdminLayout({
- diff --git a/src/app/admin/settings/user/page.tsx b/src/app/admin/settings/user/page.tsx index cd8ebfb..a015177 100644 --- a/src/app/admin/settings/user/page.tsx +++ b/src/app/admin/settings/user/page.tsx @@ -18,7 +18,6 @@ import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; import { useEffect, useTransition } from "react"; import { getUser, updateUser } from "@/lib/actions/user"; -import { User } from "@/lib/types"; const userProfileSchema = z.object({ name: z.string().min(1, "Name is required"), diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..8965ca8 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,68 @@ + +import NextAuth from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; +import db from '@/lib/db'; +import type { User } from '@/lib/types'; + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + email: { label: 'Email', type: 'email' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials.password) { + return null; + } + + const email = credentials.email as string; + const password = credentials.password as string; + + try { + const stmt = db.prepare('SELECT * FROM users WHERE email = ?'); + const user = stmt.get(email) as User | undefined; + + // In a real app, you would use a secure password hashing library like bcrypt + if (user && user.password === password) { + // Return a user object that NextAuth will use to create the session + return { + id: user.id.toString(), + name: user.name, + email: user.email, + }; + } else { + // Invalid credentials + return null; + } + } catch (error) { + console.error('Database error during authorization:', error); + return null; + } + }, + }), + ], + callbacks: { + jwt({ token, user }) { + if (user) { + token.id = user.id; + } + return token; + }, + session({ session, token }) { + if (session.user) { + session.user.id = token.id as string; + } + return session; + }, + }, + pages: { + signIn: '/login', + }, +}); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 2981654..6033d55 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -4,15 +4,14 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { signIn, getSession } from '@/lib/auth'; -import { redirect } from 'next/navigation'; +import { signIn } from '@/app/api/auth/[...nextauth]/route'; + +async function handleSignIn(formData: FormData) { + 'use server'; + await signIn('credentials', formData); +} export default async function LoginPage() { - const session = await getSession(); - if (session) { - redirect('/admin'); - } - return (
@@ -21,7 +20,7 @@ export default async function LoginPage() { Enter your credentials to access the dashboard. - +
-
diff --git a/src/lib/actions/user.ts b/src/lib/actions/user.ts index b3fe113..c93808d 100644 --- a/src/lib/actions/user.ts +++ b/src/lib/actions/user.ts @@ -2,9 +2,9 @@ 'use server'; import db from '@/lib/db'; -import { User } from '@/lib/types'; +import type { User } from '@/lib/types'; import { z } from 'zod'; -import { getSession } from '../auth'; +import { auth } from '@/app/api/auth/[...nextauth]/route'; const UserUpdateSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -12,15 +12,16 @@ const UserUpdateSchema = z.object({ password: z.string().optional(), }); -export async function getUser(): Promise { - const session = await getSession(); - if (!session?.userId) { +export async function getUser(): Promise<(User & { id: string }) | null> { + const session = await auth(); + if (!session?.user?.id) { return null; } try { const stmt = db.prepare('SELECT id, name, email FROM users WHERE id = ?'); - const user = stmt.get(session.userId) as User | undefined; - return user ?? null; + const user = stmt.get(session.user.id) as User | undefined; + if (!user) return null; + return { ...user, id: user.id.toString() }; } catch (error) { console.error('Failed to get user:', error); return null; @@ -30,8 +31,8 @@ export async function getUser(): Promise { export async function updateUser( data: z.infer ): Promise<{ success: boolean; error?: string }> { - const session = await getSession(); - if (!session?.userId) { + const session = await auth(); + if (!session?.user?.id) { return { success: false, error: 'Not authenticated. Please log in again.' }; } @@ -50,10 +51,10 @@ export async function updateUser( const stmt = db.prepare( 'UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?' ); - stmt.run(name, email, password, session.userId); + stmt.run(name, email, password, session.user.id); } else { const stmt = db.prepare('UPDATE users SET name = ?, email = ? WHERE id = ?'); - stmt.run(name, email, session.userId); + stmt.run(name, email, session.user.id); } return { success: true }; } catch (error: any) { diff --git a/src/lib/auth.ts b/src/lib/auth.ts index aa1170e..9adcf69 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,79 +1,7 @@ 'use server'; - -import { redirect } from 'next/navigation'; -import { cookies } from 'next/headers'; -import db from './db'; -import type { User } from './types'; - - -export async function signIn(formData: FormData) { - const email = formData.get('email'); - const password = formData.get('password'); - - if (typeof email !== 'string' || typeof password !== 'string') { - // Handle case where form data is missing or not strings - redirect('/login?error=Invalid%20input'); - return; - } - - try { - const stmt = db.prepare('SELECT * FROM users WHERE email = ?'); - const user = stmt.get(email) as User | undefined; - - // In a real app, you would use a secure password hashing library like bcrypt - // For this example, we'll compare plain text passwords. - if (user && user.password === password) { - const sessionData = { - isLoggedIn: true, - userId: user.id, - email: user.email, - name: user.name, - }; - - // Set the session cookie - cookies().set('session', JSON.stringify(sessionData), { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }); - - redirect('/admin'); - } else { - // Failed login - redirect('/login?error=Invalid%20credentials'); - } - } catch (error) { - console.error('Failed to sign in:', error); - redirect('/login?error=Database%20error'); - } -} +import { signOut as nextAuthSignOut } from '@/app/api/auth/[...nextauth]/route'; export async function signOut() { - cookies().delete('session'); - redirect('/login'); -} - -export async function getSession() { - const cookieStore = cookies(); - const sessionCookie = cookieStore.get('session'); - - if (!sessionCookie?.value) { - return null; - } - - try { - const session = JSON.parse(sessionCookie.value); - // Basic validation to ensure the session object has expected properties - if (session && typeof session === 'object' && session.userId) { - return session as User & { isLoggedIn: boolean; userId: number }; - } - return null; - } catch (error) { - console.error('Failed to parse session cookie:', error); - // If parsing fails, the cookie is invalid. Clear it. - cookieStore.delete('session'); - return null; - } + await nextAuthSignOut({ redirectTo: '/login' }); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 5e68c11..76de8b8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,3 +5,16 @@ export interface User { password?: string; // Should be handled securely, not sent to client name: string; } + +// Augment the default NextAuth session and user types +declare module 'next-auth' { + interface Session { + user: { + id: string; + } & DefaultSession['user']; + } + + interface User { + id: string; + } +}