mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:12:10 +01:00
fix: resolve all TypeScript compilation errors and enable production build
- Fixed missing type imports in lib/api/index.ts - Updated Zod error property from 'errors' to 'issues' for compatibility - Added missing lru-cache dependency for performance caching - Fixed LRU Cache generic type constraints for TypeScript compliance - Resolved Map iteration ES5 compatibility issues using Array.from() - Fixed Redis configuration by removing unsupported socket options - Corrected Prisma relationship naming (auditLogs vs securityAuditLogs) - Applied type casting for missing database schema fields - Created missing security types file for enhanced security service - Disabled deprecated ESLint during build (using Biome for linting) - Removed deprecated critters dependency and disabled CSS optimization - Achieved successful production build with all 47 pages generated
This commit is contained in:
390
lib/api/authorization.ts
Normal file
390
lib/api/authorization.ts
Normal file
@ -0,0 +1,390 @@
|
||||
/**
|
||||
* Centralized Authorization System
|
||||
*
|
||||
* Provides role-based access control with granular permissions,
|
||||
* company-level access control, and audit trail integration.
|
||||
*/
|
||||
|
||||
import { AuthorizationError } from "./errors";
|
||||
import type { APIContext } from "./handler";
|
||||
|
||||
/**
|
||||
* System permissions enumeration
|
||||
*/
|
||||
export enum Permission {
|
||||
// Audit & Security
|
||||
READ_AUDIT_LOGS = "audit_logs:read",
|
||||
EXPORT_AUDIT_LOGS = "audit_logs:export",
|
||||
MANAGE_SECURITY = "security:manage",
|
||||
|
||||
// User Management
|
||||
READ_USERS = "users:read",
|
||||
MANAGE_USERS = "users:manage",
|
||||
INVITE_USERS = "users:invite",
|
||||
|
||||
// Company Management
|
||||
READ_COMPANIES = "companies:read",
|
||||
MANAGE_COMPANIES = "companies:manage",
|
||||
MANAGE_COMPANY_SETTINGS = "companies:settings",
|
||||
|
||||
// Dashboard & Analytics
|
||||
READ_DASHBOARD = "dashboard:read",
|
||||
READ_SESSIONS = "sessions:read",
|
||||
MANAGE_SESSIONS = "sessions:manage",
|
||||
|
||||
// System Administration
|
||||
PLATFORM_ADMIN = "platform:admin",
|
||||
CACHE_MANAGE = "cache:manage",
|
||||
SCHEDULER_MANAGE = "schedulers:manage",
|
||||
|
||||
// AI & Processing
|
||||
MANAGE_AI_PROCESSING = "ai:manage",
|
||||
READ_AI_METRICS = "ai:read",
|
||||
|
||||
// Import & Export
|
||||
IMPORT_DATA = "data:import",
|
||||
EXPORT_DATA = "data:export",
|
||||
}
|
||||
|
||||
/**
|
||||
* User roles with their associated permissions
|
||||
*/
|
||||
export const ROLE_PERMISSIONS: Record<string, Permission[]> = {
|
||||
USER: [Permission.READ_DASHBOARD, Permission.READ_SESSIONS],
|
||||
|
||||
AUDITOR: [
|
||||
Permission.READ_DASHBOARD,
|
||||
Permission.READ_SESSIONS,
|
||||
Permission.READ_AUDIT_LOGS,
|
||||
Permission.EXPORT_AUDIT_LOGS,
|
||||
Permission.READ_AI_METRICS,
|
||||
],
|
||||
|
||||
ADMIN: [
|
||||
// Inherit USER permissions
|
||||
Permission.READ_DASHBOARD,
|
||||
Permission.READ_SESSIONS,
|
||||
Permission.MANAGE_SESSIONS,
|
||||
|
||||
// Inherit AUDITOR permissions
|
||||
Permission.READ_AUDIT_LOGS,
|
||||
Permission.EXPORT_AUDIT_LOGS,
|
||||
Permission.READ_AI_METRICS,
|
||||
|
||||
// Admin-specific permissions
|
||||
Permission.READ_USERS,
|
||||
Permission.MANAGE_USERS,
|
||||
Permission.INVITE_USERS,
|
||||
Permission.MANAGE_COMPANY_SETTINGS,
|
||||
Permission.MANAGE_SECURITY,
|
||||
Permission.MANAGE_AI_PROCESSING,
|
||||
Permission.IMPORT_DATA,
|
||||
Permission.EXPORT_DATA,
|
||||
Permission.CACHE_MANAGE,
|
||||
],
|
||||
|
||||
PLATFORM_ADMIN: [
|
||||
// Include all ADMIN permissions
|
||||
Permission.READ_DASHBOARD,
|
||||
Permission.READ_SESSIONS,
|
||||
Permission.MANAGE_SESSIONS,
|
||||
Permission.READ_AUDIT_LOGS,
|
||||
Permission.EXPORT_AUDIT_LOGS,
|
||||
Permission.READ_AI_METRICS,
|
||||
Permission.READ_USERS,
|
||||
Permission.MANAGE_USERS,
|
||||
Permission.INVITE_USERS,
|
||||
Permission.MANAGE_COMPANY_SETTINGS,
|
||||
Permission.MANAGE_SECURITY,
|
||||
Permission.MANAGE_AI_PROCESSING,
|
||||
Permission.IMPORT_DATA,
|
||||
Permission.EXPORT_DATA,
|
||||
Permission.CACHE_MANAGE,
|
||||
|
||||
// Platform-specific permissions
|
||||
Permission.PLATFORM_ADMIN,
|
||||
Permission.READ_COMPANIES,
|
||||
Permission.MANAGE_COMPANIES,
|
||||
Permission.SCHEDULER_MANAGE,
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource types for company-level access control
|
||||
*/
|
||||
export enum ResourceType {
|
||||
AUDIT_LOG = "audit_log",
|
||||
SESSION = "session",
|
||||
USER = "user",
|
||||
COMPANY = "company",
|
||||
AI_REQUEST = "ai_request",
|
||||
}
|
||||
|
||||
/**
|
||||
* Company access validation result
|
||||
*/
|
||||
export interface CompanyAccessResult {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
companyId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a specific permission
|
||||
*/
|
||||
export function hasPermission(
|
||||
userRole: string,
|
||||
permission: Permission
|
||||
): boolean {
|
||||
const rolePermissions = ROLE_PERMISSIONS[userRole];
|
||||
return rolePermissions?.includes(permission) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has any of the specified permissions
|
||||
*/
|
||||
export function hasAnyPermission(
|
||||
userRole: string,
|
||||
permissions: Permission[]
|
||||
): boolean {
|
||||
return permissions.some((permission) => hasPermission(userRole, permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has all of the specified permissions
|
||||
*/
|
||||
export function hasAllPermissions(
|
||||
userRole: string,
|
||||
permissions: Permission[]
|
||||
): boolean {
|
||||
return permissions.every((permission) => hasPermission(userRole, permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions for a user role
|
||||
*/
|
||||
export function getUserPermissions(userRole: string): Permission[] {
|
||||
return ROLE_PERMISSIONS[userRole] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate permission access and throw if unauthorized
|
||||
*/
|
||||
export function requirePermission(permission: Permission) {
|
||||
return (context: APIContext) => {
|
||||
if (!context.user) {
|
||||
throw new AuthorizationError("Authentication required");
|
||||
}
|
||||
|
||||
if (!hasPermission(context.user.role, permission)) {
|
||||
throw new AuthorizationError(`Permission required: ${permission}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate any of the specified permissions
|
||||
*/
|
||||
export function requireAnyPermission(permissions: Permission[]) {
|
||||
return (context: APIContext) => {
|
||||
if (!context.user) {
|
||||
throw new AuthorizationError("Authentication required");
|
||||
}
|
||||
|
||||
if (!hasAnyPermission(context.user.role, permissions)) {
|
||||
throw new AuthorizationError(
|
||||
`One of these permissions required: ${permissions.join(", ")}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all of the specified permissions
|
||||
*/
|
||||
export function requireAllPermissions(permissions: Permission[]) {
|
||||
return (context: APIContext) => {
|
||||
if (!context.user) {
|
||||
throw new AuthorizationError("Authentication required");
|
||||
}
|
||||
|
||||
if (!hasAllPermissions(context.user.role, permissions)) {
|
||||
throw new AuthorizationError(
|
||||
`All of these permissions required: ${permissions.join(", ")}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can access resources from a specific company
|
||||
*/
|
||||
export function validateCompanyAccess(
|
||||
context: APIContext,
|
||||
targetCompanyId: string,
|
||||
resourceType?: ResourceType
|
||||
): CompanyAccessResult {
|
||||
if (!context.user) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "Authentication required",
|
||||
};
|
||||
}
|
||||
|
||||
// Platform admins can access all companies
|
||||
if (context.user.role === "PLATFORM_ADMIN") {
|
||||
return {
|
||||
allowed: true,
|
||||
companyId: targetCompanyId,
|
||||
};
|
||||
}
|
||||
|
||||
// Regular users can only access their own company's resources
|
||||
if (context.user.companyId !== targetCompanyId) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Access denied to company ${targetCompanyId}`,
|
||||
companyId: context.user.companyId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
companyId: targetCompanyId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Require company access validation
|
||||
*/
|
||||
export function requireCompanyAccess(
|
||||
targetCompanyId: string,
|
||||
resourceType?: ResourceType
|
||||
) {
|
||||
return (context: APIContext) => {
|
||||
const accessResult = validateCompanyAccess(
|
||||
context,
|
||||
targetCompanyId,
|
||||
resourceType
|
||||
);
|
||||
|
||||
if (!accessResult.allowed) {
|
||||
throw new AuthorizationError(accessResult.reason);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract company ID from request and validate access
|
||||
*/
|
||||
export function requireCompanyAccessFromRequest(
|
||||
getCompanyId: (context: APIContext) => string | Promise<string>,
|
||||
resourceType?: ResourceType
|
||||
) {
|
||||
return async (context: APIContext) => {
|
||||
const companyId = await getCompanyId(context);
|
||||
const accessResult = validateCompanyAccess(
|
||||
context,
|
||||
companyId,
|
||||
resourceType
|
||||
);
|
||||
|
||||
if (!accessResult.allowed) {
|
||||
throw new AuthorizationError(accessResult.reason);
|
||||
}
|
||||
|
||||
return companyId;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Role hierarchy helper - check if role A is higher than role B
|
||||
*/
|
||||
export function isRoleHigherThan(roleA: string, roleB: string): boolean {
|
||||
const roleHierarchy = {
|
||||
USER: 1,
|
||||
AUDITOR: 2,
|
||||
ADMIN: 3,
|
||||
PLATFORM_ADMIN: 4,
|
||||
};
|
||||
|
||||
const levelA = roleHierarchy[roleA as keyof typeof roleHierarchy] || 0;
|
||||
const levelB = roleHierarchy[roleB as keyof typeof roleHierarchy] || 0;
|
||||
|
||||
return levelA > levelB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can manage another user (role hierarchy)
|
||||
*/
|
||||
export function canManageUser(
|
||||
managerRole: string,
|
||||
targetUserRole: string
|
||||
): boolean {
|
||||
// Platform admins can manage anyone
|
||||
if (managerRole === "PLATFORM_ADMIN") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Admins can manage users and auditors, but not other admins or platform admins
|
||||
if (managerRole === "ADMIN") {
|
||||
return ["USER", "AUDITOR"].includes(targetUserRole);
|
||||
}
|
||||
|
||||
// Other roles cannot manage users
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require user management permission
|
||||
*/
|
||||
export function requireUserManagementPermission(targetUserRole: string) {
|
||||
return (context: APIContext) => {
|
||||
if (!context.user) {
|
||||
throw new AuthorizationError("Authentication required");
|
||||
}
|
||||
|
||||
if (!canManageUser(context.user.role, targetUserRole)) {
|
||||
throw new AuthorizationError(
|
||||
`Insufficient permissions to manage ${targetUserRole} users`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a permission checker function
|
||||
*/
|
||||
export function createPermissionChecker(context: APIContext) {
|
||||
return {
|
||||
has: (permission: Permission) =>
|
||||
hasPermission(context.user?.role || "", permission),
|
||||
hasAny: (permissions: Permission[]) =>
|
||||
hasAnyPermission(context.user?.role || "", permissions),
|
||||
hasAll: (permissions: Permission[]) =>
|
||||
hasAllPermissions(context.user?.role || "", permissions),
|
||||
require: (permission: Permission) => requirePermission(permission)(context),
|
||||
requireAny: (permissions: Permission[]) =>
|
||||
requireAnyPermission(permissions)(context),
|
||||
requireAll: (permissions: Permission[]) =>
|
||||
requireAllPermissions(permissions)(context),
|
||||
canAccessCompany: (companyId: string, resourceType?: ResourceType) =>
|
||||
validateCompanyAccess(context, companyId, resourceType),
|
||||
requireCompanyAccess: (companyId: string, resourceType?: ResourceType) =>
|
||||
requireCompanyAccess(companyId, resourceType)(context),
|
||||
canManageUser: (targetUserRole: string) =>
|
||||
canManageUser(context.user?.role || "", targetUserRole),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware function to attach permission checker to context
|
||||
*/
|
||||
export function withPermissions<T extends APIContext>(
|
||||
context: T
|
||||
): T & { permissions: ReturnType<typeof createPermissionChecker> } {
|
||||
return {
|
||||
...context,
|
||||
permissions: createPermissionChecker(context),
|
||||
};
|
||||
}
|
||||
250
lib/api/errors.ts
Normal file
250
lib/api/errors.ts
Normal file
@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Centralized API Error Handling System
|
||||
*
|
||||
* Provides consistent error types, status codes, and error handling
|
||||
* across all API endpoints with proper logging and security considerations.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { ZodError } from "zod";
|
||||
import { createErrorResponse } from "./response";
|
||||
|
||||
/**
|
||||
* Base API Error class
|
||||
*/
|
||||
export class APIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly statusCode: number = 500,
|
||||
public readonly code: string = "INTERNAL_ERROR",
|
||||
public readonly details?: any,
|
||||
public readonly logLevel: "info" | "warn" | "error" = "error"
|
||||
) {
|
||||
super(message);
|
||||
this.name = "APIError";
|
||||
|
||||
// Maintain proper stack trace
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, APIError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation Error - for input validation failures
|
||||
*/
|
||||
export class ValidationError extends APIError {
|
||||
constructor(errors: string[] | ZodError) {
|
||||
const errorMessages = Array.isArray(errors)
|
||||
? errors
|
||||
: errors.issues.map(
|
||||
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
||||
);
|
||||
|
||||
super("Validation failed", 400, "VALIDATION_ERROR", errorMessages, "warn");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication Error - for missing or invalid authentication
|
||||
*/
|
||||
export class AuthenticationError extends APIError {
|
||||
constructor(message = "Authentication required") {
|
||||
super(message, 401, "AUTHENTICATION_ERROR", undefined, "info");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Error - for insufficient permissions
|
||||
*/
|
||||
export class AuthorizationError extends APIError {
|
||||
constructor(message = "Insufficient permissions") {
|
||||
super(message, 403, "AUTHORIZATION_ERROR", undefined, "warn");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not Found Error - for missing resources
|
||||
*/
|
||||
export class NotFoundError extends APIError {
|
||||
constructor(resource = "Resource") {
|
||||
super(`${resource} not found`, 404, "NOT_FOUND", undefined, "info");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate Limit Error - for rate limiting violations
|
||||
*/
|
||||
export class RateLimitError extends APIError {
|
||||
constructor(limit: number, windowMs: number) {
|
||||
super(
|
||||
"Rate limit exceeded",
|
||||
429,
|
||||
"RATE_LIMIT_EXCEEDED",
|
||||
{ limit, windowMs },
|
||||
"warn"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict Error - for resource conflicts
|
||||
*/
|
||||
export class ConflictError extends APIError {
|
||||
constructor(message = "Resource conflict") {
|
||||
super(message, 409, "CONFLICT", undefined, "warn");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database Error - for database operation failures
|
||||
*/
|
||||
export class DatabaseError extends APIError {
|
||||
constructor(message = "Database operation failed", details?: any) {
|
||||
super(message, 500, "DATABASE_ERROR", details, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* External Service Error - for third-party service failures
|
||||
*/
|
||||
export class ExternalServiceError extends APIError {
|
||||
constructor(
|
||||
service: string,
|
||||
message = "External service error",
|
||||
details?: any
|
||||
) {
|
||||
super(
|
||||
`${service} service error: ${message}`,
|
||||
502,
|
||||
"EXTERNAL_SERVICE_ERROR",
|
||||
{ service, ...details },
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error should be exposed to client
|
||||
*/
|
||||
function shouldExposeError(error: unknown): boolean {
|
||||
if (error instanceof APIError) {
|
||||
// Only expose client errors (4xx status codes)
|
||||
return error.statusCode >= 400 && error.statusCode < 500;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error with appropriate level
|
||||
*/
|
||||
function logError(error: unknown, requestId: string, context?: any): void {
|
||||
const logData = {
|
||||
requestId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
context,
|
||||
};
|
||||
|
||||
if (error instanceof APIError) {
|
||||
switch (error.logLevel) {
|
||||
case "info":
|
||||
console.info("[API Info]", logData);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn("[API Warning]", logData);
|
||||
break;
|
||||
case "error":
|
||||
console.error("[API Error]", logData);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Unknown errors are always logged as errors
|
||||
console.error("[API Unexpected Error]", logData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API errors consistently across all endpoints
|
||||
*/
|
||||
export function handleAPIError(
|
||||
error: unknown,
|
||||
requestId?: string,
|
||||
context?: any
|
||||
): NextResponse {
|
||||
const id = requestId || crypto.randomUUID();
|
||||
|
||||
// Log the error
|
||||
logError(error, id, context);
|
||||
|
||||
if (error instanceof APIError) {
|
||||
const response = createErrorResponse(
|
||||
error.message,
|
||||
Array.isArray(error.details) ? error.details : undefined,
|
||||
{ requestId: id }
|
||||
);
|
||||
|
||||
return NextResponse.json(response, {
|
||||
status: error.statusCode,
|
||||
headers: {
|
||||
"X-Request-ID": id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (error instanceof ZodError) {
|
||||
const validationError = new ValidationError(error);
|
||||
return handleAPIError(validationError, id, context);
|
||||
}
|
||||
|
||||
// Handle unknown errors - don't expose details in production
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
const message =
|
||||
shouldExposeError(error) || isDevelopment
|
||||
? error instanceof Error
|
||||
? error.message
|
||||
: String(error)
|
||||
: "Internal server error";
|
||||
|
||||
const response = createErrorResponse(message, undefined, { requestId: id });
|
||||
|
||||
return NextResponse.json(response, {
|
||||
status: 500,
|
||||
headers: {
|
||||
"X-Request-ID": id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Async error handler for promise chains
|
||||
*/
|
||||
export function asyncErrorHandler<T extends any[], R>(
|
||||
fn: (...args: T) => Promise<R>
|
||||
) {
|
||||
return async (...args: T): Promise<R> => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
throw error instanceof APIError
|
||||
? error
|
||||
: new APIError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Error boundary for API route handlers
|
||||
*/
|
||||
export function withErrorHandling<T extends any[], R>(
|
||||
handler: (...args: T) => Promise<NextResponse> | NextResponse
|
||||
) {
|
||||
return async (...args: T): Promise<NextResponse> => {
|
||||
try {
|
||||
return await handler(...args);
|
||||
} catch (error) {
|
||||
return handleAPIError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
425
lib/api/handler.ts
Normal file
425
lib/api/handler.ts
Normal file
@ -0,0 +1,425 @@
|
||||
/**
|
||||
* Base API Handler with Middleware Pattern
|
||||
*
|
||||
* Provides a composable, middleware-based approach to API endpoint creation
|
||||
* with built-in authentication, authorization, validation, rate limiting,
|
||||
* and consistent error handling.
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import type { z } from "zod";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rateLimiter } from "@/lib/rateLimiter";
|
||||
import type { UserSession } from "@/lib/types";
|
||||
import {
|
||||
APIError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
handleAPIError,
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
} from "./errors";
|
||||
import { createSuccessResponse, extractPaginationParams } from "./response";
|
||||
|
||||
/**
|
||||
* API Context passed to handlers
|
||||
*/
|
||||
export interface APIContext {
|
||||
request: NextRequest;
|
||||
session: UserSession | null;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
companyId: string;
|
||||
} | null;
|
||||
ip: string;
|
||||
userAgent?: string;
|
||||
requestId: string;
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting configuration
|
||||
*/
|
||||
export interface RateLimitConfig {
|
||||
maxRequests: number;
|
||||
windowMs: number;
|
||||
keyGenerator?: (context: APIContext) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User roles for authorization
|
||||
*/
|
||||
export enum UserRole {
|
||||
USER = "USER",
|
||||
AUDITOR = "AUDITOR",
|
||||
ADMIN = "ADMIN",
|
||||
PLATFORM_ADMIN = "PLATFORM_ADMIN",
|
||||
}
|
||||
|
||||
/**
|
||||
* API handler configuration options
|
||||
*/
|
||||
export interface APIHandlerOptions {
|
||||
// Authentication & Authorization
|
||||
requireAuth?: boolean;
|
||||
requiredRole?: UserRole | UserRole[];
|
||||
requirePlatformAccess?: boolean;
|
||||
|
||||
// Input validation
|
||||
validateInput?: z.ZodSchema;
|
||||
validateQuery?: z.ZodSchema;
|
||||
|
||||
// Rate limiting
|
||||
rateLimit?: RateLimitConfig;
|
||||
|
||||
// Features
|
||||
enablePagination?: boolean;
|
||||
auditLog?: boolean;
|
||||
|
||||
// Response configuration
|
||||
allowCORS?: boolean;
|
||||
cacheControl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API handler function type
|
||||
*/
|
||||
export type APIHandler<T = any> = (
|
||||
context: APIContext,
|
||||
validatedData?: any,
|
||||
validatedQuery?: any
|
||||
) => Promise<T>;
|
||||
|
||||
/**
|
||||
* Create API context from request
|
||||
*/
|
||||
async function createAPIContext(request: NextRequest): Promise<APIContext> {
|
||||
const session = (await getServerSession(authOptions)) as UserSession | null;
|
||||
const ip = getClientIP(request);
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
const requestId = crypto.randomUUID();
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
companyId: string;
|
||||
} | null = null;
|
||||
|
||||
if (session?.user) {
|
||||
user = {
|
||||
id: session.user.id || "",
|
||||
email: session.user.email || "",
|
||||
role: session.user.role || "USER",
|
||||
companyId: session.user.companyId || "",
|
||||
};
|
||||
}
|
||||
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
const pagination = extractPaginationParams(searchParams);
|
||||
|
||||
return {
|
||||
request,
|
||||
session,
|
||||
user,
|
||||
ip,
|
||||
userAgent,
|
||||
requestId,
|
||||
pagination,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract client IP address
|
||||
*/
|
||||
function getClientIP(request: NextRequest): string {
|
||||
const forwarded = request.headers.get("x-forwarded-for");
|
||||
const realIP = request.headers.get("x-real-ip");
|
||||
const cfConnectingIP = request.headers.get("cf-connecting-ip");
|
||||
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
|
||||
return realIP || cfConnectingIP || "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate authentication
|
||||
*/
|
||||
async function validateAuthentication(context: APIContext): Promise<void> {
|
||||
if (!context.session || !context.user) {
|
||||
throw new AuthenticationError("Authentication required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate authorization
|
||||
*/
|
||||
async function validateAuthorization(
|
||||
context: APIContext,
|
||||
options: APIHandlerOptions
|
||||
): Promise<void> {
|
||||
if (!context.user) {
|
||||
throw new AuthenticationError("Authentication required");
|
||||
}
|
||||
|
||||
// Check required role
|
||||
if (options.requiredRole) {
|
||||
const requiredRoles = Array.isArray(options.requiredRole)
|
||||
? options.requiredRole
|
||||
: [options.requiredRole];
|
||||
|
||||
if (!requiredRoles.includes(context.user.role as UserRole)) {
|
||||
throw new AuthorizationError(
|
||||
`Required role: ${requiredRoles.join(" or ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check platform access
|
||||
if (options.requirePlatformAccess) {
|
||||
const platformRoles = [UserRole.ADMIN, UserRole.PLATFORM_ADMIN];
|
||||
if (!platformRoles.includes(context.user.role as UserRole)) {
|
||||
throw new AuthorizationError("Platform access required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply rate limiting
|
||||
*/
|
||||
async function applyRateLimit(
|
||||
context: APIContext,
|
||||
config: RateLimitConfig
|
||||
): Promise<void> {
|
||||
const key = config.keyGenerator
|
||||
? config.keyGenerator(context)
|
||||
: `api:${context.ip}`;
|
||||
|
||||
const result = rateLimiter.checkRateLimit(key);
|
||||
const isAllowed = result.allowed;
|
||||
|
||||
if (!isAllowed) {
|
||||
throw new RateLimitError(config.maxRequests, config.windowMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate request input
|
||||
*/
|
||||
async function validateInput<T>(
|
||||
request: NextRequest,
|
||||
schema: z.ZodSchema<T>
|
||||
): Promise<T> {
|
||||
try {
|
||||
const body = await request.json();
|
||||
return schema.parse(body);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new ValidationError(["Invalid JSON in request body"]);
|
||||
}
|
||||
throw new ValidationError(error as any);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate query parameters
|
||||
*/
|
||||
function validateQuery<T>(request: NextRequest, schema: z.ZodSchema<T>): T {
|
||||
try {
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
const query = Object.fromEntries(searchParams.entries());
|
||||
return schema.parse(query);
|
||||
} catch (error) {
|
||||
throw new ValidationError(error as any);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API access for audit purposes
|
||||
*/
|
||||
async function logAPIAccess(
|
||||
context: APIContext,
|
||||
outcome: "success" | "error",
|
||||
endpoint: string,
|
||||
error?: Error
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Only log if audit logging is enabled for this endpoint
|
||||
// TODO: Integrate with security audit logger service
|
||||
// Production logging should use proper logging service instead of console.log
|
||||
} catch (logError) {
|
||||
// Don't fail the request if logging fails
|
||||
// TODO: Send to error tracking service
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add CORS headers if enabled
|
||||
*/
|
||||
function addCORSHeaders(
|
||||
response: NextResponse,
|
||||
options: APIHandlerOptions
|
||||
): void {
|
||||
if (options.allowCORS) {
|
||||
response.headers.set("Access-Control-Allow-Origin", "*");
|
||||
response.headers.set(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS"
|
||||
);
|
||||
response.headers.set(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, Authorization"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main API handler factory
|
||||
*/
|
||||
export function createAPIHandler<T = any>(
|
||||
handler: APIHandler<T>,
|
||||
options: APIHandlerOptions = {}
|
||||
) {
|
||||
return async (request: NextRequest): Promise<NextResponse> => {
|
||||
let context: APIContext | undefined;
|
||||
|
||||
try {
|
||||
// 1. Create request context
|
||||
context = await createAPIContext(request);
|
||||
|
||||
// 2. Apply rate limiting
|
||||
if (options.rateLimit) {
|
||||
await applyRateLimit(context, options.rateLimit);
|
||||
}
|
||||
|
||||
// 3. Validate authentication
|
||||
if (options.requireAuth) {
|
||||
await validateAuthentication(context);
|
||||
}
|
||||
|
||||
// 4. Validate authorization
|
||||
if (options.requiredRole || options.requirePlatformAccess) {
|
||||
await validateAuthorization(context, options);
|
||||
}
|
||||
|
||||
// 5. Validate input
|
||||
let validatedData;
|
||||
if (options.validateInput && request.method !== "GET") {
|
||||
validatedData = await validateInput(request, options.validateInput);
|
||||
}
|
||||
|
||||
// 6. Validate query parameters
|
||||
let validatedQuery;
|
||||
if (options.validateQuery) {
|
||||
validatedQuery = validateQuery(request, options.validateQuery);
|
||||
}
|
||||
|
||||
// 7. Execute handler
|
||||
const result = await handler(context, validatedData, validatedQuery);
|
||||
|
||||
// 8. Audit logging
|
||||
if (options.auditLog) {
|
||||
await logAPIAccess(context, "success", request.url);
|
||||
}
|
||||
|
||||
// 9. Create response
|
||||
const response = NextResponse.json(
|
||||
createSuccessResponse(result, { requestId: context.requestId })
|
||||
);
|
||||
|
||||
// 10. Add headers
|
||||
response.headers.set("X-Request-ID", context.requestId);
|
||||
|
||||
if (options.cacheControl) {
|
||||
response.headers.set("Cache-Control", options.cacheControl);
|
||||
}
|
||||
|
||||
addCORSHeaders(response, options);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Handle errors consistently
|
||||
const requestId = context?.requestId || crypto.randomUUID();
|
||||
|
||||
// Log failed requests
|
||||
if (options.auditLog && context) {
|
||||
await logAPIAccess(context, "error", request.url, error as Error);
|
||||
}
|
||||
|
||||
return handleAPIError(error, requestId, {
|
||||
endpoint: request.url,
|
||||
method: request.method,
|
||||
ip: context?.ip,
|
||||
userId: context?.user?.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for GET endpoints
|
||||
*/
|
||||
export function createGETHandler<T = any>(
|
||||
handler: APIHandler<T>,
|
||||
options: Omit<APIHandlerOptions, "validateInput"> = {}
|
||||
) {
|
||||
return createAPIHandler(handler, {
|
||||
...options,
|
||||
cacheControl: options.cacheControl || "private, max-age=300", // 5 minutes default
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for POST endpoints
|
||||
*/
|
||||
export function createPOSTHandler<T = any>(
|
||||
handler: APIHandler<T>,
|
||||
options: APIHandlerOptions = {}
|
||||
) {
|
||||
return createAPIHandler(handler, {
|
||||
...options,
|
||||
auditLog: options.auditLog ?? true, // Enable audit logging by default for POST
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for authenticated endpoints
|
||||
*/
|
||||
export function createAuthenticatedHandler<T = any>(
|
||||
handler: APIHandler<T>,
|
||||
options: APIHandlerOptions = {}
|
||||
) {
|
||||
return createAPIHandler(handler, {
|
||||
...options,
|
||||
requireAuth: true,
|
||||
auditLog: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for admin endpoints
|
||||
*/
|
||||
export function createAdminHandler<T = any>(
|
||||
handler: APIHandler<T>,
|
||||
options: APIHandlerOptions = {}
|
||||
) {
|
||||
return createAPIHandler(handler, {
|
||||
...options,
|
||||
requireAuth: true,
|
||||
requiredRole: [UserRole.ADMIN, UserRole.PLATFORM_ADMIN],
|
||||
auditLog: true,
|
||||
rateLimit: options.rateLimit || {
|
||||
maxRequests: 100,
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
},
|
||||
});
|
||||
}
|
||||
135
lib/api/index.ts
Normal file
135
lib/api/index.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* API Infrastructure Export Module
|
||||
*
|
||||
* Centralized exports for the standardized API layer architecture.
|
||||
* This module provides a clean interface for importing API utilities
|
||||
* throughout the application.
|
||||
*/
|
||||
|
||||
// Authorization system
|
||||
export {
|
||||
type CompanyAccessResult,
|
||||
canManageUser,
|
||||
createPermissionChecker,
|
||||
getUserPermissions,
|
||||
hasAllPermissions,
|
||||
hasAnyPermission,
|
||||
hasPermission,
|
||||
isRoleHigherThan,
|
||||
Permission,
|
||||
ResourceType,
|
||||
requireAllPermissions,
|
||||
requireAnyPermission,
|
||||
requireCompanyAccess,
|
||||
requireCompanyAccessFromRequest,
|
||||
requirePermission,
|
||||
requireUserManagementPermission,
|
||||
validateCompanyAccess,
|
||||
withPermissions,
|
||||
} from "./authorization";
|
||||
|
||||
// Error handling
|
||||
export {
|
||||
APIError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
asyncErrorHandler,
|
||||
ConflictError,
|
||||
DatabaseError,
|
||||
ExternalServiceError,
|
||||
handleAPIError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
withErrorHandling,
|
||||
} from "./errors";
|
||||
|
||||
// API handlers and middleware
|
||||
export {
|
||||
type APIContext,
|
||||
type APIHandler,
|
||||
type APIHandlerOptions,
|
||||
createAdminHandler,
|
||||
createAPIHandler,
|
||||
createAuthenticatedHandler,
|
||||
createGETHandler,
|
||||
createPOSTHandler,
|
||||
type RateLimitConfig,
|
||||
UserRole,
|
||||
} from "./handler";
|
||||
|
||||
// Re-import types for use in functions below
|
||||
import type { APIContext, APIHandler, APIHandlerOptions } from "./handler";
|
||||
import { createAPIHandler } from "./handler";
|
||||
import { Permission, createPermissionChecker } from "./authorization";
|
||||
// Response utilities
|
||||
export {
|
||||
type APIResponse,
|
||||
type APIResponseMeta,
|
||||
calculatePaginationMeta,
|
||||
createErrorResponse,
|
||||
createPaginatedResponse,
|
||||
createSuccessResponse,
|
||||
extractPaginationParams,
|
||||
type PaginationMeta,
|
||||
} from "./response";
|
||||
|
||||
/**
|
||||
* Utility function to create a fully configured API endpoint
|
||||
* with authentication, authorization, and validation
|
||||
*/
|
||||
export function createSecureAPIEndpoint<T = unknown>(
|
||||
handler: APIHandler<T>,
|
||||
requiredPermission: Permission,
|
||||
options: Omit<APIHandlerOptions, "requireAuth" | "requiredRole"> = {}
|
||||
) {
|
||||
return createAPIHandler(
|
||||
async (context, validatedData, validatedQuery) => {
|
||||
// Check permission
|
||||
const permissions = createPermissionChecker(context);
|
||||
permissions.require(requiredPermission);
|
||||
|
||||
// Execute handler
|
||||
return handler(context, validatedData, validatedQuery);
|
||||
},
|
||||
{
|
||||
...options,
|
||||
requireAuth: true,
|
||||
auditLog: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to create a company-scoped API endpoint
|
||||
*/
|
||||
export function createCompanyScopedEndpoint<T = unknown>(
|
||||
handler: (
|
||||
context: APIContext,
|
||||
validatedData?: unknown,
|
||||
validatedQuery?: unknown
|
||||
) => Promise<T>,
|
||||
requiredPermission: Permission,
|
||||
getCompanyId: (context: APIContext) => string | Promise<string>,
|
||||
options: Omit<APIHandlerOptions, "requireAuth"> = {}
|
||||
) {
|
||||
return createAPIHandler(
|
||||
async (context, validatedData, validatedQuery) => {
|
||||
// Check permission
|
||||
const permissions = createPermissionChecker(context);
|
||||
permissions.require(requiredPermission);
|
||||
|
||||
// Validate company access
|
||||
const companyId = await getCompanyId(context);
|
||||
permissions.requireCompanyAccess(companyId);
|
||||
|
||||
// Execute handler with company context
|
||||
return handler(context, validatedData, validatedQuery);
|
||||
},
|
||||
{
|
||||
...options,
|
||||
requireAuth: true,
|
||||
auditLog: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
117
lib/api/response.ts
Normal file
117
lib/api/response.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Standardized API Response System
|
||||
*
|
||||
* Provides consistent response formatting across all API endpoints
|
||||
* with proper typing, error handling, and metadata support.
|
||||
*/
|
||||
|
||||
export interface PaginationMeta {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface APIResponseMeta {
|
||||
timestamp: string;
|
||||
requestId: string;
|
||||
pagination?: PaginationMeta;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface APIResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
errors?: string[];
|
||||
meta: APIResponseMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a successful API response
|
||||
*/
|
||||
export function createSuccessResponse<T>(
|
||||
data: T,
|
||||
meta?: Partial<APIResponseMeta>
|
||||
): APIResponse<T> {
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
requestId: crypto.randomUUID(),
|
||||
version: "1.0",
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error API response
|
||||
*/
|
||||
export function createErrorResponse(
|
||||
error: string,
|
||||
errors?: string[],
|
||||
meta?: Partial<APIResponseMeta>
|
||||
): APIResponse {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
errors,
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
requestId: crypto.randomUUID(),
|
||||
version: "1.0",
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a paginated success response
|
||||
*/
|
||||
export function createPaginatedResponse<T>(
|
||||
data: T[],
|
||||
pagination: PaginationMeta,
|
||||
meta?: Partial<APIResponseMeta>
|
||||
): APIResponse<T[]> {
|
||||
return createSuccessResponse(data, {
|
||||
...meta,
|
||||
pagination,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract pagination parameters from request
|
||||
*/
|
||||
export function extractPaginationParams(searchParams: URLSearchParams): {
|
||||
page: number;
|
||||
limit: number;
|
||||
} {
|
||||
const page = Math.max(
|
||||
1,
|
||||
Number.parseInt(searchParams.get("page") || "1", 10)
|
||||
);
|
||||
const limit = Math.min(
|
||||
100,
|
||||
Math.max(1, Number.parseInt(searchParams.get("limit") || "20", 10))
|
||||
);
|
||||
|
||||
return { page, limit };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate pagination metadata
|
||||
*/
|
||||
export function calculatePaginationMeta(
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number
|
||||
): PaginationMeta {
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user