# Authorization Implementation Guide ## Project Overview This document outlines the implementation strategy for adding authentication and authorization to the Project Management Panel - a Next.js 15 application with SQLite database. ## Current State Analysis ### ✅ What We Have - **Framework**: Next.js 15 with App Router - **Database**: SQLite with better-sqlite3 - **API Routes**: Multiple unprotected endpoints (projects, contracts, tasks, notes) - **UI**: Basic navigation and CRUD interfaces - **Security**: ⚠️ **NONE** - All endpoints are publicly accessible ### ❌ What's Missing - User authentication system - Session management - Role-based access control - API route protection - Input validation & sanitization - Security middleware ## Recommended Implementation Strategy ### 1. Authentication Solution: NextAuth.js **Why NextAuth.js?** - ✅ Native Next.js 15 App Router support - ✅ Database session management - ✅ Built-in security features (CSRF, JWT handling) - ✅ Flexible provider system - ✅ SQLite adapter available ### 2. Role-Based Access Control (RBAC) **Proposed User Roles:** | Role | Permissions | Use Case | |------|-------------|----------| | **Admin** | Full system access, user management | System administrators | | **Project Manager** | Manage all projects/tasks, view reports | Team leads, supervisors | | **User** | View/edit assigned projects/tasks | Regular employees | | **Read-only** | View-only access to data | Clients, stakeholders | ## Implementation Plan ### Phase 1: Foundation Setup #### 1.1 Install Dependencies ```bash npm install next-auth@beta @auth/better-sqlite3-adapter npm install bcryptjs zod npm install @types/bcryptjs # if using TypeScript ``` #### 1.2 Environment Configuration Create `.env.local`: ```env # NextAuth.js Configuration NEXTAUTH_SECRET=your-super-secret-key-here-minimum-32-characters NEXTAUTH_URL=http://localhost:3000 # Database DATABASE_URL=./data/database.sqlite # Optional: Email configuration for password reset EMAIL_SERVER_HOST=smtp.gmail.com EMAIL_SERVER_PORT=587 EMAIL_SERVER_USER=your-email@gmail.com EMAIL_SERVER_PASSWORD=your-app-password EMAIL_FROM=noreply@yourapp.com ``` #### 1.3 Database Schema Extension Add to `src/lib/init-db.js`: ```sql -- Users table CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT CHECK(role IN ('admin', 'project_manager', 'user', 'read_only')) DEFAULT 'user', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, is_active INTEGER DEFAULT 1, last_login TEXT, failed_login_attempts INTEGER DEFAULT 0, locked_until TEXT ); -- NextAuth.js required tables CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), 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, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), sessionToken TEXT UNIQUE NOT NULL, userId TEXT NOT NULL, expires TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS verification_tokens ( identifier TEXT NOT NULL, token TEXT UNIQUE NOT NULL, expires TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (identifier, token) ); -- Audit log table for security tracking CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, action TEXT NOT NULL, resource_type TEXT, resource_id TEXT, ip_address TEXT, user_agent TEXT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, details TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ); -- Create indexes for performance CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(sessionToken); CREATE INDEX IF NOT EXISTS idx_accounts_user ON accounts(userId); CREATE INDEX IF NOT EXISTS idx_audit_user_timestamp ON audit_logs(user_id, timestamp); ``` ### Phase 2: Authentication Core #### 2.1 NextAuth.js Configuration Create `src/lib/auth.js`: ```javascript import NextAuth from "next-auth" import CredentialsProvider from "next-auth/providers/credentials" import { BetterSQLite3Adapter } from "@auth/better-sqlite3-adapter" import db from "./db.js" import bcrypt from "bcryptjs" import { z } from "zod" const loginSchema = z.object({ email: z.string().email("Invalid email format"), password: z.string().min(6, "Password must be at least 6 characters") }) export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: BetterSQLite3Adapter(db), session: { strategy: "database", maxAge: 30 * 24 * 60 * 60, // 30 days updateAge: 24 * 60 * 60, // 24 hours }, providers: [ CredentialsProvider({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" } }, async authorize(credentials, req) { try { // Validate input const validatedFields = loginSchema.parse(credentials) // Check if user exists and is active const user = db.prepare(` SELECT id, email, name, password_hash, role, is_active, failed_login_attempts, locked_until FROM users WHERE email = ? AND is_active = 1 `).get(validatedFields.email) if (!user) { throw new Error("Invalid credentials") } // Check if account is locked if (user.locked_until && new Date(user.locked_until) > new Date()) { throw new Error("Account temporarily locked") } // Verify password const isValidPassword = await bcrypt.compare(validatedFields.password, user.password_hash) if (!isValidPassword) { // Increment failed attempts db.prepare(` UPDATE users SET failed_login_attempts = failed_login_attempts + 1, locked_until = CASE WHEN failed_login_attempts >= 4 THEN datetime('now', '+15 minutes') ELSE locked_until END WHERE id = ? `).run(user.id) throw new Error("Invalid credentials") } // Reset failed attempts and update last login db.prepare(` UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login = CURRENT_TIMESTAMP WHERE id = ? `).run(user.id) // Log successful login logAuditEvent(user.id, 'LOGIN_SUCCESS', 'user', user.id, req) return { id: user.id, email: user.email, name: user.name, role: user.role } } catch (error) { console.error("Login error:", error) return null } } }) ], callbacks: { async jwt({ token, user, account }) { if (user) { token.role = user.role token.userId = user.id } return token }, async session({ session, token, user }) { if (token) { session.user.id = token.userId || token.sub session.user.role = token.role || user?.role } return session }, async signIn({ user, account, profile, email, credentials }) { // Additional sign-in logic if needed return true } }, pages: { signIn: '/auth/signin', signOut: '/auth/signout', error: '/auth/error' }, events: { async signOut({ session, token }) { if (session?.user?.id) { logAuditEvent(session.user.id, 'LOGOUT', 'user', session.user.id) } } } }) // Audit logging helper function logAuditEvent(userId, action, resourceType, resourceId, req = null) { try { db.prepare(` INSERT INTO audit_logs (user_id, action, resource_type, resource_id, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?) `).run( userId, action, resourceType, resourceId, req?.ip || 'unknown', req?.headers?.['user-agent'] || 'unknown' ) } catch (error) { console.error("Audit log error:", error) } } ``` #### 2.2 API Route Handlers Create `src/app/api/auth/[...nextauth]/route.js`: ```javascript import { handlers } from "@/lib/auth" export const { GET, POST } = handlers ``` ### Phase 3: Authorization Middleware #### 3.1 API Protection Middleware Create `src/lib/middleware/auth.js`: ```javascript import { auth } from "@/lib/auth" import { NextResponse } from "next/server" import { z } from "zod" // Role hierarchy for permission checking const ROLE_HIERARCHY = { 'admin': 4, 'project_manager': 3, 'user': 2, 'read_only': 1 } export function withAuth(handler, options = {}) { return async (req, context) => { try { const session = await auth() // Check if user is authenticated if (!session?.user) { return NextResponse.json( { error: "Authentication required" }, { status: 401 } ) } // Check if user account is active const user = db.prepare("SELECT is_active FROM users WHERE id = ?").get(session.user.id) if (!user?.is_active) { return NextResponse.json( { error: "Account deactivated" }, { status: 403 } ) } // Check role-based permissions if (options.requiredRole && !hasPermission(session.user.role, options.requiredRole)) { logAuditEvent(session.user.id, 'ACCESS_DENIED', options.resource || 'api', req.url) return NextResponse.json( { error: "Insufficient permissions" }, { status: 403 } ) } // Check resource-specific permissions if (options.checkResourceAccess) { const hasAccess = await options.checkResourceAccess(session.user, context.params) if (!hasAccess) { return NextResponse.json( { error: "Access denied to this resource" }, { status: 403 } ) } } // Validate request body if schema provided if (options.bodySchema && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) { try { const body = await req.json() options.bodySchema.parse(body) } catch (error) { return NextResponse.json( { error: "Invalid request data", details: error.errors }, { status: 400 } ) } } // Add user info to request req.user = session.user req.session = session // Call the original handler return await handler(req, context) } catch (error) { console.error("Auth middleware error:", error) return NextResponse.json( { error: "Internal server error" }, { status: 500 } ) } } } export function hasPermission(userRole, requiredRole) { return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole] } // Helper for read-only operations export function withReadAuth(handler) { return withAuth(handler, { requiredRole: 'read_only' }) } // Helper for user-level operations export function withUserAuth(handler) { return withAuth(handler, { requiredRole: 'user' }) } // Helper for project manager operations export function withManagerAuth(handler) { return withAuth(handler, { requiredRole: 'project_manager' }) } // Helper for admin operations export function withAdminAuth(handler) { return withAuth(handler, { requiredRole: 'admin' }) } ``` #### 3.2 Client-Side Route Protection Create `src/components/auth/ProtectedRoute.js`: ```javascript "use client" import { useSession } from "next-auth/react" import { useRouter } from "next/navigation" import { useEffect } from "react" export function ProtectedRoute({ children, requiredRole = null, fallback = null }) { const { data: session, status } = useSession() const router = useRouter() useEffect(() => { if (status === "loading") return // Still loading if (!session) { router.push('/auth/signin') return } if (requiredRole && !hasPermission(session.user.role, requiredRole)) { router.push('/unauthorized') return } }, [session, status, router, requiredRole]) if (status === "loading") { return