mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
- Introduce dedicated error pages for dashboard and video routes - Add specific not-found pages for dashboard, projects, videos, and settings - Implement global `not-found.tsx` for general unhandled routes - Integrate root and dashboard layouts with ErrorBoundary and Suspense - Add new UI components: Accordion, Hover Card, Menubar, Navigation Menu, Select, Tabs - Update Navbar to utilize the new Navigation Menu component - Enhance `button` component with a `link` variant for better styling - Refine existing UI components (dialog, dropdown, input, etc.) - Update Tailwind config with new colors and animation extensions
157 lines
4.2 KiB
TypeScript
157 lines
4.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
|
|
/**
|
|
* Standardized API error response format
|
|
* All API routes should use this format for consistency
|
|
*/
|
|
export interface ApiErrorResponse {
|
|
error: string;
|
|
code?: string;
|
|
details?: Record<string, string[]>;
|
|
}
|
|
|
|
/**
|
|
* Standardized API success response format
|
|
*/
|
|
export interface ApiSuccessResponse<T = unknown> {
|
|
data: T;
|
|
meta?: {
|
|
page?: number;
|
|
limit?: number;
|
|
total?: number;
|
|
totalPages?: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* HTTP status codes used in the API
|
|
*/
|
|
export const HttpStatus = {
|
|
OK: 200,
|
|
CREATED: 201,
|
|
BAD_REQUEST: 400,
|
|
UNAUTHORIZED: 401,
|
|
FORBIDDEN: 403,
|
|
NOT_FOUND: 404,
|
|
CONFLICT: 409,
|
|
UNPROCESSABLE_ENTITY: 422,
|
|
TOO_MANY_REQUESTS: 429,
|
|
INTERNAL_SERVER_ERROR: 500,
|
|
} as const;
|
|
|
|
/**
|
|
* Error codes for client-side handling
|
|
*/
|
|
export const ErrorCode = {
|
|
// Authentication errors
|
|
UNAUTHORIZED: "UNAUTHORIZED",
|
|
FORBIDDEN: "FORBIDDEN",
|
|
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
|
|
|
// Resource errors
|
|
NOT_FOUND: "NOT_FOUND",
|
|
ALREADY_EXISTS: "ALREADY_EXISTS",
|
|
|
|
// Validation errors
|
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
INVALID_INPUT: "INVALID_INPUT",
|
|
|
|
// Rate limiting
|
|
RATE_LIMITED: "RATE_LIMITED",
|
|
|
|
// Server errors
|
|
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
} as const;
|
|
|
|
/**
|
|
* Creates a standardized error response
|
|
*
|
|
* @param message - Human-readable error message
|
|
* @param status - HTTP status code
|
|
* @param code - Machine-readable error code for client handling
|
|
* @param details - Additional error details for validation errors (field -> messages[])
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* return errorResponse("Project not found", 404, ErrorCode.NOT_FOUND);
|
|
* return errorResponse("Invalid input", 400, ErrorCode.VALIDATION_ERROR, { email: ["Invalid email format"] });
|
|
* ```
|
|
*/
|
|
export function errorResponse(
|
|
message: string,
|
|
status: number,
|
|
code?: string,
|
|
details?: Record<string, string[]>
|
|
): NextResponse<ApiErrorResponse> {
|
|
const body: ApiErrorResponse = { error: message };
|
|
if (code) body.code = code;
|
|
if (details) {
|
|
// Sanitize: only allow string arrays to prevent accidental data leakage
|
|
const sanitized: Record<string, string[]> = {};
|
|
for (const [key, value] of Object.entries(details)) {
|
|
if (Array.isArray(value) && value.every(v => typeof v === 'string')) {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
if (Object.keys(sanitized).length > 0) {
|
|
body.details = sanitized;
|
|
}
|
|
}
|
|
|
|
return NextResponse.json(body, { status });
|
|
}
|
|
|
|
/**
|
|
* Creates a standardized success response
|
|
*
|
|
* @param data - Response data
|
|
* @param status - HTTP status code (default: 200)
|
|
* @param meta - Pagination or other metadata (optional)
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* return successResponse({ projects: [] });
|
|
* return successResponse({ projects: [] }, 200, { page: 1, limit: 10, total: 100 });
|
|
* ```
|
|
*/
|
|
export function successResponse<T>(
|
|
data: T,
|
|
status: number = HttpStatus.OK,
|
|
meta?: ApiSuccessResponse["meta"]
|
|
): NextResponse<ApiSuccessResponse<T>> {
|
|
const body: ApiSuccessResponse<T> = { data };
|
|
if (meta) body.meta = meta;
|
|
|
|
return NextResponse.json(body, { status });
|
|
}
|
|
|
|
/**
|
|
* Common error response helpers
|
|
*/
|
|
export const apiErrors = {
|
|
unauthorized: (message = "Unauthorized") =>
|
|
errorResponse(message, HttpStatus.UNAUTHORIZED, ErrorCode.UNAUTHORIZED),
|
|
|
|
forbidden: (message = "Forbidden") =>
|
|
errorResponse(message, HttpStatus.FORBIDDEN, ErrorCode.FORBIDDEN),
|
|
|
|
notFound: (resource = "Resource") =>
|
|
errorResponse(`${resource} not found`, HttpStatus.NOT_FOUND, ErrorCode.NOT_FOUND),
|
|
|
|
badRequest: (message = "Bad request") =>
|
|
errorResponse(message, HttpStatus.BAD_REQUEST, ErrorCode.INVALID_INPUT),
|
|
|
|
validationError: (message: string, details?: Record<string, string[]>) =>
|
|
errorResponse(message, HttpStatus.UNPROCESSABLE_ENTITY, ErrorCode.VALIDATION_ERROR, details),
|
|
|
|
conflict: (message: string) =>
|
|
errorResponse(message, HttpStatus.CONFLICT, ErrorCode.ALREADY_EXISTS),
|
|
|
|
rateLimited: (message = "Too many requests") =>
|
|
errorResponse(message, HttpStatus.TOO_MANY_REQUESTS, ErrorCode.RATE_LIMITED),
|
|
|
|
internalError: (message = "Internal server error") =>
|
|
errorResponse(message, HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_ERROR),
|
|
};
|