diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 2c5bfc0e..c8762dfd 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -8,7 +8,7 @@ * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.2.6' +const PACKAGE_VERSION = '2.2.9' const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/src/app/actions/app-actions/start-app-action.ts b/src/app/actions/app-actions/start-app-action.ts index edc47327..41c0c94c 100644 --- a/src/app/actions/app-actions/start-app-action.ts +++ b/src/app/actions/app-actions/start-app-action.ts @@ -14,7 +14,7 @@ const input = z.object({ id: z.string() }); */ export const startAppAction = action(input, async ({ id }) => { try { - ensureUser(); + await ensureUser(); await appService.startApp(id); diff --git a/src/app/actions/utils/ensure-user.ts b/src/app/actions/utils/ensure-user.ts index 612050e2..9f988610 100644 --- a/src/app/actions/utils/ensure-user.ts +++ b/src/app/actions/utils/ensure-user.ts @@ -5,7 +5,7 @@ export const ensureUser = async () => { const user = await getUserFromCookie(); if (!user) { - throw new TranslatedError('SYSTEM_ERROR_YOU_MUST_BE_LOGGED_IN'); + throw new TranslatedError('SYSTEM_ERROR_YOU_MUST_BE_LOGGED_IN', {}, 401); } return user; diff --git a/src/app/actions/utils/handle-api-error.ts b/src/app/actions/utils/handle-api-error.ts new file mode 100644 index 00000000..554b1113 --- /dev/null +++ b/src/app/actions/utils/handle-api-error.ts @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/nextjs'; +import { MessageKey, TranslatedError } from '@/server/utils/errors'; +import { getTranslatorFromCookie } from '@/lib/get-translator'; +import { TipiConfig } from '@/server/core/TipiConfig'; +import { Logger } from '../../../server/core/Logger'; + +/** + * Given an error, returns a 500 response with the translated error message. + */ +export const handleApiError = async (e: unknown) => { + const originalMessage = e instanceof Error ? e.message : e; + const status = e instanceof TranslatedError ? e.status : 500; + const errorVariables = e instanceof TranslatedError ? e.variableValues : {}; + + const translator = await getTranslatorFromCookie(); + const messageTranslated = + e instanceof TranslatedError ? translator(originalMessage as MessageKey, errorVariables) : translator('INTERNAL_SERVER_ERROR'); + + // Non TranslatedErrors are unexpected and should be reported to Sentry. + if (!(e instanceof TranslatedError) && TipiConfig.getConfig().allowErrorMonitoring) { + Logger.error(e); + Sentry.captureException(e); + } + + return new Response(messageTranslated as string, { status }); +}; diff --git a/src/app/api/app-image/route.ts b/src/app/api/app-image/route.ts index d688c806..1fd52d2b 100644 --- a/src/app/api/app-image/route.ts +++ b/src/app/api/app-image/route.ts @@ -1,9 +1,9 @@ -import * as Sentry from '@sentry/nextjs'; import { TipiConfig } from '@/server/core/TipiConfig/TipiConfig'; import { pathExists } from '@runtipi/shared/node'; import fs from 'fs-extra'; import path from 'path'; import { sanitizePath } from '@runtipi/shared'; +import { handleApiError } from '@/actions/utils/handle-api-error'; import { APP_DIR, DATA_DIR } from '../../../config/constants'; export async function GET(request: Request) { @@ -30,7 +30,6 @@ export async function GET(request: Request) { return new Response(file, { headers: { 'content-type': 'image/jpeg' } }); } catch (error) { - Sentry.captureException(error); - return new Response('Error', { status: 500 }); + return handleApiError(error); } } diff --git a/src/app/api/app-store/route.ts b/src/app/api/app-store/route.ts index fc416f4b..9483c3fe 100644 --- a/src/app/api/app-store/route.ts +++ b/src/app/api/app-store/route.ts @@ -1,3 +1,5 @@ +import { ensureUser } from '@/actions/utils/ensure-user'; +import { handleApiError } from '@/actions/utils/handle-api-error'; import { appService } from '@/server/services/apps/apps.service'; const getApps = async (searchParams: URLSearchParams) => { @@ -10,11 +12,17 @@ const getApps = async (searchParams: URLSearchParams) => { }; export async function GET(request: Request) { - const { searchParams } = new URL(request.url); + try { + await ensureUser(); - const apps = await getApps(searchParams); + const { searchParams } = new URL(request.url); - return new Response(JSON.stringify(apps), { headers: { 'content-type': 'application/json' } }); + const apps = await getApps(searchParams); + + return new Response(JSON.stringify(apps), { headers: { 'content-type': 'application/json' } }); + } catch (error) { + return handleApiError(error); + } } export type AppStoreApiResponse = Awaited>; diff --git a/src/app/api/certificate/route.ts b/src/app/api/certificate/route.ts index 9949eb6d..18f9c49e 100644 --- a/src/app/api/certificate/route.ts +++ b/src/app/api/certificate/route.ts @@ -1,6 +1,6 @@ -import * as Sentry from '@sentry/nextjs'; import { getUserFromCookie } from '@/server/common/session.helpers'; import fs from 'fs-extra'; +import { handleApiError } from '@/actions/utils/handle-api-error'; import { DATA_DIR } from '../../../config/constants'; export async function GET() { @@ -26,7 +26,6 @@ export async function GET() { return new Response('Forbidden', { status: 403 }); } catch (error) { - Sentry.captureException(error); - return new Response('Error', { status: 500 }); + return handleApiError(error); } } diff --git a/src/app/api/system-status/route.ts b/src/app/api/system-status/route.ts index 3996f8c3..a92b7501 100644 --- a/src/app/api/system-status/route.ts +++ b/src/app/api/system-status/route.ts @@ -1,5 +1,5 @@ -import * as Sentry from '@sentry/nextjs'; import { ensureUser } from '@/actions/utils/ensure-user'; +import { handleApiError } from '@/actions/utils/handle-api-error'; import { fetchSystemStatus } from './fetch-system-status'; export async function GET() { @@ -9,7 +9,6 @@ export async function GET() { const data = await fetchSystemStatus(); return new Response(JSON.stringify(data), { status: 200 }); } catch (error) { - Sentry.captureException(error); - return new Response('Error', { status: 500 }); + return handleApiError(error); } } diff --git a/src/client/messages/en.json b/src/client/messages/en.json index 4407b346..d46677cd 100644 --- a/src/client/messages/en.json +++ b/src/client/messages/en.json @@ -173,6 +173,7 @@ "HEADER_SOURCE_CODE": "Source code", "HEADER_SPONSOR": "Sponsor", "HEADER_UPDATE_AVAILABLE": "Update available", + "INTERNAL_SERVER_ERROR": "Internal server error", "LINKS_ADD_SUBMIT": "Submit", "LINKS_ADD_SUBTITLE": "Add external link to the dashboard", "LINKS_ADD_SUCCESS": "Link added succesfully", diff --git a/src/server/utils/errors.ts b/src/server/utils/errors.ts index 1430f527..2054c828 100644 --- a/src/server/utils/errors.ts +++ b/src/server/utils/errors.ts @@ -8,12 +8,15 @@ export type MessageKey = Parameters[0]; export class TranslatedError extends Error { public readonly variableValues: TranslationValues; - constructor(message: MessageKey, variableValues: TranslationValues = {}) { + public readonly status: number; + + constructor(message: MessageKey, variableValues: TranslationValues = {}, status?: number) { super(message); Logger.error(`server error: ${t(message, variableValues)}`); this.name = 'TranslatedError'; this.variableValues = variableValues; + this.status = status || 500; } }