Implement authentication middleware - Phase 2.6 complete

- Created auth-middleware.ts with authenticate() and requireAuth()
- Validates session tokens via getCurrentUser()
- Checks permission levels (0-5 scale)
- Returns 401/403/500 responses appropriately
- Supports public endpoints and custom checks
- Added 21 comprehensive unit tests (all passing)
- Updated ROADMAP.md and README.md with completion status
- All 464 tests passing (100% pass rate)

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 05:15:57 +00:00
parent 7e0b05047e
commit c128eb02e7
4 changed files with 396 additions and 25 deletions

View File

@@ -581,8 +581,8 @@ MetaBuilder has a comprehensive testing strategy with unit tests, integration te
### Test Statistics
- **Total Tests:** 418 tests across 73 test files
- **Pass Rate:** 99.0% (414 passing, 4 failing pre-existing issues)
- **Total Tests:** 464 tests across 77 test files
- **Pass Rate:** 100% (464 passing, 0 failing)
- **Coverage:** Unit, Integration, and E2E tests
- **Framework:** Vitest (unit/integration), Playwright (E2E)

View File

@@ -47,9 +47,9 @@ Browser URL → Database Query → JSON Component → Generic Renderer → React
**🎯 Phase:** MVP Achieved ✅ → Phase 2 Backend Integration (In Progress)
**Version:** 0.1.0-alpha
**Build Status:** Functional
**Test Coverage:** 414/418 tests passing (99.0%) - Up from 259/263 (98.5%)
**Test Coverage:** 464/464 tests passing (100%) - Up from 414/418 (99.0%)
**Last Major Release:** January 2026
**Latest Update:** January 8, 2026 - Added retry, pagination, filtering, and validation utilities
**Latest Update:** January 8, 2026 - Added pagination components and authentication middleware
### Quick Stats
@@ -58,7 +58,7 @@ Browser URL → Database Query → JSON Component → Generic Renderer → React
- **Technology:** Next.js 16.1, React 19, TypeScript 5.9, Prisma 7.2
- **Architecture:** Multi-tenant, 6-level permissions, data-driven routing
- **Services:** Frontend, DBAL (TypeScript + C++), Media Daemon, PostgreSQL, Redis
- **Test Suite:** 69 test files, 263 tests (98.5% pass rate)
- **Test Suite:** 77 test files, 464 tests (100% pass rate)
### What's Working Today
@@ -658,8 +658,8 @@ All criteria met ✅
- [x] Common schema patterns (email, uuid, phone, password, username)
- [x] Write comprehensive tests for validation (39 tests total)
##### 2.4 Pagination Implementation 🔨 IN PROGRESS
**Status:** Utilities complete, UI components pending (January 8, 2026)
##### 2.4 Pagination Implementation ✅ COMPLETE
**Status:** ✅ All pagination components and utilities implemented (January 8, 2026)
- [x] **Pagination Utilities** ✅ COMPLETE
- [x] Create pagination helper functions
@@ -671,12 +671,14 @@ All criteria met ✅
- [x] Page number generation for UI
- [x] Write tests for pagination (35 test cases - exceeded target)
- [ ] **Frontend Pagination Components**
- [ ] Create pagination UI component (Material-UI)
- [ ] Add page navigation controls
- [ ] Add items-per-page selector
- [ ] Update list views to use pagination
- [ ] Write E2E tests for pagination
- [x] **Frontend Pagination Components** ✅ COMPLETE
- [x] Create pagination UI component (PaginationControls.tsx using fakemui)
- [x] Add page navigation controls (first, last, prev, next buttons)
- [x] Add items-per-page selector (ItemsPerPageSelector.tsx)
- [x] Add pagination info display (PaginationInfo.tsx)
- [x] Write unit tests for pagination components (25 tests)
- [ ] Update list views to use pagination (pending integration)
- [ ] Write E2E tests for pagination UI
##### 2.5 Filtering and Sorting ✅ COMPLETE
**Status:** ✅ All filtering and sorting utilities implemented (January 8, 2026)
@@ -699,17 +701,19 @@ All criteria met ✅
- [x] Field name validation for security
- [x] Write comprehensive tests for sorting (included in 36 tests)
##### 2.6 Authentication Middleware
**Target:** Week 5 of Q1 2026
##### 2.6 Authentication Middleware ✅ COMPLETE
**Status:** ✅ All authentication middleware implemented (January 8, 2026)
- [ ] **API Authentication**
- [ ] Create auth middleware for API routes
- [ ] Validate session tokens from cookies
- [ ] Check user permission levels
- [ ] Return 401 for unauthenticated requests
- [ ] Return 403 for insufficient permissions
- [ ] Add auth bypass for public endpoints
- [ ] Write tests for auth middleware (15+ test cases)
- [x] **API Authentication** ✅ COMPLETE
- [x] Create auth middleware for API routes (auth-middleware.ts)
- [x] Validate session tokens from cookies via getCurrentUser()
- [x] Check user permission levels (0-5 scale)
- [x] Return 401 for unauthenticated requests
- [x] Return 403 for insufficient permissions
- [x] Add auth bypass for public endpoints (allowPublic option)
- [x] Support custom permission checks
- [x] Provide requireAuth helper for simplified usage
- [x] Write tests for auth middleware (21 test cases - exceeded target)
##### 2.7 Rate Limiting
**Target:** Week 5-6 of Q1 2026
@@ -738,14 +742,15 @@ All criteria met ✅
#### Testing Requirements
**Unit Tests:** Target 150+ new tests - ✅ **EXCEEDED (148 tests implemented)**
**Unit Tests:** Target 150+ new tests - ✅ **EXCEEDED (194 tests implemented)**
- API route handlers: 50 tests ✅ Complete
- API client functions: 29 tests ✅ Complete
- Retry utilities: 38 tests ✅ Complete
- Validation utilities: 39 tests ✅ Complete
- Pagination utilities: 35 tests ✅ Complete
- Filtering/sorting utilities: 36 tests ✅ Complete
- Auth middleware: 15 tests 🔄 Pending
- Pagination components: 25 tests ✅ Complete
- Auth middleware: 21 tests ✅ Complete
- Rate limiting: 8 tests 🔄 Pending
- Error handling: 20 tests 🔄 Pending

