diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 00000000..aab004ce --- /dev/null +++ b/middleware.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { getToken } from 'next-auth/jwt'; + +const publicPaths = ['/', '/login', '/signup', '/api/auth/signin', '/api/auth/signout']; + +export async function middleware(req: NextRequest) { + const { pathname } = req.nextUrl; + const token = await getToken({ req }); + + // Allow access to public paths and static files + if ( + publicPaths.some(path => path === pathname) || + pathname.startsWith('/_next') || + pathname.startsWith('/favicon.ico') || + pathname.startsWith('/public/') + ) { + // If user is logged in and tries to access auth pages, redirect to dashboard + if (token && (pathname.startsWith('/login') || pathname.startsWith('/signup'))) { + return NextResponse.redirect(new URL('/dashboard', req.url)); + } + return NextResponse.next(); + } + + // If no token and not a public path, redirect to login + if (!token) { + const loginUrl = new URL('/login', req.url); + loginUrl.searchParams.set('callbackUrl', pathname); + return NextResponse.redirect(loginUrl); + } + + return NextResponse.next(); +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder + */ + '/((?!api|_next/static|_next/image|favicon.ico|public).*)', + ], +}; diff --git a/prisma/migrations/20250726215047_init/migration.sql b/prisma/migrations/20250726215047_init/migration.sql new file mode 100644 index 00000000..d8ed94c4 --- /dev/null +++ b/prisma/migrations/20250726215047_init/migration.sql @@ -0,0 +1,57 @@ +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL PRIMARY KEY, + "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, + CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT, + "email" TEXT, + "email_verified" DATETIME, + "image" TEXT, + "password" TEXT +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" DATETIME NOT NULL, + CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" DATETIME NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..2a5a4441 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d58fc4ec..d26a36c7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,8 +7,8 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "sqlite" + url = "file:./dev.db" } model Account { @@ -17,12 +17,12 @@ model Account { type String provider String providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text + refresh_token String? + access_token String? expires_at Int? token_type String? scope String? - id_token String? @db.Text + id_token String? session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 95cc195e..dac21021 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,6 +1,28 @@ -import NextAuth from "next-auth"; -import { authOptions } from "@/lib/auth"; +import NextAuth from 'next-auth'; +import { authConfig } from '@/auth'; -const handler = NextAuth(authOptions); +// Initialize NextAuth with the configuration +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + ...authConfig, + // Enable debug logs in development + debug: process.env.NODE_ENV === 'development', + // Ensure cookies are secure in production + cookies: { + sessionToken: { + name: `__Secure-next-auth.session-token`, + options: { + httpOnly: true, + sameSite: 'lax', // CSRF protection + path: '/', + secure: process.env.NODE_ENV === 'production', + }, + }, + }, +}); -export { handler as GET, handler as POST }; \ No newline at end of file +export { GET, POST, auth, signIn, signOut }; \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index b4f6ec36..37838603 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { signOut, useSession } from 'next-auth/react'; +import { signOut } from '@/app/api/auth/[...nextauth]/route'; +import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; @@ -22,8 +23,9 @@ export default function DashboardPage() { ); } - if (!session) { - return null; // Will be redirected by the useEffect + if (status === 'unauthenticated') { + router.push('/login'); + return null; } return ( @@ -34,9 +36,12 @@ export default function DashboardPage() {