diff --git a/local.db-shm b/local.db-shm index f90e7a6..d9dfb5a 100644 Binary files a/local.db-shm and b/local.db-shm differ diff --git a/next.config.ts b/next.config.ts index f25f324..dfc3c22 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,6 @@ -import type {NextConfig} from 'next'; -const nextConfig: NextConfig = { +/** @type {import('next').NextConfig} */ +const nextConfig = { /* config options here */ output: 'standalone', typescript: { @@ -21,4 +21,4 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; +module.exports = nextConfig; diff --git a/package.json b/package.json index a0acf5a..05bc4ee 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,3 @@ - { "name": "nextn", "version": "0.1.0", @@ -14,64 +13,63 @@ "db:seed": "tsx scripts/seed.ts" }, "dependencies": { + "@next-auth/better-sqlite3-adapter": "^0.3.1", "@genkit-ai/googleai": "^1.14.1", - "@genkit-ai/next": "^1.14.1", - "@hookform/resolvers": "^4.1.3", - "@radix-ui/react-accordion": "^1.2.3", - "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-tooltip": "^1.1.8", - "better-sqlite3": "^11.1.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-menubar": "^1.0.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tooltip": "^1.0.7", + "better-sqlite3": "^9.4.3", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", "date-fns": "^3.6.0", - "dotenv": "^16.5.0", - "embla-carousel-react": "^8.6.0", - "firebase": "^11.9.1", - "framer-motion": "^11.3.12", - "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", + "dotenv": "^16.4.5", + "embla-carousel-react": "^8.0.0", + "firebase": "^10.9.0", + "framer-motion": "^11.0.20", + "genkit": "latest", + "lucide-react": "^0.359.0", + "next": "14.2.35", + "next-auth": "4.24.7", + "nodemailer": "^6.9.13", "pdf-lib": "^1.17.1", - "react": "^18.3.1", - "react-day-picker": "^8.10.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.54.2", - "recharts": "^2.15.1", - "tailwind-merge": "^3.0.1", + "react": "^18.2.0", + "react-day-picker": "^8.10.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.51.1", + "recharts": "^2.12.3", + "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", - "zod": "^3.24.2" + "zod": "^3.22.4" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.11", + "@types/better-sqlite3": "^7.6.9", "@types/node": "^20", - "@types/nodemailer": "^6.4.15", + "@types/nodemailer": "^6.4.14", "@types/react": "^18", "@types/react-dom": "^18", - "genkit-cli": "^1.14.1", + "genkit-cli": "latest", "postcss": "^8", "tailwindcss": "^3.4.1", - "tsx": "^4.16.2", + "tsx": "^4.7.1", "typescript": "^5" } } diff --git a/src/ai/genkit.ts b/src/ai/genkit.ts index cbf2594..eac7929 100644 --- a/src/ai/genkit.ts +++ b/src/ai/genkit.ts @@ -3,5 +3,4 @@ import {googleAI} from '@genkit-ai/googleai'; export const ai = genkit({ plugins: [googleAI()], - model: 'googleai/gemini-2.0-flash', }); diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index d0a7a10..6f9cc81 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,5 +1,5 @@ -import { auth, signOut } from "@/auth" +import { auth } from "@/auth" import Link from 'next/link'; import { Sidebar, @@ -26,6 +26,7 @@ import { } from "lucide-react" import { Button } from "@/components/ui/button"; import { logout } from "@/lib/actions/auth"; +import { redirect } from "next/navigation"; async function AdminLayout({ children, @@ -35,8 +36,6 @@ async function AdminLayout({ const session = await auth() if (!session) { - // This should be handled by middleware, but as a fallback - const { redirect } = await import("next/navigation") redirect("/auth") } diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 0a98352..eac77c8 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,7 @@ -import { handlers } from '@/auth'; -export const { GET, POST } = handlers; +import { authOptions } from "@/auth"; +import NextAuth from "next-auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST } diff --git a/src/app/globals.css b/src/app/globals.css index 6311183..e8c73cf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -4,26 +4,35 @@ @layer base { :root { - --background: 222 47% 11%; - --foreground: 0 0% 98%; - --card: 224 35% 15%; - --card-foreground: 0 0% 98%; - --popover: 222 47% 11%; - --popover-foreground: 0 0% 98%; - --primary: 221 100% 58%; - --primary-foreground: 0 0% 9%; - --secondary: 222 25% 20%; - --secondary-foreground: 0 0% 98%; - --muted: 222 25% 20%; - --muted-foreground: 222 10% 63.9%; - --accent: 263 47% 53%; - --accent-foreground: 0 0% 98%; + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221 83% 53%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 224 35% 20%; - --input: 222 25% 20%; - --ring: 221 100% 58%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; --radius: 0.5rem; + + --sidebar-background: 222 47% 11%; + --sidebar-foreground: 0 0% 98%; + --sidebar-primary: 221 100% 58%; + --sidebar-primary-foreground: 0 0% 9%; + --sidebar-accent: 222 25% 20%; + --sidebar-accent-foreground: 0 0% 98%; + --sidebar-border: 224 35% 20%; + --sidebar-ring: 221 100% 58%; } .dark { @@ -46,6 +55,15 @@ --border: 224 35% 20%; --input: 222 25% 20%; --ring: 221 100% 58%; + + --sidebar-background: 222 47% 11%; + --sidebar-foreground: 0 0% 98%; + --sidebar-primary: 221 100% 58%; + --sidebar-primary-foreground: 0 0% 9%; + --sidebar-accent: 222 25% 20%; + --sidebar-accent-foreground: 0 0% 98%; + --sidebar-border: 224 35% 20%; + --sidebar-ring: 221 100% 58%; } } diff --git a/src/auth.config.ts b/src/auth.config.ts index c3a1bd3..f6c3670 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -1,30 +1,8 @@ import type { NextAuthConfig } from 'next-auth'; - + export const authConfig = { pages: { signIn: '/auth', }, - providers: [ - // The Credentials provider logic has been moved to src/auth.ts - // to prevent the database module from being bundled with middleware. - ], - 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 === '/auth') { - return Response.redirect(new URL('/admin', nextUrl)); - } - return true; - } - - return true; - }, - }, } satisfies NextAuthConfig; diff --git a/src/auth.ts b/src/auth.ts index 5427421..39875ae 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,14 +1,22 @@ -import NextAuth from 'next-auth'; -import { authConfig } from './auth.config'; -import Credentials from 'next-auth/providers/credentials'; +import NextAuth, { NextAuthOptions } from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; import { z } from 'zod'; import { getUserByEmail } from '@/lib/actions/user'; +import getDb from './lib/db'; +import { BetterSqlite3Adapter } from '@next-auth/better-sqlite3-adapter'; -export const { handlers, auth, signIn, signOut } = NextAuth({ - ...authConfig, +const db = getDb(); + +export const authOptions: NextAuthOptions = { + adapter: BetterSqlite3Adapter(db), providers: [ - Credentials({ + CredentialsProvider({ + name: 'credentials', + credentials: { + email: { label: 'email', type: 'text' }, + password: { label: 'password', type: 'password' }, + }, async authorize(credentials) { const parsedCredentials = z .object({ email: z.string().email(), password: z.string().min(1) }) @@ -20,13 +28,11 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ const user = await getUserByEmail(email); if (!user || !user.password) 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. + // This is a temporary solution for the demo. + // In a real application, you should hash and compare passwords securely. const passwordsMatch = password === user.password; if (passwordsMatch) { - // The user object returned here will be encoded in the JWT. return { id: user.id, name: user.name, email: user.email }; } } @@ -35,5 +41,27 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ return null; }, }), - ] -}); + ], + pages: { + signIn: '/auth', + }, + session: { + strategy: 'jwt', + }, + 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; + } + } +}; + +export const { handlers, auth, signIn, signOut } = NextAuth(authOptions); diff --git a/src/lib/actions/auth.ts b/src/lib/actions/auth.ts index ef2a38b..ddfa738 100644 --- a/src/lib/actions/auth.ts +++ b/src/lib/actions/auth.ts @@ -1,25 +1,22 @@ + 'use server'; import { signIn, signOut } from '@/auth'; -import { AuthError } from 'next-auth'; export async function login( prevState: { message: string } | undefined, formData: FormData ) { - try { - await signIn('credentials', formData); - } catch (error) { - if (error instanceof AuthError) { - switch (error.type) { - case 'CredentialsSignin': - return { message: 'Invalid credentials.' }; - default: - return { message: 'Something went wrong. Please try again.' }; - } + try { + await signIn('credentials', Object.fromEntries(formData)); + return { message: "Successfully signed in." } + } catch (error) { + console.error("Login error:", error); + if ((error as Error).message.includes('CredentialsSignin')) { + return { message: 'Invalid credentials.' }; + } + return { message: 'Something went wrong. Please try again.' }; } - throw error; - } } diff --git a/src/lib/actions/user.ts b/src/lib/actions/user.ts index ffc1530..b369850 100644 --- a/src/lib/actions/user.ts +++ b/src/lib/actions/user.ts @@ -77,7 +77,7 @@ export async function updateUser(data: UserFormValues): Promise<{ success: boole return { success: false, message: 'This email is already taken.' }; } - if (password) { + if (password && password.length > 0) { // 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. diff --git a/src/middleware.ts b/src/middleware.ts index d9d8e3e..51fa375 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,10 +1,7 @@ -import NextAuth from 'next-auth'; -import { authConfig } from '@/auth.config'; - -export default NextAuth(authConfig).auth; - +export { default } from "next-auth/middleware" + export const config = { // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], + matcher: ['/admin/:path*'], };