diff --git a/AUTHORIZATION_IMPLEMENTATION.md b/AUTHORIZATION_IMPLEMENTATION.md new file mode 100644 index 0000000..4424c2e --- /dev/null +++ b/AUTHORIZATION_IMPLEMENTATION.md @@ -0,0 +1,696 @@ +# 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