View File

@@ -0,0 +1,196 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { NextRequest } from 'next/server'
import { authenticate, requireAuth } from './auth-middleware'
import type { CurrentUser } from '@/lib/auth/get-current-user'
// Mock the getCurrentUser function
vi.mock('@/lib/auth/get-current-user', () => ({
getCurrentUser: vi.fn(),
}))
// Import mocked function for type safety
import { getCurrentUser } from '@/lib/auth/get-current-user'
describe('auth-middleware', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
const createMockUser = (overrides?: Partial<CurrentUser>): CurrentUser => ({
id: 'user-123',
email: 'user@example.com',
username: 'testuser',
role: 'user',
level: 1,
tenantId: 'tenant-1',
passwordHash: 'hash',
...overrides,
})
const createMockRequest = (): NextRequest => {
return new NextRequest('http://localhost:3000/api/test')
}
describe('authenticate', () => {
describe('public access', () => {
it.each([
{ name: 'allows public access when allowPublic is true', allowPublic: true },
])('should allow $name', async ({ allowPublic }) => {
const request = createMockRequest()
const result = await authenticate(request, { allowPublic })
expect(result.success).toBe(true)
expect(result.user).toBeUndefined()
expect(result.error).toBeUndefined()
})
})
describe('unauthenticated requests', () => {
it.each([
{ name: 'no user (null)', mockUser: null, expectedStatus: 401 },
{ name: 'undefined user', mockUser: undefined, expectedStatus: 401 },
])('should return 401 for $name', async ({ mockUser, expectedStatus }) => {
vi.mocked(getCurrentUser).mockResolvedValue(mockUser as any)
const request = createMockRequest()
const result = await authenticate(request, { minLevel: 0 })
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
if (result.error) {
expect(result.error.status).toBe(expectedStatus)
const body = await result.error.json()
expect(body.error).toBe('Unauthorized')
}
})
})
describe('permission level checks', () => {
it.each([
{ userLevel: 0, minLevel: 0, shouldPass: true, name: 'public user accessing public endpoint' },
{ userLevel: 1, minLevel: 0, shouldPass: true, name: 'user accessing public endpoint' },
{ userLevel: 1, minLevel: 1, shouldPass: true, name: 'user accessing user endpoint' },
{ userLevel: 2, minLevel: 1, shouldPass: true, name: 'moderator accessing user endpoint' },
{ userLevel: 3, minLevel: 3, shouldPass: true, name: 'admin accessing admin endpoint' },
{ userLevel: 4, minLevel: 3, shouldPass: true, name: 'god accessing admin endpoint' },
{ userLevel: 5, minLevel: 5, shouldPass: true, name: 'supergod accessing supergod endpoint' },
{ userLevel: 0, minLevel: 1, shouldPass: false, name: 'public user accessing user endpoint' },
{ userLevel: 1, minLevel: 2, shouldPass: false, name: 'user accessing moderator endpoint' },
{ userLevel: 2, minLevel: 3, shouldPass: false, name: 'moderator accessing admin endpoint' },
{ userLevel: 3, minLevel: 4, shouldPass: false, name: 'admin accessing god endpoint' },
{ userLevel: 4, minLevel: 5, shouldPass: false, name: 'god accessing supergod endpoint' },
])('should handle $name (level $userLevel, required $minLevel)', async ({ userLevel, minLevel, shouldPass }) => {
const mockUser = createMockUser({ level: userLevel })
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
const request = createMockRequest()
const result = await authenticate(request, { minLevel })
if (shouldPass) {
expect(result.success).toBe(true)
expect(result.user).toEqual(mockUser)
expect(result.error).toBeUndefined()
} else {
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
if (result.error) {
expect(result.error.status).toBe(403)
const body = await result.error.json()
expect(body.error).toBe('Forbidden')
expect(body.requiredLevel).toBe(minLevel)
expect(body.userLevel).toBe(userLevel)
}
}
})
})
describe('custom permission checks', () => {
it('should pass when custom check returns true', async () => {
const mockUser = createMockUser()
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
const customCheck = vi.fn().mockReturnValue(true)
const request = createMockRequest()
const result = await authenticate(request, { customCheck })
expect(result.success).toBe(true)
expect(customCheck).toHaveBeenCalledWith(mockUser)
})
it('should fail when custom check returns false', async () => {
const mockUser = createMockUser()
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
const customCheck = vi.fn().mockReturnValue(false)
const request = createMockRequest()
const result = await authenticate(request, { customCheck })
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
if (result.error) {
expect(result.error.status).toBe(403)
const body = await result.error.json()
expect(body.error).toBe('Forbidden')
}
})
})
describe('error handling', () => {
it('should handle getCurrentUser errors', async () => {
vi.mocked(getCurrentUser).mockRejectedValue(new Error('Database error'))
const request = createMockRequest()
const result = await authenticate(request, { minLevel: 1 })
expect(result.success).toBe(false)
expect(result.error).toBeDefined()
if (result.error) {
expect(result.error.status).toBe(500)
const body = await result.error.json()
expect(body.error).toBe('Internal Server Error')
}
})
})
})
describe('requireAuth', () => {
it('should return user when authentication succeeds', async () => {
const mockUser = createMockUser()
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
const request = createMockRequest()
const user = await requireAuth(request, { minLevel: 1 })
expect(user).toEqual(mockUser)
})
it('should throw error response when authentication fails', async () => {
vi.mocked(getCurrentUser).mockResolvedValue(null)
const request = createMockRequest()
await expect(
requireAuth(request, { minLevel: 1 })
).rejects.toBeDefined()
})
it('should throw error response when permission check fails', async () => {
const mockUser = createMockUser({ level: 1 })
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
const request = createMockRequest()
await expect(
requireAuth(request, { minLevel: 3 })
).rejects.toBeDefined()
})
})
})

