Files
panel/AUTHORIZATION_IMPLEMENTATION.md

18 KiB

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

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

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:

# 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:

-- 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:

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:

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:

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:

"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 <div className="flex justify-center items-center h-64">Loading...</div>
  }
  
  if (!session) {
    return fallback || <div>Redirecting to login...</div>
  }
  
  if (requiredRole && !hasPermission(session.user.role, requiredRole)) {
    return fallback || <div>Access denied</div>
  }
  
  return children
}

function hasPermission(userRole, requiredRole) {
  const roleHierarchy = {
    'admin': 4,
    'project_manager': 3,
    'user': 2,
    'read_only': 1
  }
  
  return roleHierarchy[userRole] >= roleHierarchy[requiredRole]
}

Phase 4: User Interface Components

4.1 Authentication Pages

Pages to create:

  • src/app/auth/signin/page.js - Login form
  • src/app/auth/signout/page.js - Logout confirmation
  • src/app/auth/error/page.js - Error handling
  • src/app/unauthorized/page.js - Access denied page

4.2 Navigation Updates

Update src/components/ui/Navigation.js to include:

  • Login/logout buttons
  • User info display
  • Role-based menu items

4.3 User Management Interface

For admin users:

  • User listing and management
  • Role assignment
  • Account activation/deactivation

Phase 5: Security Enhancements

5.1 Input Validation Schemas

Create src/lib/schemas/ with Zod schemas for all API endpoints:

// src/lib/schemas/project.js
import { z } from "zod"

export const createProjectSchema = z.object({
  contract_id: z.number().int().positive(),
  project_name: z.string().min(1).max(255),
  project_number: z.string().min(1).max(50),
  address: z.string().optional(),
  // ... other fields
})

export const updateProjectSchema = createProjectSchema.partial()

5.2 Rate Limiting

Implement rate limiting for sensitive endpoints:

// src/lib/middleware/rateLimit.js
const attempts = new Map()

export function withRateLimit(handler, options = { maxAttempts: 5, windowMs: 15 * 60 * 1000 }) {
  return async (req, context) => {
    const key = req.ip || 'unknown'
    const now = Date.now()
    const window = attempts.get(key) || { count: 0, resetTime: now + options.windowMs }
    
    if (now > window.resetTime) {
      window.count = 1
      window.resetTime = now + options.windowMs
    } else {
      window.count++
    }
    
    attempts.set(key, window)
    
    if (window.count > options.maxAttempts) {
      return NextResponse.json(
        { error: "Too many requests" }, 
        { status: 429 }
      )
    }
    
    return handler(req, context)
  }
}

Implementation Checklist

Phase 1: Foundation

  • Install dependencies
  • Create environment configuration
  • Extend database schema
  • Create initial admin user script

Phase 2: Authentication

  • Configure NextAuth.js
  • Create API route handlers
  • Test login/logout functionality

Phase 3: Authorization

  • Implement API middleware
  • Protect existing API routes
  • Create client-side route protection

Phase 4: User Interface

  • Create authentication pages
  • Update navigation component
  • Build user management interface

Phase 5: Security

  • Add input validation to all endpoints
  • Implement rate limiting
  • Add audit logging
  • Create security headers middleware

Security Best Practices

1. Password Security

  • Minimum 8 characters
  • Require special characters, numbers
  • Hash with bcrypt (cost factor 12+)
  • Implement password history

2. Session Security

  • Secure cookies
  • Session rotation
  • Timeout handling
  • Device tracking

3. API Security

  • Input validation on all endpoints
  • SQL injection prevention (prepared statements)
  • XSS protection
  • CSRF tokens

4. Audit & Monitoring

  • Log all authentication events
  • Monitor failed login attempts
  • Track permission changes
  • Alert on suspicious activity

Testing Strategy

1. Authentication Tests

  • Valid/invalid login attempts
  • Password reset functionality
  • Session expiration
  • Account lockout

2. Authorization Tests

  • Role-based access control
  • API endpoint protection
  • Resource-level permissions
  • Privilege escalation attempts

3. Security Tests

  • SQL injection attempts
  • XSS attacks
  • CSRF attacks
  • Rate limiting

Deployment Considerations

1. Environment Variables

  • Use strong, random secrets
  • Different keys per environment
  • Secure secret management

2. Database Security

  • Regular backups
  • Encryption at rest
  • Network security
  • Access logging

3. Application Security

  • HTTPS enforcement
  • Security headers
  • Content Security Policy
  • Regular security updates

Migration Strategy

1. Development Phase

  • Implement on development branch
  • Test thoroughly with sample data
  • Document all changes

2. Staging Deployment

  • Deploy to staging environment
  • Performance testing
  • Security testing
  • User acceptance testing

3. Production Deployment

  • Database backup before migration
  • Gradual rollout
  • Monitor for issues
  • Rollback plan ready

Resources and Documentation

NextAuth.js

Security Libraries

Best Practices


Next Steps: Choose which phase to implement first and create detailed implementation tickets for development.