diff --git a/app/(auth)/dashboard/page.tsx b/app/(auth)/dashboard/page.tsx new file mode 100644 index 0000000..b8e1a83 --- /dev/null +++ b/app/(auth)/dashboard/page.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function DashboardPage() { + return ( +
DashboardPage
+ ) +} diff --git a/app/events/[eventId]/guests/page.tsx b/app/(auth)/events/[eventId]/guests/page.tsx similarity index 100% rename from app/events/[eventId]/guests/page.tsx rename to app/(auth)/events/[eventId]/guests/page.tsx diff --git a/app/events/create/page.tsx b/app/(auth)/events/create/page.tsx similarity index 100% rename from app/events/create/page.tsx rename to app/(auth)/events/create/page.tsx diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..60035d8 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,21 @@ +'use client' + +import { SessionProvider, useSession } from 'next-auth/react' +import { redirect } from 'next/navigation' +import { ReactNode } from 'react' +import Navbar from '@/components/Navbar' + +export default function AuthLayout({ children }: { children: ReactNode }) { + + return ( + <> + + +
+ {/* Could also add a private header here */} + {children} +
+
+ + ) +} diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx new file mode 100644 index 0000000..a4a066a --- /dev/null +++ b/app/(public)/layout.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react' + +export default function PublicLayout({ children }: { children: ReactNode }) { + return ( +
+ {/* Public site header if any */} + {children} +
+ ) +} \ No newline at end of file diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx new file mode 100644 index 0000000..d3cb67b --- /dev/null +++ b/app/(public)/login/page.tsx @@ -0,0 +1,10 @@ +import LoginForm from '@/components/LoginForm'; +import React, { useState } from 'react' + +export default function LoginPage() { + return ( +
+ +
+ ) +} diff --git a/app/page.tsx b/app/(public)/page.tsx similarity index 100% rename from app/page.tsx rename to app/(public)/page.tsx diff --git a/app/setup/page.tsx b/app/(public)/setup/page.tsx similarity index 100% rename from app/setup/page.tsx rename to app/(public)/setup/page.tsx diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..f58d6e0 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,76 @@ +import NextAuth, { type NextAuthOptions } from 'next-auth' +import CredentialsProvider from 'next-auth/providers/credentials' +import { PrismaClient } from '@prisma/client' +import bcrypt from 'bcrypt' + +const prisma = new PrismaClient() + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + email: { label: 'Email', type: 'email' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) { + console.log('[AUTH] Missing credentials') + return null + } + + const user = await prisma.user.findUnique({ + where: { email: credentials.email }, + }) + + if (!user) { + console.log('[AUTH] User not found') + return null + } + + if (!user.password) { + console.log('[AUTH] User has no password set') + return null + } + + const isValid = await bcrypt.compare(credentials.password, user.password) + if (!isValid) { + console.log('[AUTH] Invalid password') + return null + } + + console.log('[AUTH] Successful login', user.email) + + return { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + } + }, + }), + ], + session: { + strategy: 'jwt', + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id + token.role = user.role + } + return token + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.id as string + session.user.role = token.role as "COUPLE" | "PLANNER" | "GUEST" + } + return session + }, + }, + secret: process.env.NEXTAUTH_SECRET, +} + +const handler = NextAuth(authOptions) +export { handler as GET, handler as POST } diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx new file mode 100644 index 0000000..9393e71 --- /dev/null +++ b/components/LoginForm.tsx @@ -0,0 +1,53 @@ +'use client' + +import { signIn } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import React, { useState } from 'react' + +export default function LoginForm() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const router = useRouter(); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + const result = await signIn('credentials', { + redirect: false, + email, + password, + }) + + console.log('[CLIENT] signIn result:', result) + + if (result?.error) { + setError(result.error) + } else { + router.push('/') + } + } + return ( +
+

Login

+ {error &&

{error}

} + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+ ) +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..27a5add --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,27 @@ +'use client' + +import { useSession, signOut } from "next-auth/react"; + +export default function Navbar() { + const { data: session, status } = useSession(); + + if (status === 'loading') return null; + if (!session?.user) return null + + return ( + + ) +} diff --git a/lib/auth.ts b/lib/auth.ts deleted file mode 100644 index 88b984c..0000000 --- a/lib/auth.ts +++ /dev/null @@ -1,69 +0,0 @@ -// lib/auth.ts -import NextAuth from 'next-auth'; -import { PrismaAdapter } from '@next-auth/prisma-adapter'; -import CredentialsProvider from 'next-auth/providers/credentials'; -import { prisma } from './prisma'; -import type { NextAuthOptions, User } from 'next-auth'; -import bcrypt from "bcrypt"; - -export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(prisma), - providers: [ - CredentialsProvider({ - name: 'Credentials', - credentials: { - email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - throw new Error("Missing email or password"); - } - - const user = await prisma.user.findUnique({ - where: { email: credentials.email }, - }); - - if (!user || !user.password) { - throw new Error("Invalid credentials"); - } - - const isValid = await bcrypt.compare(credentials.password, user.password); - if (!isValid) { - throw new Error("Invalid credentials"); - } - - return { - id: user.id, - email: user.email, - name: user.name, - role: user.role, - }; - }, - }), - ], - session: { - strategy: 'jwt', - }, - callbacks: { - async session({ session, token }: { session: any; token: any }) { - if (session.user) { - session.user.id = token.sub!; - session.user.role = token.role; - } - return session; - }, - async jwt({ token, user }: { token: any; user?: any }) { - if (user) { - token.role = user.role; - } - return token; - }, - }, - secret: process.env.NEXTAUTH_SECRET, -}; - -export const { - handlers: { GET, POST }, - auth, -} = NextAuth(authOptions); diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index 435ed8c..618203b 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -1,20 +1,21 @@ -import NextAuth from "next-auth"; +import NextAuth from "next-auth" declare module "next-auth" { - interface User { - id: string; - role: "COUPLE" | "PLANNER" | "GUEST"; - } - interface Session { user: { - id: string; - email: string; - role: "COUPLE" | "PLANNER" | "GUEST"; - }; + id: string + email: string + role: "COUPLE" | "PLANNER" | "GUEST" + } + } + + interface User { + id: string + role: "COUPLE" | "PLANNER" | "GUEST" } interface JWT { - role: "COUPLE" | "PLANNER" | "GUEST"; + id: string + role: "COUPLE" | "PLANNER" | "GUEST" } }