286 lines
7.1 KiB
TypeScript
286 lines
7.1 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { ZodError } from 'zod';
|
|
import { VkClientError } from '@/integrations/vk/vk-errors';
|
|
|
|
export abstract class AppError extends Error {
|
|
abstract readonly statusCode: number;
|
|
abstract readonly code: string;
|
|
readonly details?: any;
|
|
|
|
constructor(message: string, details?: any) {
|
|
super(message);
|
|
this.name = this.constructor.name;
|
|
this.details = details;
|
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
}
|
|
}
|
|
|
|
export class ValidationError extends AppError {
|
|
readonly statusCode = 400;
|
|
readonly code = 'VALIDATION_ERROR';
|
|
}
|
|
|
|
export class UnauthorizedError extends AppError {
|
|
readonly statusCode = 401;
|
|
readonly code = 'UNAUTHORIZED';
|
|
}
|
|
|
|
export class VkReauthenticationRequiredError extends AppError {
|
|
readonly statusCode = 401;
|
|
readonly code = 'VK_REAUTHENTICATION_REQUIRED';
|
|
}
|
|
|
|
export class ForbiddenError extends AppError {
|
|
readonly statusCode = 403;
|
|
readonly code = 'FORBIDDEN';
|
|
}
|
|
|
|
export class VkPermissionRequiredError extends AppError {
|
|
readonly statusCode = 403;
|
|
readonly code = 'VK_PERMISSION_REQUIRED';
|
|
}
|
|
|
|
export class VkResourcePrivateError extends AppError {
|
|
readonly statusCode = 403;
|
|
readonly code = 'VK_RESOURCE_PRIVATE';
|
|
}
|
|
|
|
export class NotFoundError extends AppError {
|
|
readonly statusCode = 404;
|
|
readonly code = 'NOT_FOUND';
|
|
}
|
|
|
|
export class ConflictError extends AppError {
|
|
readonly statusCode = 409;
|
|
readonly code = 'CONFLICT';
|
|
}
|
|
|
|
export class IdempotencyKeyReusedError extends AppError {
|
|
readonly statusCode = 409;
|
|
readonly code = 'IDEMPOTENCY_KEY_REUSED';
|
|
|
|
constructor(
|
|
message: string = 'Idempotency key was previously used with different request parameters',
|
|
details?: any
|
|
) {
|
|
super(message, details);
|
|
}
|
|
}
|
|
|
|
export class DrawAlreadyCompletedError extends AppError {
|
|
readonly statusCode = 409;
|
|
readonly code = 'DRAW_ALREADY_COMPLETED';
|
|
|
|
constructor(
|
|
message: string = 'Giveaway has already been drawn and finalized. Repeat draws are not permitted.',
|
|
details?: any
|
|
) {
|
|
super(message, details);
|
|
}
|
|
}
|
|
|
|
export class RateLimitError extends AppError {
|
|
readonly statusCode = 429;
|
|
readonly code = 'RATE_LIMIT_EXCEEDED';
|
|
}
|
|
|
|
export class DependencyUnavailableError extends AppError {
|
|
readonly statusCode = 503;
|
|
readonly code = 'DEPENDENCY_UNAVAILABLE';
|
|
}
|
|
|
|
export class InternalError extends AppError {
|
|
readonly statusCode = 500;
|
|
readonly code = 'INTERNAL_SERVER_ERROR';
|
|
}
|
|
|
|
/**
|
|
* Normalizes any error into a consistent JSON response
|
|
*/
|
|
export function handleApiError(error: unknown): NextResponse {
|
|
// Handle Zod validation errors
|
|
if (error instanceof ZodError) {
|
|
const formattedIssues = error.issues.map(i => ({
|
|
path: i.path.join('.'),
|
|
message: i.message,
|
|
}));
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'Invalid request payload',
|
|
details: formattedIssues,
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Handle known AppErrors
|
|
if (error instanceof AppError) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: error.code,
|
|
message: error.message,
|
|
details: error.details || null,
|
|
},
|
|
},
|
|
{ status: error.statusCode }
|
|
);
|
|
}
|
|
|
|
// Handle typed VK client errors safely without exposing tokens or internal structures
|
|
if (error instanceof VkClientError) {
|
|
switch (error.category) {
|
|
case 'AUTH':
|
|
case 'REAUTHENTICATION_REQUIRED':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_REAUTHENTICATION_REQUIRED',
|
|
message: 'VK authorization expired or required. Please reconnect your VK account.',
|
|
},
|
|
},
|
|
{ status: 401 }
|
|
);
|
|
|
|
case 'PERMISSION':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_PERMISSION_REQUIRED',
|
|
message: 'Insufficient VK permissions to access this resource or perform this action.',
|
|
},
|
|
},
|
|
{ status: 403 }
|
|
);
|
|
|
|
case 'PRIVATE_RESOURCE':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_RESOURCE_PRIVATE',
|
|
message: 'This VK resource or post is in a private/restricted community or profile.',
|
|
},
|
|
},
|
|
{ status: 403 }
|
|
);
|
|
|
|
case 'NOT_FOUND':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_RESOURCE_NOT_FOUND',
|
|
message: 'VK post or resource was not found. Please check the post URL.',
|
|
},
|
|
},
|
|
{ status: 404 }
|
|
);
|
|
|
|
case 'RATE_LIMIT':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_RATE_LIMIT_EXCEEDED',
|
|
message: 'VK API rate limit reached. Please retry in a few moments.',
|
|
},
|
|
},
|
|
{ status: 429 }
|
|
);
|
|
|
|
case 'TIMEOUT':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_GATEWAY_TIMEOUT',
|
|
message: 'VK API did not respond in time. Please try again.',
|
|
},
|
|
},
|
|
{ status: 504 }
|
|
);
|
|
|
|
case 'INCOMPLETE_DATA':
|
|
case 'PAGINATION_LIMIT_REACHED':
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_INCOMPLETE_DATA',
|
|
message: error.message,
|
|
},
|
|
},
|
|
{ status: 502 }
|
|
);
|
|
|
|
default:
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'VK_UPSTREAM_ERROR',
|
|
message: 'A temporary error occurred while communicating with VK API.',
|
|
},
|
|
},
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Handle SyntaxError (Malformed JSON in request body)
|
|
if (error instanceof SyntaxError && 'body' in error) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'INVALID_JSON',
|
|
message: 'Malformed JSON payload in request body',
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Handle Prisma Known Request Errors
|
|
const anyErr = error as any;
|
|
if (anyErr?.code === 'P2002') {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'CONFLICT',
|
|
message: 'Resource conflict or duplicate constraint violation',
|
|
},
|
|
},
|
|
{ status: 409 }
|
|
);
|
|
}
|
|
|
|
// Unexpected runtime errors
|
|
console.error('Unhandled server error:', error);
|
|
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
const errorMessage = isProd
|
|
? 'An unexpected internal server error occurred'
|
|
: (error instanceof Error ? error.message : 'Internal Server Error');
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: 'INTERNAL_SERVER_ERROR',
|
|
message: errorMessage,
|
|
},
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|