add backend API maintenance screen

This commit is contained in:
Owen
2025-12-20 15:42:33 -05:00
committed by Owen Schwartz
parent 8af95ea1ca
commit b8ffc601d4
4 changed files with 145 additions and 32 deletions

View File

@@ -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);

View File

@@ -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<ReturnType<typeof query>>
>;
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<any> {
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<GetMaintenanceInfoResponse>(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"
)
);
}
}

View File

@@ -30,3 +30,4 @@ export * from "./removeRoleFromResource";
export * from "./addUserToResource";
export * from "./removeUserFromResource";
export * from "./listAllResourceNames";
export * from "./getMaintenanceInfo";

View File

@@ -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<AxiosResponse<GetMaintenanceInfoResponse>>(
`/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 (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="max-w-2xl w-full bg-white/10 backdrop-blur-lg rounded-3xl p-8 shadow-2xl border border-white/20">
<div className="text-center">
<div className="text-6xl mb-6 animate-pulse">
🔧
</div>
<div className="text-6xl mb-6 animate-pulse">🔧</div>
<h1 className="text-4xl font-bold text-black mb-4">
{title}
</h1>
<p className="text-xl text-black/90 mb-6">
{message}
</p>
<p className="text-xl text-black/90 mb-6">{message}</p>
{estimatedTime && (
<div className="mt-8 p-4 bg-white/15 rounded-xl">
@@ -61,4 +67,4 @@ export default async function MaintenanceScreen() {
</div>
</div>
);
}
}