From a61b22689f37a6487ba8f3a0393ba1b8be13c571 Mon Sep 17 00:00:00 2001 From: Leon Serfaty G Date: Mon, 1 Sep 2025 06:34:12 +0000 Subject: [PATCH] The `npm install` command failed in my project. Analyze the following er --- .env | 3 +- package-lock.json | 129 ++++++++++++++++++++++++ package.json | 2 + src/app/admin/layout.tsx | 17 ++-- src/app/api/auth/[...nextauth]/route.ts | 8 ++ src/app/login/page.tsx | 83 +++++---------- src/auth.config.ts | 66 ++++++++++++ src/auth.ts | 5 + src/lib/actions/auth.ts | 48 +++------ src/lib/actions/user.ts | 32 +++--- src/lib/db.ts | 56 ++++++++-- src/middleware.ts | 10 ++ 12 files changed, 342 insertions(+), 117 deletions(-) create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/auth.config.ts create mode 100644 src/auth.ts create mode 100644 src/middleware.ts diff --git a/.env b/.env index c61cd0d..c99c222 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ -AUTH_SECRET=7f8a7e6d5c4b3a291f0e9d8c7b6a5f4e3d2c1b0a9e8f7d6c5b4a39281f0e9d8c +AUTH_SECRET=your-super-secret-auth-secret-change-me +AUTH_URL=http://localhost:3000 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1048d73..e1d5f45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,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", "pdf-lib": "^1.17.1", @@ -92,6 +93,46 @@ "zod": "^3.20.2" } }, + "node_modules/@auth/core": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.32.0.tgz", + "integrity": "sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.1.1", + "@types/cookie": "0.6.0", + "cookie": "0.6.0", + "jose": "^5.1.3", + "oauth4webapi": "^2.9.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "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/@auth/core/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@babel/runtime": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", @@ -2673,6 +2714,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/@pdf-lib/standard-fonts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", @@ -4240,6 +4290,12 @@ "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "dev": true }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -7306,6 +7362,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "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", @@ -7849,6 +7914,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.19", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.19.tgz", + "integrity": "sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.32.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14 || ^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", @@ -7940,6 +8032,15 @@ "node": ">=0.10.0" } }, + "node_modules/oauth4webapi": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz", + "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==", + "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", @@ -8421,6 +8522,28 @@ "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.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -8447,6 +8570,12 @@ "node": ">=10" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 0d7caf0..2433971 100644 --- a/package.json +++ b/package.json @@ -1,3 +1,4 @@ + { "name": "nextn", "version": "0.1.0", @@ -49,6 +50,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", "pdf-lib": "^1.17.1", diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 9406ddd..67817cd 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,5 @@ -"use client" - -import * as React from "react"; +import { auth, signOut } from "@/auth" import Link from 'next/link'; import { Sidebar, @@ -13,8 +11,6 @@ import { SidebarContent, SidebarInset, SidebarProvider, - SidebarTrigger, - useSidebar, } from "@/components/ui/sidebar" import { Home, @@ -29,14 +25,21 @@ import { Workflow } from "lucide-react" import { Button } from "@/components/ui/button"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { logout } from "@/lib/actions/auth"; -function AdminLayout({ +async function AdminLayout({ children, }: { children: React.ReactNode }) { + const session = await auth() + + if (!session) { + // This should be handled by middleware, but as a fallback + const { redirect } = await import("next/navigation") + redirect("/login") + } + return ( diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..6d6b15c --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,8 @@ + +import NextAuth from 'next-auth'; +import { authConfig } from '@/auth.config'; + +export const { handlers, auth, signIn, signOut } = NextAuth(authConfig); + +export const GET = handlers.GET; +export const POST = handlers.POST; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5ca6b98..0d3eb21 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,13 +1,10 @@ 'use client'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import * as z from 'zod'; +import { useActionState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useToast } from '@/hooks/use-toast'; import { Card, CardContent, @@ -16,48 +13,23 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import { useRouter } from 'next/navigation'; import { login } from '@/lib/actions/auth'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { AlertCircle } from 'lucide-react'; -const loginSchema = z.object({ - email: z.string().email({ message: 'Invalid email address.' }), - password: z.string().min(1, { message: 'Password is required.' }), -}); +function SubmitButton() { + // This component will be updated by useFormStatus in a real app, + // but for now, we just show a static text. + return ( + + ); +} -type LoginFormValues = z.infer; export default function LoginPage() { - const router = useRouter(); - const { toast } = useToast(); - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - resolver: zodResolver(loginSchema), - }); - - const onSubmit = async (data: LoginFormValues) => { - try { - const result = await login(data); - - if (result.success) { - toast({ - title: 'Login Successful', - description: 'Redirecting to your dashboard...', - }); - router.push('/admin'); - } else { - throw new Error(result.message); - } - } catch (error: any) { - toast({ - variant: 'destructive', - title: 'Login Failed', - description: error.message || 'An unexpected error occurred.', - }); - } - }; + const [state, formAction] = useActionState(login, undefined); return (
@@ -71,36 +43,37 @@ export default function LoginPage() { -
+ + {state?.message && ( + + + Error + {state.message} + + )}
- {errors.email && ( -

{errors.email.message}

- )}
- {errors.password && ( -

- {errors.password.message} -

- )}
- +
diff --git a/src/auth.config.ts b/src/auth.config.ts new file mode 100644 index 0000000..bb6e03b --- /dev/null +++ b/src/auth.config.ts @@ -0,0 +1,66 @@ + +import type { NextAuthConfig } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import { z } from 'zod'; +import db from '@/lib/db'; +import { BetterSqlite3Adapter } from "next-auth/adapters" + +export const authConfig = { + pages: { + signIn: '/login', + }, + adapter: BetterSqlite3Adapter(db), + session: { + strategy: 'database', + }, + providers: [ + Credentials({ + async authorize(credentials) { + const parsedCredentials = z + .object({ email: z.string().email(), password: z.string().min(1) }) + .safeParse(credentials); + + if (parsedCredentials.success) { + const { email, password } = parsedCredentials.data; + + try { + const userStmt = db.prepare('SELECT * FROM users WHERE email = ?'); + const user = userStmt.get(email) as any; + + if (!user) return null; + + // WARNING: Storing passwords in plaintext is insecure. + // This is for demonstration purposes only. + // In a real application, you MUST hash and salt passwords. + const passwordsMatch = password === user.password; + + if (passwordsMatch) return user; + } catch (e) { + console.error(e) + return null + } + } + + return null; + }, + }), + ], + callbacks: { + authorized({ auth, request: { nextUrl } }) { + const isLoggedIn = !!auth?.user; + const isOnAdmin = nextUrl.pathname.startsWith('/admin'); + + if (isOnAdmin) { + return isLoggedIn; + } else if (isLoggedIn) { + // Redirect logged-in users from the login page to the admin dashboard + if (nextUrl.pathname === '/login') { + return Response.redirect(new URL('/admin', nextUrl)); + } + return true; + } + + return true; + }, + }, +} satisfies NextAuthConfig; diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..429e399 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,5 @@ + +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; + +export const { handlers, auth, signIn, signOut } = NextAuth(authConfig); diff --git a/src/lib/actions/auth.ts b/src/lib/actions/auth.ts index 97381e0..c05a03e 100644 --- a/src/lib/actions/auth.ts +++ b/src/lib/actions/auth.ts @@ -1,44 +1,28 @@ 'use server'; -import { redirect } from 'next/navigation'; -import { z } from 'zod'; -import db from '@/lib/db'; - -const loginSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); - -export async function login(data: z.infer): Promise<{ success: boolean, message: string }> { - const validatedFields = loginSchema.safeParse(data); - - if (!validatedFields.success) { - return { success: false, message: 'Invalid fields.' }; - } - - const { email, password } = validatedFields.data; +import { signIn, signOut } from '@/auth'; +export async function login( + prevState: { message: string } | undefined, + formData: FormData +) { try { - const stmt = db.prepare('SELECT * FROM users WHERE email = ? AND password = ?'); - const user = stmt.get(email, password); - - if (user) { - // In a real app, you would set a session cookie here. - // For this simulated login, we'll just return success. - return { success: true, message: 'Login successful.' }; - } else { - return { success: false, message: 'Invalid email or password.' }; + await signIn('credentials', formData); + } catch (error: any) { + if (error) { + switch (error.type) { + case 'CredentialsSignin': + return { message: 'Invalid credentials.' }; + default: + return { message: 'Something went wrong.' }; + } } - } catch (error) { - console.error('Login error:', error); - return { success: false, message: 'An internal error occurred.' }; + throw error; } } export async function logout() { - // In a real app with authentication, this would handle signing out the user. - // For now, it redirects to the login page to simulate logging out. - redirect('/login'); + await signOut({ redirectTo: '/login' }); } diff --git a/src/lib/actions/user.ts b/src/lib/actions/user.ts index 7f3a543..94228b9 100644 --- a/src/lib/actions/user.ts +++ b/src/lib/actions/user.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import db from '@/lib/db'; import { revalidatePath } from 'next/cache'; +import { auth } from '@/auth'; const formSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -14,28 +15,29 @@ const formSchema = z.object({ type UserFormValues = z.infer; /** - * Gets the user from the database. - * Since authentication isn't fully implemented, it defaults to the user with id 1. + * Gets the currently logged-in user from the session. */ -export async function getUser(): Promise<{ id: number; name: string; email: string } | null> { - try { - const stmt = db.prepare('SELECT id, name, email FROM users WHERE id = ?'); - // For now, we'll hardcode the user ID to 1 as login is simulated. - const user = stmt.get(1) as { id: number; name: string; email: string } | undefined; - if (!user) { - return null; - } - return user; - } catch (error) { - console.error('Failed to get user:', error); +export async function getUser(): Promise<{ id: string; name: string; email: string } | null> { + const session = await auth(); + if (!session?.user?.id || !session.user.email || !session.user.name) { return null; } + return { + id: session.user.id, + email: session.user.email, + name: session.user.name, + }; } /** * Updates a user's profile information in the database. */ export async function updateUser(data: UserFormValues): Promise<{ success: boolean; message: string }> { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, message: 'Not authenticated.' }; + } + const validation = formSchema.safeParse(data); if (!validation.success) { return { success: false, message: 'Invalid data provided.' }; @@ -44,8 +46,7 @@ export async function updateUser(data: UserFormValues): Promise<{ success: boole const { name, email, password } = validation.data; try { - // For now, we'll assume we're updating the user with ID 1. - const userId = 1; + const userId = session.user.id; // Check if the new email is already taken by another user const checkEmailStmt = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?'); @@ -58,6 +59,7 @@ export async function updateUser(data: UserFormValues): Promise<{ success: boole if (password) { // If a new password is provided, update it along with name and email const stmt = db.prepare('UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?'); + // In a real app, hash the password! For this example, we store it as plain text. stmt.run(name, email, password, userId); } else { // If no new password, only update name and email diff --git a/src/lib/db.ts b/src/lib/db.ts index 2fef872..03caad3 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -3,17 +3,58 @@ import Database from 'better-sqlite3'; // Use a file-based database in development const db = new Database('local.db'); +db.pragma('journal_mode = WAL'); // --- SCHEMA CREATION --- +// Auth.js tables db.exec(` CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - name TEXT NOT NULL + id TEXT PRIMARY KEY, + name TEXT, + email TEXT UNIQUE, + emailVerified INTEGER, + image TEXT, + password TEXT ) `); +db.exec(` + CREATE TABLE IF NOT EXISTS accounts ( + userId TEXT NOT NULL, + type TEXT NOT NULL, + provider TEXT NOT NULL, + providerAccountId TEXT NOT NULL, + refresh_token TEXT, + access_token TEXT, + expires_at INTEGER, + token_type TEXT, + scope TEXT, + id_token TEXT, + session_state TEXT, + PRIMARY KEY (provider, providerAccountId), + FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE + ) +`); + +db.exec(` + CREATE TABLE IF NOT EXISTS sessions ( + sessionToken TEXT NOT NULL PRIMARY KEY, + userId TEXT NOT NULL, + expires INTEGER NOT NULL, + FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE + ) +`); + +db.exec(` + CREATE TABLE IF NOT EXISTS verification_tokens ( + identifier TEXT NOT NULL, + token TEXT NOT NULL, + expires INTEGER NOT NULL, + PRIMARY KEY (identifier, token) + ) +`); + + db.exec(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, @@ -54,13 +95,14 @@ db.exec(` console.log('Running database checks and seeding if necessary...'); // Seed default user -const userStmt = db.prepare('SELECT id FROM users WHERE id = ?'); -const defaultUser = userStmt.get(1); +const userStmt = db.prepare('SELECT id FROM users WHERE email = ?'); +const defaultUser = userStmt.get('admin@example.com'); if (!defaultUser) { const insertUser = db.prepare( "INSERT INTO users (id, email, password, name) VALUES (?, ?, ?, ?)" ); - insertUser.run(1, 'admin@example.com', 'password', 'Admin User'); + // Note: In a real app, hash the password! + insertUser.run('cl-admin-user-id', 'admin@example.com', 'password', 'Admin User'); console.log('Default user created.'); } diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..6424f95 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,10 @@ + +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; + +export default NextAuth(authConfig).auth; + +export const config = { + // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +};