18 KiB
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
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
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 formsrc/app/auth/signout/page.js- Logout confirmationsrc/app/auth/error/page.js- Error handlingsrc/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.