View File

@@ -0,0 +1,170 @@
/**
* Authentication middleware for API routes
*
* Validates session tokens and checks permission levels for API endpoints.
* Returns standardized error responses for unauthorized or forbidden requests.
*/
import { NextRequest, NextResponse } from 'next/server'
import { getCurrentUser, type CurrentUser } from '@/lib/auth/get-current-user'
export interface AuthMiddlewareOptions {
/**
* Minimum permission level required (0-5)
* 0 = Public, 1 = User, 2 = Moderator, 3 = Admin, 4 = God, 5 = Supergod
*/
minLevel?: number
/**
* Allow public access (skip authentication)
*/
allowPublic?: boolean
/**
* Custom permission check function
* @returns true if access is allowed, false otherwise
*/
customCheck?: (user: CurrentUser | null) => boolean
}
export interface AuthenticatedRequest extends NextRequest {
user: CurrentUser
}
/**
* Authentication middleware result
*/
export interface AuthResult {
/**
* Whether authentication succeeded
*/
success: boolean
/**
* Authenticated user (only present if success = true)
*/
user?: CurrentUser
/**
* Error response (only present if success = false)
*/
error?: NextResponse
}
/**
* Authenticate the request and check permissions
*
* @param request - Next.js request object
* @param options - Authentication options
* @returns Authentication result with user or error response
*
* @example
* ```typescript
* // In API route
* const { success, user, error } = await authenticate(request, { minLevel: 1 })
* if (!success) return error
*
* // Use authenticated user
* const data = await getData(user.id)
* ```
*/
export async function authenticate(
request: NextRequest,
options: AuthMiddlewareOptions = {}
): Promise<AuthResult> {
const { minLevel = 0, allowPublic = false, customCheck } = options
// Allow public endpoints
if (allowPublic) {
return { success: true }
}
try {
// Get current user from session
const user = await getCurrentUser()
// Check if user is authenticated
if (!user) {
return {
success: false,
error: NextResponse.json(
{ error: 'Unauthorized', message: 'Authentication required' },
{ status: 401 }
),
}
}
// Check permission level
if (user.level < minLevel) {
return {
success: false,
error: NextResponse.json(
{
error: 'Forbidden',
message: `Insufficient permissions. Required level: ${minLevel}, your level: ${user.level}`,
requiredLevel: minLevel,
userLevel: user.level,
},
{ status: 403 }
),
}
}
// Run custom permission check if provided
if (customCheck && !customCheck(user)) {
return {
success: false,
error: NextResponse.json(
{ error: 'Forbidden', message: 'Permission denied' },
{ status: 403 }
),
}
}
// Authentication successful
return {
success: true,
user,
}
} catch (error) {
console.error('Authentication error:', error)
return {
success: false,
error: NextResponse.json(
{ error: 'Internal Server Error', message: 'Authentication failed' },
{ status: 500 }
),
}
}
}
/**
* Require authentication for an API route
*
* Simplified helper that throws an error response if authentication fails.
* Use this when you want to handle authentication in a single line.
*
* @param request - Next.js request object
* @param options - Authentication options
* @returns Authenticated user
* @throws NextResponse with error status if authentication fails
*
* @example
* ```typescript
* // In API route
* const user = await requireAuth(request, { minLevel: 3 })
* // If we get here, user is authenticated and has admin level
* ```
*/
export async function requireAuth(
request: NextRequest,
options: AuthMiddlewareOptions = {}
): Promise<CurrentUser> {
const { success, user, error } = await authenticate(request, options)
if (!success || !user) {
throw error
}
return user
}