2025-11-24 01:00:50 +01:00
|
|
|
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
|
|
|
import type { Context } from 'hono';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Standard application error with code for easy identification
|
|
|
|
|
*/
|
|
|
|
|
export class AppError extends Error {
|
|
|
|
|
constructor(
|
|
|
|
|
error: string | Error,
|
|
|
|
|
public code: string,
|
|
|
|
|
public statusCode: ContentfulStatusCode = 500,
|
|
|
|
|
public field?: string
|
|
|
|
|
) {
|
|
|
|
|
super(error instanceof Error ? error.message : error);
|
|
|
|
|
this.name = 'AppError';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Standard error response format
|
|
|
|
|
*/
|
|
|
|
|
export interface ErrorResponse {
|
|
|
|
|
success: false;
|
|
|
|
|
error: string;
|
|
|
|
|
code: string;
|
|
|
|
|
field?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Error codes constants for consistency
|
|
|
|
|
export const ErrorCodes = {
|
2026-03-09 11:18:22 +01:00
|
|
|
// Common errors
|
|
|
|
|
NOT_FOUND: 'NOT_FOUND',
|
|
|
|
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
|
|
|
|
2025-12-02 15:27:41 +01:00
|
|
|
// Dashboard Analytics errors
|
|
|
|
|
ORG_ANALYTICS_FETCH_FAILED: 'ORG_ANALYTICS_FETCH_FAILED',
|
|
|
|
|
NO_ORG_ID_PROVIDED: 'NO_ORG_ID_PROVIDED',
|
|
|
|
|
|
2025-12-22 19:57:27 +01:00
|
|
|
// Community errors
|
|
|
|
|
UPDATE_COMMUNITY_QUESTION_FAILED: 'UPDATE_COMMUNITY_QUESTION_FAILED',
|
|
|
|
|
ADD_COMMUNITY_QUESTION_FAILED: 'ADD_COMMUNITY_QUESTION_FAILED',
|
|
|
|
|
COMMUNITY_QUESTIONS_FETCH_FAILED: 'COMMUNITY_QUESTIONS_FETCH_FAILED',
|
|
|
|
|
COMMUNITY_QUESTION_NOT_FOUND: 'COMMUNITY_QUESTION_NOT_FOUND',
|
|
|
|
|
COMMENT_SUBMISSION_FAILED: 'COMMENT_SUBMISSION_FAILED',
|
|
|
|
|
UPVOTE_FAILED: 'UPVOTE_FAILED',
|
|
|
|
|
DELETE_COMMENT_FAILED: 'DELETE_COMMENT_FAILED',
|
|
|
|
|
DELETE_QUESTION_FAILED: 'DELETE_QUESTION_FAILED',
|
|
|
|
|
|
2025-11-24 01:00:50 +01:00
|
|
|
// Organization errors
|
|
|
|
|
SITENAME_EXISTS: 'SITENAME_EXISTS',
|
|
|
|
|
ORG_CREATE_FAILED: 'ORG_CREATE_FAILED',
|
|
|
|
|
ORG_NOT_FOUND: 'ORG_NOT_FOUND',
|
|
|
|
|
ORGANIZATION_NOT_FOUND: 'ORGANIZATION_NOT_FOUND',
|
|
|
|
|
ORG_TEAM_FETCH_FAILED: 'ORG_TEAM_FETCH_FAILED',
|
|
|
|
|
ORG_AUDIENCE_FETCH_FAILED: 'ORG_AUDIENCE_FETCH_FAILED',
|
|
|
|
|
COURSES_FETCH_FAILED: 'COURSES_FETCH_FAILED',
|
|
|
|
|
ORG_PLAN_CREATE_FAILED: 'ORG_PLAN_CREATE_FAILED',
|
|
|
|
|
ORG_PLAN_UPDATE_FAILED: 'ORG_PLAN_UPDATE_FAILED',
|
|
|
|
|
ORG_PLAN_CANCEL_FAILED: 'ORG_PLAN_CANCEL_FAILED',
|
|
|
|
|
ORG_PLAN_NOT_FOUND: 'ORG_PLAN_NOT_FOUND',
|
2025-12-02 09:54:38 +01:00
|
|
|
ORG_TEAM_INVITE_FAILED: 'ORG_TEAM_INVITE_FAILED',
|
|
|
|
|
ORG_TEAM_REMOVE_FAILED: 'ORG_TEAM_REMOVE_FAILED',
|
|
|
|
|
ORG_TEAM_EMAIL_EXISTS: 'ORG_TEAM_EMAIL_EXISTS',
|
|
|
|
|
ORG_TEAM_NOT_AUTHORIZED: 'ORG_TEAM_NOT_AUTHORIZED',
|
2026-03-09 11:18:22 +01:00
|
|
|
TAG_GROUP_NOT_FOUND: 'TAG_GROUP_NOT_FOUND',
|
|
|
|
|
TAG_NOT_FOUND: 'TAG_NOT_FOUND',
|
|
|
|
|
TAG_FETCH_FAILED: 'TAG_FETCH_FAILED',
|
|
|
|
|
TAG_CREATE_FAILED: 'TAG_CREATE_FAILED',
|
|
|
|
|
TAG_UPDATE_FAILED: 'TAG_UPDATE_FAILED',
|
|
|
|
|
TAG_ASSIGNMENT_FAILED: 'TAG_ASSIGNMENT_FAILED',
|
|
|
|
|
|
|
|
|
|
// Asset errors
|
|
|
|
|
ASSET_NOT_FOUND: 'ASSET_NOT_FOUND',
|
|
|
|
|
ASSET_IN_USE: 'ASSET_IN_USE',
|
|
|
|
|
ASSET_USAGE_NOT_FOUND: 'ASSET_USAGE_NOT_FOUND',
|
|
|
|
|
ASSET_CREATE_FAILED: 'ASSET_CREATE_FAILED',
|
|
|
|
|
ASSET_FETCH_FAILED: 'ASSET_FETCH_FAILED',
|
|
|
|
|
ASSET_LIST_FAILED: 'ASSET_LIST_FAILED',
|
|
|
|
|
ASSET_UPDATE_FAILED: 'ASSET_UPDATE_FAILED',
|
|
|
|
|
ASSET_DELETE_FAILED: 'ASSET_DELETE_FAILED',
|
|
|
|
|
ASSET_ATTACH_FAILED: 'ASSET_ATTACH_FAILED',
|
|
|
|
|
ASSET_DETACH_FAILED: 'ASSET_DETACH_FAILED',
|
|
|
|
|
ASSET_USAGE_FETCH_FAILED: 'ASSET_USAGE_FETCH_FAILED',
|
|
|
|
|
ASSET_EXPORT_FAILED: 'ASSET_EXPORT_FAILED',
|
|
|
|
|
ASSET_STORAGE_FETCH_FAILED: 'ASSET_STORAGE_FETCH_FAILED',
|
2025-11-24 01:00:50 +01:00
|
|
|
|
|
|
|
|
// Profile errors
|
|
|
|
|
PROFILE_UPDATE_FAILED: 'PROFILE_UPDATE_FAILED',
|
|
|
|
|
PROFILE_NOT_FOUND: 'PROFILE_NOT_FOUND',
|
|
|
|
|
|
|
|
|
|
// Account errors
|
|
|
|
|
ACCOUNT_NOT_FOUND: 'ACCOUNT_NOT_FOUND',
|
|
|
|
|
|
|
|
|
|
// Auth errors
|
|
|
|
|
FORBIDDEN: 'FORBIDDEN',
|
|
|
|
|
|
2026-03-09 11:18:22 +01:00
|
|
|
// Course errors
|
|
|
|
|
COURSE_NOT_FOUND: 'COURSE_NOT_FOUND',
|
|
|
|
|
COURSE_FETCH_FAILED: 'COURSE_FETCH_FAILED',
|
|
|
|
|
|
|
|
|
|
// Lesson errors
|
|
|
|
|
LESSON_NOT_FOUND: 'LESSON_NOT_FOUND',
|
|
|
|
|
LESSON_FETCH_FAILED: 'LESSON_FETCH_FAILED',
|
|
|
|
|
COURSE_SECTION_NOT_FOUND: 'COURSE_SECTION_NOT_FOUND',
|
|
|
|
|
|
|
|
|
|
// Exercise errors
|
|
|
|
|
EXERCISE_NOT_FOUND: 'EXERCISE_NOT_FOUND',
|
|
|
|
|
EXERCISE_FETCH_FAILED: 'EXERCISE_FETCH_FAILED',
|
|
|
|
|
|
|
|
|
|
// Submission errors
|
|
|
|
|
SUBMISSION_NOT_FOUND: 'SUBMISSION_NOT_FOUND',
|
|
|
|
|
SUBMISSION_FETCH_FAILED: 'SUBMISSION_FETCH_FAILED',
|
|
|
|
|
|
|
|
|
|
// Comment errors
|
|
|
|
|
COMMENT_NOT_FOUND: 'COMMENT_NOT_FOUND',
|
|
|
|
|
LESSON_COMMENT_UPDATE_FAILED: 'LESSON_COMMENT_UPDATE_FAILED',
|
|
|
|
|
NEWSFEED_COMMENT_UPDATE_FAILED: 'NEWSFEED_COMMENT_UPDATE_FAILED',
|
|
|
|
|
|
|
|
|
|
// Quiz errors
|
|
|
|
|
QUIZ_NOT_FOUND: 'QUIZ_NOT_FOUND',
|
|
|
|
|
QUIZ_FETCH_FAILED: 'QUIZ_FETCH_FAILED',
|
|
|
|
|
QUIZ_CREATE_FAILED: 'QUIZ_CREATE_FAILED',
|
|
|
|
|
QUIZ_UPDATE_FAILED: 'QUIZ_UPDATE_FAILED',
|
|
|
|
|
QUIZ_DELETE_FAILED: 'QUIZ_DELETE_FAILED',
|
|
|
|
|
|
|
|
|
|
// Lesson Language errors
|
|
|
|
|
LESSON_LANGUAGE_NOT_FOUND: 'LESSON_LANGUAGE_NOT_FOUND',
|
|
|
|
|
LESSON_LANGUAGE_FETCH_FAILED: 'LESSON_LANGUAGE_FETCH_FAILED',
|
|
|
|
|
LESSON_LANGUAGE_CREATE_FAILED: 'LESSON_LANGUAGE_CREATE_FAILED',
|
|
|
|
|
LESSON_LANGUAGE_UPDATE_FAILED: 'LESSON_LANGUAGE_UPDATE_FAILED',
|
|
|
|
|
|
|
|
|
|
// SSO errors
|
|
|
|
|
SSO_REGISTRATION_FAILED: 'SSO_REGISTRATION_FAILED',
|
|
|
|
|
SSO_NOT_FOUND: 'SSO_NOT_FOUND',
|
|
|
|
|
SSO_NOT_ENABLED: 'SSO_NOT_ENABLED',
|
|
|
|
|
UPGRADE_REQUIRED: 'UPGRADE_REQUIRED',
|
|
|
|
|
CONFLICT: 'CONFLICT',
|
|
|
|
|
|
|
|
|
|
// Token exchange errors
|
|
|
|
|
TOKEN_EXCHANGE_INVALID_TOKEN: 'TOKEN_EXCHANGE_INVALID_TOKEN',
|
|
|
|
|
TOKEN_EXCHANGE_EXPIRED: 'TOKEN_EXCHANGE_EXPIRED',
|
|
|
|
|
TOKEN_EXCHANGE_ORG_NOT_FOUND: 'TOKEN_EXCHANGE_ORG_NOT_FOUND',
|
|
|
|
|
TOKEN_EXCHANGE_NOT_ENABLED: 'TOKEN_EXCHANGE_NOT_ENABLED',
|
|
|
|
|
|
|
|
|
|
// Token auth admin errors
|
|
|
|
|
TOKEN_AUTH_ALREADY_EXISTS: 'TOKEN_AUTH_ALREADY_EXISTS',
|
|
|
|
|
|
2025-11-24 01:00:50 +01:00
|
|
|
// Generic
|
|
|
|
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
|
|
|
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles errors consistently across all routes
|
|
|
|
|
* @param c - Hono context
|
|
|
|
|
* @param error - The error to handle
|
|
|
|
|
* @param fallbackMessage - Default error message if error is not an AppError
|
|
|
|
|
* @param fallbackCode - Default error code if error is not an AppError
|
|
|
|
|
* @returns JSON error response with appropriate status code
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```typescript
|
|
|
|
|
* try {
|
|
|
|
|
* const data = await someService();
|
|
|
|
|
* return c.json({ success: true, data });
|
|
|
|
|
* } catch (error) {
|
|
|
|
|
* return handleError(c, error, 'Failed to process request');
|
|
|
|
|
* }
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export const handleError = (
|
|
|
|
|
c: Context,
|
|
|
|
|
error: unknown,
|
|
|
|
|
fallbackMessage: string = 'An unexpected error occurred',
|
2026-03-09 11:18:22 +01:00
|
|
|
fallbackCode?: string
|
2025-11-24 01:00:50 +01:00
|
|
|
) => {
|
|
|
|
|
console.error('Error in route:', error);
|
|
|
|
|
|
|
|
|
|
if (error instanceof AppError) {
|
2026-03-09 11:18:22 +01:00
|
|
|
const isServerError = error.statusCode >= 500;
|
|
|
|
|
const responseCode = isServerError ? (fallbackCode ?? error.code) : error.code;
|
|
|
|
|
const responseMessage = isServerError ? fallbackMessage : error.message;
|
|
|
|
|
|
2025-11-24 01:00:50 +01:00
|
|
|
return c.json<ErrorResponse>(
|
|
|
|
|
{
|
|
|
|
|
success: false,
|
2026-03-09 11:18:22 +01:00
|
|
|
error: responseMessage,
|
|
|
|
|
code: responseCode,
|
|
|
|
|
field: isServerError ? undefined : error.field
|
2025-11-24 01:00:50 +01:00
|
|
|
},
|
|
|
|
|
error.statusCode
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.json<ErrorResponse>(
|
|
|
|
|
{
|
|
|
|
|
success: false,
|
|
|
|
|
error: fallbackMessage,
|
2026-03-09 11:18:22 +01:00
|
|
|
code: fallbackCode ?? ErrorCodes.INTERNAL_ERROR
|
2025-11-24 01:00:50 +01:00
|
|
|
},
|
|
|
|
|
500
|
|
|
|
|
);
|
|
|
|
|
};
|