diff --git a/AUTHORIZATION_IMPLEMENTATION.md b/AUTHORIZATION_IMPLEMENTATION.md index 4424c2e..b2911fc 100644 --- a/AUTHORIZATION_IMPLEMENTATION.md +++ b/AUTHORIZATION_IMPLEMENTATION.md @@ -4,28 +4,52 @@ 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 +## Current State Analysis (Updated: June 25, 2025) + +### ✅ What We Have Implemented -### ✅ 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 +- **Authentication**: NextAuth.js v5 with credentials provider +- **User Management**: Complete user CRUD operations with bcrypt password hashing +- **Database Schema**: Users table with roles, audit logs, sessions +- **API Protection**: Middleware system with role-based access control +- **Session Management**: JWT-based sessions with 30-day expiration +- **Security Features**: Account lockout, failed login tracking, password validation +- **UI Components**: Authentication provider, navigation with user context +- **Auth Pages**: Sign-in page implemented -### ❌ What's Missing -- User authentication system -- Session management -- Role-based access control -- API route protection -- Input validation & sanitization -- Security middleware +### ✅ What's Protected + +- **API Routes**: All major endpoints (projects, contracts, tasks, notes) are protected +- **Role Hierarchy**: admin > project_manager > user > read_only +- **Navigation**: Role-based menu items (admin sees user management) +- **Session Security**: Automatic session management and validation + +### 🔄 Partially Implemented + +- **Auth Pages**: Sign-in exists, missing sign-out and error pages +- **User Interface**: Basic auth integration, could use more polish +- **Admin Features**: User management backend exists, UI needs completion +- **Audit Logging**: Database schema exists, not fully integrated + +### ❌ Still Missing + +- Complete user management UI for admins +- Password reset functionality +- Rate limiting implementation +- Enhanced input validation schemas +- CSRF protection +- Security headers middleware +- Comprehensive error handling +- Email notifications ## 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) @@ -36,181 +60,195 @@ This document outlines the implementation strategy for adding authentication and **Proposed User Roles:** -| Role | Permissions | Use Case | -|------|-------------|----------| -| **Admin** | Full system access, user management | System administrators | +| 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 | +| **User** | View/edit assigned projects/tasks | Regular employees | +| **Read-only** | View-only access to data | Clients, stakeholders | -## Implementation Plan +## Implementation Status -### Phase 1: Foundation Setup +### ✅ Phase 1: Foundation Setup - COMPLETED -#### 1.1 Install Dependencies +#### 1.1 Dependencies - ✅ INSTALLED -```bash -npm install next-auth@beta @auth/better-sqlite3-adapter -npm install bcryptjs zod -npm install @types/bcryptjs # if using TypeScript -``` +- NextAuth.js v5 (beta) +- bcryptjs for password hashing +- Zod for validation +- Better-sqlite3 adapter compatibility -#### 1.2 Environment Configuration +#### 1.2 Environment Configuration - ✅ COMPLETED -Create `.env.local`: -```env -# NextAuth.js Configuration -NEXTAUTH_SECRET=your-super-secret-key-here-minimum-32-characters -NEXTAUTH_URL=http://localhost:3000 +- `.env.local` configured with NEXTAUTH_SECRET and NEXTAUTH_URL +- Database URL configuration +- Development environment setup -# Database -DATABASE_URL=./data/database.sqlite +#### 1.3 Database Schema - ✅ IMPLEMENTED -# 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 -``` +- Users table with roles and security features +- Sessions table for NextAuth.js +- Audit logs table for security tracking +- Proper indexes for performance -#### 1.3 Database Schema Extension +#### 1.4 Initial Admin User - ✅ COMPLETED -Add to `src/lib/init-db.js`: +- `scripts/create-admin.js` script available +- Default admin user: admin@localhost.com / admin123456 -```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 -); +### ✅ Phase 2: Authentication Core - COMPLETED --- 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 -); +#### 2.1 NextAuth.js Configuration - ✅ IMPLEMENTED -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 -); +- **File**: `src/lib/auth.js` +- Credentials provider with email/password +- JWT session strategy with 30-day expiration +- Account lockout after 5 failed attempts (15-minute lockout) +- Password verification with bcrypt +- Failed login attempt tracking +- Session callbacks for role management -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) -); +#### 2.2 API Route Handlers - ✅ IMPLEMENTED --- 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) -); +- **File**: `src/app/api/auth/[...nextauth]/route.js` +- NextAuth.js handlers properly configured --- 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); -``` +#### 2.3 User Management System - ✅ IMPLEMENTED -### Phase 2: Authentication Core +- **File**: `src/lib/userManagement.js` +- Complete CRUD operations for users +- Password hashing and validation +- Role management functions +- User lookup by ID and email -#### 2.1 NextAuth.js Configuration +### ✅ Phase 3: Authorization Middleware - COMPLETED -Create `src/lib/auth.js`: +#### 3.1 API Protection Middleware - ✅ IMPLEMENTED + +- **File**: `src/lib/middleware/auth.js` +- `withAuth()` function for protecting routes +- Role hierarchy enforcement (admin=4, project_manager=3, user=2, read_only=1) +- Helper functions: `withReadAuth`, `withUserAuth`, `withAdminAuth`, `withManagerAuth` +- Proper error handling and status codes + +#### 3.2 Protected API Routes - ✅ IMPLEMENTED + +Example in `src/app/api/projects/route.js`: + +- GET requests require read_only access +- POST requests require user access +- All major API endpoints are protected + +#### 3.3 Session Provider - ✅ IMPLEMENTED + +- **File**: `src/components/auth/AuthProvider.js` +- NextAuth SessionProvider wrapper +- Integrated into root layout + +### 🔄 Phase 4: User Interface - PARTIALLY COMPLETED + +#### 4.1 Authentication Pages - 🔄 PARTIAL + +- ✅ **Sign-in page**: `src/app/auth/signin/page.js` - Complete with form validation +- ❌ **Sign-out page**: Missing +- 🔄 **Error page**: `src/app/auth/error/page.js` - Basic implementation +- ❌ **Unauthorized page**: Missing + +#### 4.2 Navigation Updates - ✅ COMPLETED + +- **File**: `src/components/ui/Navigation.js` +- User session integration with useSession +- Role-based menu items (admin sees user management) +- Sign-out functionality +- Conditional rendering based on auth status + +#### 4.3 User Management Interface - ❌ MISSING + +- Backend exists in userManagement.js +- Admin UI for user CRUD operations needed +- Role assignment interface needed + +### ❌ Phase 5: Security Enhancements - NOT STARTED + +#### 5.1 Input Validation Schemas - ❌ MISSING + +- Zod schemas for API endpoints +- Request validation middleware + +#### 5.2 Rate Limiting - ❌ MISSING + +- Rate limiting middleware +- IP-based request tracking + +#### 5.3 Security Headers - ❌ MISSING + +- CSRF protection +- Security headers middleware +- Content Security Policy ```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" +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") -}) + 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(` + 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(` + ` + ) + .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 @@ -219,87 +257,92 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ ELSE locked_until END WHERE id = ? - `).run(user.id) - - throw new Error("Invalid credentials") - } - - // Reset failed attempts and update last login - db.prepare(` + ` + ).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) - } - } - } -}) + ` + ).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(` + 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) - } + ` + ).run( + userId, + action, + resourceType, + resourceId, + req?.ip || "unknown", + req?.headers?.["user-agent"] || "unknown" + ); + } catch (error) { + console.error("Audit log error:", error); + } } ``` @@ -308,9 +351,9 @@ function logAuditEvent(userId, action, resourceType, resourceId, req = null) { Create `src/app/api/auth/[...nextauth]/route.js`: ```javascript -import { handlers } from "@/lib/auth" +import { handlers } from "@/lib/auth"; -export const { GET, POST } = handlers +export const { GET, POST } = handlers; ``` ### Phase 3: Authorization Middleware @@ -320,111 +363,129 @@ export const { GET, POST } = handlers Create `src/lib/middleware/auth.js`: ```javascript -import { auth } from "@/lib/auth" -import { NextResponse } from "next/server" -import { z } from "zod" +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 -} + 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 } - ) - } - } + 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] + return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole]; } // Helper for read-only operations export function withReadAuth(handler) { - return withAuth(handler, { requiredRole: 'read_only' }) + return withAuth(handler, { requiredRole: "read_only" }); } // Helper for user-level operations export function withUserAuth(handler) { - return withAuth(handler, { requiredRole: 'user' }) + return withAuth(handler, { requiredRole: "user" }); } // Helper for project manager operations export function withManagerAuth(handler) { - return withAuth(handler, { requiredRole: 'project_manager' }) + return withAuth(handler, { requiredRole: "project_manager" }); } // Helper for admin operations export function withAdminAuth(handler) { - return withAuth(handler, { requiredRole: 'admin' }) + return withAuth(handler, { requiredRole: "admin" }); } ``` @@ -433,54 +494,60 @@ export function withAdminAuth(handler) { Create `src/components/auth/ProtectedRoute.js`: ```javascript -"use client" +"use client"; -import { useSession } from "next-auth/react" -import { useRouter } from "next/navigation" -import { useEffect } from "react" +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