From b8ffc601d49bdb1471b59e890ba42873d83884db Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 15:42:33 -0500 Subject: [PATCH] add backend API maintenance screen --- server/routers/external.ts | 2 + server/routers/resource/getMaintenanceInfo.ts | 104 ++++++++++++++++++ server/routers/resource/index.ts | 1 + src/app/maintenance-screen/page.tsx | 70 ++++++------ 4 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 server/routers/resource/getMaintenanceInfo.ts diff --git a/server/routers/external.ts b/server/routers/external.ts index f85cc4be..4600e742 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -57,6 +57,8 @@ unauthenticated.get("/", (_, res) => { res.status(HttpCode.OK).json({ message: "Healthy" }); }); +unauthenticated.get("/maintenance/info", resource.getMaintenanceInfo); + // Authenticated Root routes export const authenticated = Router(); authenticated.use(verifySessionUserMiddleware); diff --git a/server/routers/resource/getMaintenanceInfo.ts b/server/routers/resource/getMaintenanceInfo.ts new file mode 100644 index 00000000..68d7e350 --- /dev/null +++ b/server/routers/resource/getMaintenanceInfo.ts @@ -0,0 +1,104 @@ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resources } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const getMaintenanceInfoSchema = z + .object({ + fullDomain: z.string().min(1, "Domain is required") + }) + .strict(); + +async function query(fullDomain: string) { + const [res] = await db + .select({ + resourceId: resources.resourceId, + name: resources.name, + fullDomain: resources.fullDomain, + maintenanceModeEnabled: resources.maintenanceModeEnabled, + maintenanceModeType: resources.maintenanceModeType, + maintenanceTitle: resources.maintenanceTitle, + maintenanceMessage: resources.maintenanceMessage, + maintenanceEstimatedTime: resources.maintenanceEstimatedTime + }) + .from(resources) + .where(eq(resources.fullDomain, fullDomain)) + .limit(1); + return res; +} + +export type GetMaintenanceInfoResponse = NonNullable< + Awaited> +>; + +registry.registerPath({ + method: "get", + path: "/maintenance/info", + description: "Get maintenance information for a resource by domain.", + tags: [OpenAPITags.Resource], + request: { + query: z.object({ + fullDomain: z.string() + }) + }, + responses: { + 200: { + description: "Maintenance information retrieved successfully" + }, + 404: { + description: "Resource not found" + } + } +}); + +export async function getMaintenanceInfo( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = getMaintenanceInfoSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { fullDomain } = parsedQuery.data; + + const maintenanceInfo = await query(fullDomain); + + if (!maintenanceInfo) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource not found") + ); + } + + return response(res, { + data: maintenanceInfo, + success: true, + error: false, + message: "Maintenance information retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while retrieving maintenance information" + ) + ); + } +} diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index e85d30f5..5c8bc481 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -30,3 +30,4 @@ export * from "./removeRoleFromResource"; export * from "./addUserToResource"; export * from "./removeUserFromResource"; export * from "./listAllResourceNames"; +export * from "./getMaintenanceInfo"; diff --git a/src/app/maintenance-screen/page.tsx b/src/app/maintenance-screen/page.tsx index 26120385..eb665bb5 100644 --- a/src/app/maintenance-screen/page.tsx +++ b/src/app/maintenance-screen/page.tsx @@ -1,51 +1,57 @@ -import { headers } from 'next/headers'; -import { db } from '@server/db'; -import { resources } from '@server/db'; -import { eq } from 'drizzle-orm'; + +import { headers } from "next/headers"; +import { internal } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { GetMaintenanceInfoResponse } from "@server/routers/resource"; + export const dynamic = "force-dynamic"; +export const revalidate = 0; export default async function MaintenanceScreen() { - let resource = null; - let title = 'Service Temporarily Unavailable'; - let message = 'We are currently experiencing technical difficulties. Please check back soon.'; - let estimatedTime; + let title = "Service Temporarily Unavailable"; + let message = + "We are currently experiencing technical difficulties. Please check back soon."; + let estimatedTime: string | null = null; - try { - const headersList = await headers(); - const host = headersList.get('host') || ''; - const hostname = host.split(':')[0]; + // Check if we're in build mode + const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build'; - const [res] = await db - .select() - .from(resources) - .where(eq(resources.fullDomain, hostname)) - .limit(1); + if (!isBuildTime) { + try { + const headersList = await headers(); + const host = headersList.get("host") || ""; + const hostname = host.split(":")[0]; - resource = res; - title = resource?.maintenanceTitle || title; - message = resource?.maintenanceMessage || message; - estimatedTime = resource?.maintenanceEstimatedTime; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - console.warn("Skipping DB lookup during build or missing config:", msg); + const res = await internal.get>( + `/maintenance/info?fullDomain=${encodeURIComponent(hostname)}` + ); + + if (res && res.status === 200) { + const maintenanceInfo = res.data.data; + title = maintenanceInfo?.maintenanceTitle || title; + message = maintenanceInfo?.maintenanceMessage || message; + estimatedTime = maintenanceInfo?.maintenanceEstimatedTime || null; + } + } catch (err) { + console.warn( + "Failed to fetch maintenance info", + err instanceof Error ? err.message : String(err) + ); + } } - + return (
-
- 🔧 -
+
🔧

{title}

-

- {message} -

+

{message}

{estimatedTime && (
@@ -61,4 +67,4 @@ export default async function MaintenanceScreen() {
); -} +} \ No newline at end of file