mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 16:26:39 +00:00
add backend API maintenance screen
This commit is contained in:
@@ -57,6 +57,8 @@ unauthenticated.get("/", (_, res) => {
|
|||||||
res.status(HttpCode.OK).json({ message: "Healthy" });
|
res.status(HttpCode.OK).json({ message: "Healthy" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
unauthenticated.get("/maintenance/info", resource.getMaintenanceInfo);
|
||||||
|
|
||||||
// Authenticated Root routes
|
// Authenticated Root routes
|
||||||
export const authenticated = Router();
|
export const authenticated = Router();
|
||||||
authenticated.use(verifySessionUserMiddleware);
|
authenticated.use(verifySessionUserMiddleware);
|
||||||
|
|||||||
104
server/routers/resource/getMaintenanceInfo.ts
Normal file
104
server/routers/resource/getMaintenanceInfo.ts
Normal 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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,3 +30,4 @@ export * from "./removeRoleFromResource";
|
|||||||
export * from "./addUserToResource";
|
export * from "./addUserToResource";
|
||||||
export * from "./removeUserFromResource";
|
export * from "./removeUserFromResource";
|
||||||
export * from "./listAllResourceNames";
|
export * from "./listAllResourceNames";
|
||||||
|
export * from "./getMaintenanceInfo";
|
||||||
|
|||||||
@@ -1,51 +1,57 @@
|
|||||||
import { headers } from 'next/headers';
|
|
||||||
import { db } from '@server/db';
|
import { headers } from "next/headers";
|
||||||
import { resources } from '@server/db';
|
import { internal } from "@app/lib/api";
|
||||||
import { eq } from 'drizzle-orm';
|
import { AxiosResponse } from "axios";
|
||||||
|
import { GetMaintenanceInfoResponse } from "@server/routers/resource";
|
||||||
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
export default async function MaintenanceScreen() {
|
export default async function MaintenanceScreen() {
|
||||||
let resource = null;
|
let title = "Service Temporarily Unavailable";
|
||||||
let title = 'Service Temporarily Unavailable';
|
let message =
|
||||||
let message = 'We are currently experiencing technical difficulties. Please check back soon.';
|
"We are currently experiencing technical difficulties. Please check back soon.";
|
||||||
let estimatedTime;
|
let estimatedTime: string | null = null;
|
||||||
|
|
||||||
try {
|
// Check if we're in build mode
|
||||||
const headersList = await headers();
|
const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build';
|
||||||
const host = headersList.get('host') || '';
|
|
||||||
const hostname = host.split(':')[0];
|
|
||||||
|
|
||||||
const [res] = await db
|
if (!isBuildTime) {
|
||||||
.select()
|
try {
|
||||||
.from(resources)
|
const headersList = await headers();
|
||||||
.where(eq(resources.fullDomain, hostname))
|
const host = headersList.get("host") || "";
|
||||||
.limit(1);
|
const hostname = host.split(":")[0];
|
||||||
|
|
||||||
resource = res;
|
const res = await internal.get<AxiosResponse<GetMaintenanceInfoResponse>>(
|
||||||
title = resource?.maintenanceTitle || title;
|
`/maintenance/info?fullDomain=${encodeURIComponent(hostname)}`
|
||||||
message = resource?.maintenanceMessage || message;
|
);
|
||||||
estimatedTime = resource?.maintenanceEstimatedTime;
|
|
||||||
} catch (err) {
|
if (res && res.status === 200) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const maintenanceInfo = res.data.data;
|
||||||
console.warn("Skipping DB lookup during build or missing config:", msg);
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
<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="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-center">
|
||||||
<div className="text-6xl mb-6 animate-pulse">
|
<div className="text-6xl mb-6 animate-pulse">🔧</div>
|
||||||
🔧
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold text-black mb-4">
|
<h1 className="text-4xl font-bold text-black mb-4">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-xl text-black/90 mb-6">
|
<p className="text-xl text-black/90 mb-6">{message}</p>
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{estimatedTime && (
|
{estimatedTime && (
|
||||||
<div className="mt-8 p-4 bg-white/15 rounded-xl">
|
<div className="mt-8 p-4 bg-white/15 rounded-xl">
|
||||||
@@ -61,4 +67,4 @@ export default async function MaintenanceScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user