move openapi out of src dir
Adapted from #3579 by @gorkem-bwl. The openapi/registry, helpers, and per-resource routes/* files are largely their work; this commit relocates the directory out of src/ so the build-time generator code is not emitted into dist/, and shifts spec generation from runtime to a build step. Co-authored-by: gorkem-bwl <gorkem.cetin@bluewavelabs.ca>
This commit is contained in:
1 parent
28baeaf218
commit
3f9019f34c
22 files changed
+11259
-3944
No files matched your search
+10031
-3940
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
||||
import "./registry.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export const successEnvelope = <T extends z.ZodTypeAny>(data: T) =>
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
msg: z.string(),
|
||||
data,
|
||||
});
|
||||
|
||||
export const successEnvelopeNoData = z.object({
|
||||
success: z.literal(true),
|
||||
msg: z.string(),
|
||||
});
|
||||
|
||||
export const errorEnvelope = z.object({
|
||||
success: z.literal(false),
|
||||
msg: z.string(),
|
||||
data: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export const bearer = [{ bearerAuth: [] }];
|
||||
|
||||
export const json = <T extends z.ZodTypeAny>(schema: T, example?: unknown) => ({
|
||||
"application/json": example === undefined ? { schema } : { schema, example },
|
||||
});
|
||||
|
||||
export const standardErrors = {
|
||||
"401": { description: "Unauthorized", content: json(errorEnvelope) },
|
||||
"403": { description: "Forbidden", content: json(errorEnvelope) },
|
||||
"500": { description: "Internal server error", content: json(errorEnvelope) },
|
||||
};
|
||||
|
||||
export const okJson = <T extends z.ZodTypeAny>(data: T, description = "OK", example?: unknown) => ({
|
||||
description,
|
||||
content: json(successEnvelope(data), example === undefined ? undefined : { success: true, msg: "OK", data: example }),
|
||||
});
|
||||
|
||||
export const okJsonNoData = (description = "OK") => ({
|
||||
description,
|
||||
content: json(successEnvelopeNoData, { success: true, msg: "OK" }),
|
||||
});
|
||||
|
||||
export const okUnknown = okJson(z.unknown());
|
||||
|
||||
export const multipart = (fields: Record<string, z.ZodTypeAny>, fileField?: string) => {
|
||||
const shape: Record<string, z.ZodTypeAny> = { ...fields };
|
||||
if (fileField) {
|
||||
shape[fileField] = z.string().openapi({ type: "string", format: "binary" });
|
||||
}
|
||||
return {
|
||||
"multipart/form-data": { schema: z.object(shape) },
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import "./registry.js";
|
||||
import "./routes/auth.js";
|
||||
import "./routes/check.js";
|
||||
import "./routes/diagnostic.js";
|
||||
import "./routes/geoCheck.js";
|
||||
import "./routes/incident.js";
|
||||
import "./routes/invite.js";
|
||||
import "./routes/log.js";
|
||||
import "./routes/maintenanceWindow.js";
|
||||
import "./routes/monitor.js";
|
||||
import "./routes/notification.js";
|
||||
import "./routes/queue.js";
|
||||
import "./routes/settings.js";
|
||||
import "./routes/statusPage.js";
|
||||
|
||||
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
|
||||
import type { JsonObject } from "swagger-ui-express";
|
||||
import { registry } from "./registry.js";
|
||||
|
||||
let cached: JsonObject | null = null;
|
||||
|
||||
export function getOpenApiSpec(): JsonObject {
|
||||
if (cached) return cached;
|
||||
|
||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||
const document = generator.generateDocument({
|
||||
openapi: "3.0.0",
|
||||
info: {
|
||||
title: "Checkmate API",
|
||||
version: "1.0.0",
|
||||
description: "Generated from Zod validation schemas. Source of truth: server/src/validation/*.ts",
|
||||
},
|
||||
servers: [{ url: "/api/v1", description: "Current server" }],
|
||||
tags: [
|
||||
{ name: "auth", description: "Sign up, log in, password reset, and the authenticated user's profile." },
|
||||
{ name: "monitors", description: "Configure and inspect monitors (HTTP, port, ping, hardware, docker, game, gRPC) for the caller's team." },
|
||||
{ name: "checks", description: "Individual monitoring check results, including history, summaries, and bulk deletes." },
|
||||
{ name: "geo-checks", description: "Geographic check results for monitors with multi-region probing enabled." },
|
||||
{ name: "incidents", description: "Downtime incidents derived from monitor state changes, including filtering and CSV export." },
|
||||
{
|
||||
name: "notifications",
|
||||
description: "Notification channels (email, webhook, Slack, Discord, PagerDuty, Matrix, Teams, Telegram, Pushover, Twilio) and test alerts.",
|
||||
},
|
||||
{ name: "maintenance-window", description: "Scheduled maintenance windows during which monitors do not generate incidents or alerts." },
|
||||
{ name: "status-page", description: "Public status pages, their configuration, and the monitors they expose." },
|
||||
{ name: "settings", description: "Global application settings (admin/superadmin)." },
|
||||
{ name: "invite", description: "Team invitations and accepting them." },
|
||||
{ name: "queue", description: "Background job queue introspection and admin actions (admin/superadmin)." },
|
||||
{ name: "diagnostic", description: "System diagnostics for the running server (admin/superadmin)." },
|
||||
{ name: "logs", description: "Application logs for the running server (admin/superadmin)." },
|
||||
],
|
||||
});
|
||||
|
||||
cached = document as unknown as JsonObject;
|
||||
return cached;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { OpenAPIRegistry, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
|
||||
import { z } from "zod";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const registry = new OpenAPIRegistry();
|
||||
|
||||
registry.registerComponent("securitySchemes", "bearerAuth", {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import { z } from "zod";
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, multipart, okJson, okJsonNoData, standardErrors } from "../helpers.js";
|
||||
import {
|
||||
loginValidation,
|
||||
registrationBodyValidation,
|
||||
recoveryValidation,
|
||||
recoveryTokenBodyValidation,
|
||||
newPasswordValidation,
|
||||
} from "@/validation/authValidation.js";
|
||||
import {
|
||||
getUserByIdParamValidation,
|
||||
editUserByIdParamValidation,
|
||||
editUserByIdBodyValidation,
|
||||
editUserPasswordByIdBodyValidation,
|
||||
createUserBodyValidation,
|
||||
editUserBodyValidation,
|
||||
} from "@/validation/userValidation.js";
|
||||
|
||||
const tags = ["auth"];
|
||||
|
||||
const userObject = z
|
||||
.object({
|
||||
_id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
|
||||
firstName: z.string().openapi({ example: "Ada" }),
|
||||
lastName: z.string().openapi({ example: "Lovelace" }),
|
||||
email: z.string().openapi({ example: "ada@example.com" }),
|
||||
role: z.array(z.string()).openapi({ example: ["admin"] }),
|
||||
teamId: z.string().optional().openapi({ example: "65f1c2a4d8b9e01234567890" }),
|
||||
profileImage: z.string().nullable().optional(),
|
||||
createdAt: z.string().openapi({ example: "2026-04-01T10:00:00.000Z" }),
|
||||
updatedAt: z.string().openapi({ example: "2026-04-15T14:30:00.000Z" }),
|
||||
})
|
||||
.passthrough()
|
||||
.openapi("User");
|
||||
|
||||
const authPayload = z
|
||||
.object({
|
||||
user: userObject,
|
||||
token: z.string().optional().openapi({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }),
|
||||
})
|
||||
.openapi("AuthPayload");
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/auth/register",
|
||||
tags,
|
||||
summary: "Register a new user (first user becomes superadmin)",
|
||||
request: { body: { content: multipart(registrationBodyValidation.shape, "profileImage") } },
|
||||
responses: { "200": okJson(authPayload), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/auth/login",
|
||||
tags,
|
||||
summary: "Log in",
|
||||
request: {
|
||||
body: {
|
||||
content: json(loginValidation, { email: "ada@example.com", password: "S3cure!Passw0rd" }),
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": okJson(authPayload, "OK", {
|
||||
user: {
|
||||
_id: "65f1c2a4d8b9e0123456789a",
|
||||
firstName: "Ada",
|
||||
lastName: "Lovelace",
|
||||
email: "ada@example.com",
|
||||
role: ["admin"],
|
||||
teamId: "65f1c2a4d8b9e01234567890",
|
||||
createdAt: "2026-04-01T10:00:00.000Z",
|
||||
updatedAt: "2026-04-15T14:30:00.000Z",
|
||||
},
|
||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
}),
|
||||
"401": { description: "Invalid credentials", content: json(z.object({ success: z.literal(false), msg: z.string() })) },
|
||||
"500": standardErrors["500"],
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/auth/recovery/request",
|
||||
tags,
|
||||
summary: "Request a password recovery email",
|
||||
request: { body: { content: json(recoveryValidation, { email: "ada@example.com" }) } },
|
||||
responses: { "200": okJsonNoData(), "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/auth/recovery/validate",
|
||||
tags,
|
||||
summary: "Validate a password recovery token",
|
||||
request: { body: { content: json(recoveryTokenBodyValidation) } },
|
||||
responses: { "200": okJsonNoData(), "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/auth/recovery/reset",
|
||||
tags,
|
||||
summary: "Reset password using a recovery token",
|
||||
request: { body: { content: json(newPasswordValidation) } },
|
||||
responses: { "200": okJsonNoData(), "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/auth/users/superadmin",
|
||||
tags,
|
||||
summary: "Check whether a superadmin user exists",
|
||||
responses: { "200": okJson(z.object({ superAdminExists: z.boolean() })), "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/auth/users",
|
||||
tags,
|
||||
summary: "List all users (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okJson(z.array(userObject)), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/auth/users",
|
||||
tags,
|
||||
summary: "Create a new user (superadmin)",
|
||||
security: bearer,
|
||||
request: { body: { content: multipart(createUserBodyValidation.shape, "profileImage") } },
|
||||
responses: { "201": okJson(userObject, "User created"), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/auth/users/{userId}",
|
||||
tags,
|
||||
summary: "Get a user by id",
|
||||
security: bearer,
|
||||
request: { params: getUserByIdParamValidation },
|
||||
responses: {
|
||||
"200": okJson(userObject),
|
||||
"404": { description: "User not found", content: json(z.object({ success: z.literal(false), msg: z.string() })) },
|
||||
...standardErrors,
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/auth/users/{userId}",
|
||||
tags,
|
||||
summary: "Edit a user (superadmin)",
|
||||
security: bearer,
|
||||
request: {
|
||||
params: editUserByIdParamValidation,
|
||||
body: { content: json(editUserByIdBodyValidation) },
|
||||
},
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/auth/users/{userId}/password",
|
||||
tags,
|
||||
summary: "Change a user's password (superadmin)",
|
||||
security: bearer,
|
||||
request: {
|
||||
params: editUserByIdParamValidation,
|
||||
body: { content: json(editUserPasswordByIdBodyValidation) },
|
||||
},
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/auth/users/{userId}",
|
||||
tags,
|
||||
summary: "Delete a user (admin/superadmin)",
|
||||
security: bearer,
|
||||
request: { params: getUserByIdParamValidation },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/auth/user",
|
||||
tags,
|
||||
summary: "Edit the currently authenticated user",
|
||||
security: bearer,
|
||||
request: { body: { content: multipart(editUserBodyValidation.shape, "profileImage") } },
|
||||
responses: { "200": okJson(userObject), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/auth/user",
|
||||
tags,
|
||||
summary: "Delete the currently authenticated user",
|
||||
security: bearer,
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { z } from "zod";
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import {
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getTeamChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
getChecksSummaryByTeamIdQueryValidation,
|
||||
} from "@/validation/checkValidation.js";
|
||||
|
||||
const tags = ["checks"];
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/checks/team/summary",
|
||||
tags,
|
||||
summary: "Aggregate check summary for the caller's team",
|
||||
security: bearer,
|
||||
request: { query: getChecksSummaryByTeamIdQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/checks/team",
|
||||
tags,
|
||||
summary: "List checks across the team",
|
||||
security: bearer,
|
||||
request: { query: getTeamChecksQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/checks/team",
|
||||
tags,
|
||||
summary: "Delete all checks for the team (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okJson(z.object({ deletedCount: z.number() })), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/checks/{monitorId}",
|
||||
tags,
|
||||
summary: "List checks for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getChecksParamValidation, query: getChecksQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/checks/{monitorId}",
|
||||
tags,
|
||||
summary: "Delete checks for a monitor",
|
||||
security: bearer,
|
||||
request: { params: deleteChecksParamValidation },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okUnknown, standardErrors } from "../helpers.js";
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/diagnostic/system",
|
||||
tags: ["diagnostic"],
|
||||
summary: "Get system diagnostics (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/geo-checks/{monitorId}",
|
||||
tags: ["geo-checks"],
|
||||
summary: "Get geo check results for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getChecksParamValidation, query: getChecksQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { z } from "zod";
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okJson, okJsonNoData, standardErrors } from "../helpers.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||
|
||||
const tags = ["incidents"];
|
||||
|
||||
const incidentIdParam = z.object({ incidentId: z.string() });
|
||||
|
||||
const incidentObject = z
|
||||
.object({
|
||||
id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789c" }),
|
||||
monitorId: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
|
||||
teamId: z.string().openapi({ example: "65f1c2a4d8b9e01234567890" }),
|
||||
startTime: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
|
||||
endTime: z.string().nullable().openapi({ example: "2026-04-15T03:34:00.000Z" }),
|
||||
status: z.boolean().openapi({ example: true, description: "true = resolved, false = ongoing" }),
|
||||
message: z.string().nullable().optional().openapi({ example: "HTTP 503 from origin" }),
|
||||
statusCode: z.number().nullable().optional().openapi({ example: 503 }),
|
||||
resolutionType: z.enum(["automatic", "manual"]).nullable().openapi({ example: "automatic" }),
|
||||
resolvedBy: z.string().nullable().optional(),
|
||||
resolvedByEmail: z.string().nullable().optional(),
|
||||
comment: z.string().nullable().optional(),
|
||||
createdAt: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
|
||||
updatedAt: z.string().openapi({ example: "2026-04-15T03:34:00.000Z" }),
|
||||
})
|
||||
.passthrough()
|
||||
.openapi("Incident");
|
||||
|
||||
const incidentSummaryItem = z
|
||||
.object({
|
||||
id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789c" }),
|
||||
monitorId: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
|
||||
monitorName: z.string().nullable().openapi({ example: "Marketing site" }),
|
||||
status: z.boolean().openapi({ example: true }),
|
||||
startTime: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
|
||||
endTime: z.string().nullable().openapi({ example: "2026-04-15T03:34:00.000Z" }),
|
||||
resolutionType: z.enum(["automatic", "manual"]).nullable().openapi({ example: "automatic" }),
|
||||
message: z.string().nullable().openapi({ example: "HTTP 503 from origin" }),
|
||||
statusCode: z.number().nullable().openapi({ example: 503 }),
|
||||
createdAt: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
|
||||
})
|
||||
.openapi("IncidentSummaryItem");
|
||||
|
||||
const incidentSummary = z
|
||||
.object({
|
||||
total: z.number().openapi({ example: 12 }),
|
||||
totalActive: z.number().openapi({ example: 1 }),
|
||||
totalManualResolutions: z.number().openapi({ example: 2 }),
|
||||
totalAutomaticResolutions: z.number().openapi({ example: 9 }),
|
||||
avgResolutionTimeHours: z.number().openapi({ example: 0.5 }),
|
||||
topMonitor: z
|
||||
.object({
|
||||
monitorId: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
|
||||
monitorName: z.string().nullable().openapi({ example: "Marketing site" }),
|
||||
incidentCount: z.number().openapi({ example: 4 }),
|
||||
})
|
||||
.nullable(),
|
||||
latestIncidents: z.array(incidentSummaryItem),
|
||||
})
|
||||
.openapi("IncidentSummary");
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/incidents/team",
|
||||
tags,
|
||||
summary: "List incidents for the caller's team",
|
||||
security: bearer,
|
||||
request: { query: getIncidentsByTeamQueryValidation },
|
||||
responses: { "200": okJson(z.array(incidentObject)), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/incidents/team/summary",
|
||||
tags,
|
||||
summary: "Incident summary for the caller's team",
|
||||
security: bearer,
|
||||
request: { query: getIncidentSummaryQueryValidation },
|
||||
responses: { "200": okJson(incidentSummary), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/incidents/{incidentId}",
|
||||
tags,
|
||||
summary: "Get an incident by id",
|
||||
security: bearer,
|
||||
request: { params: incidentIdParam },
|
||||
responses: {
|
||||
"200": okJson(z.object({ incident: incidentObject, monitor: z.unknown(), user: z.unknown().nullable() })),
|
||||
...standardErrors,
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/incidents/{incidentId}/resolve",
|
||||
tags,
|
||||
summary: "Manually resolve an incident (admin/superadmin)",
|
||||
security: bearer,
|
||||
request: { params: incidentIdParam },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJson, okJsonNoData, standardErrors } from "../helpers.js";
|
||||
import { z } from "zod";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
||||
|
||||
const tags = ["invite"];
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/invite/send",
|
||||
tags,
|
||||
summary: "Send an invite email (admin/superadmin)",
|
||||
security: bearer,
|
||||
request: { body: { content: json(inviteBodyValidation) } },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/invite/verify",
|
||||
tags,
|
||||
summary: "Verify an invite token",
|
||||
request: { body: { content: json(inviteVerificationBodyValidation) } },
|
||||
responses: { "200": okJson(z.object({ email: z.string(), role: z.array(z.string()).optional() }).passthrough()), "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/invite",
|
||||
tags,
|
||||
summary: "Create an invite token (admin/superadmin)",
|
||||
security: bearer,
|
||||
request: { body: { content: json(inviteBodyValidation) } },
|
||||
responses: { "200": okJson(z.object({ token: z.string() }).passthrough()), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okUnknown, standardErrors } from "../helpers.js";
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/logs",
|
||||
tags: ["logs"],
|
||||
summary: "Get application logs (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import {
|
||||
createMaintenanceWindowBodyValidation,
|
||||
getMaintenanceWindowByIdParamValidation,
|
||||
getMaintenanceWindowsByTeamIdQueryValidation,
|
||||
getMaintenanceWindowsByMonitorIdParamValidation,
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
editMaintenanceWindowByIdParamValidation,
|
||||
editMaintenanceByIdWindowBodyValidation,
|
||||
} from "@/validation/maintenanceWindowValidation.js";
|
||||
|
||||
const tags = ["maintenance-window"];
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/maintenance-window",
|
||||
tags,
|
||||
summary: "Create one or more maintenance windows",
|
||||
security: bearer,
|
||||
request: { body: { content: json(createMaintenanceWindowBodyValidation) } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/maintenance-window/team",
|
||||
tags,
|
||||
summary: "List maintenance windows for the caller's team",
|
||||
security: bearer,
|
||||
request: { query: getMaintenanceWindowsByTeamIdQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/maintenance-window/monitor/{monitorId}",
|
||||
tags,
|
||||
summary: "List maintenance windows for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getMaintenanceWindowsByMonitorIdParamValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/maintenance-window/{id}",
|
||||
tags,
|
||||
summary: "Get a maintenance window by id",
|
||||
security: bearer,
|
||||
request: { params: getMaintenanceWindowByIdParamValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/maintenance-window/{id}",
|
||||
tags,
|
||||
summary: "Edit a maintenance window",
|
||||
security: bearer,
|
||||
request: { params: editMaintenanceWindowByIdParamValidation, body: { content: json(editMaintenanceByIdWindowBodyValidation) } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/maintenance-window/{id}",
|
||||
tags,
|
||||
summary: "Delete a maintenance window",
|
||||
security: bearer,
|
||||
request: { params: deleteMaintenanceWindowByIdParamValidation },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
import { z } from "zod";
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
|
||||
import {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorsWithChecksQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
pauseMonitorParamValidation,
|
||||
getUptimeDetailsByIdParamValidation,
|
||||
getUptimeDetailsByIdQueryValidation,
|
||||
importMonitorsBodyValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
} from "@/validation/monitorValidation.js";
|
||||
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
||||
|
||||
const tags = ["monitors"];
|
||||
|
||||
const monitorObject = z
|
||||
.object({
|
||||
_id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
|
||||
name: z.string().openapi({ example: "Marketing site" }),
|
||||
description: z.string().optional().openapi({ example: "Production marketing site monitored from the EU region" }),
|
||||
type: z.enum(MonitorTypes).openapi({ example: "http" }),
|
||||
url: z.string().openapi({ example: "https://www.example.com" }),
|
||||
port: z.number().optional().openapi({ example: 443 }),
|
||||
isActive: z.boolean().openapi({ example: true }),
|
||||
interval: z.number().openapi({ example: 60000 }),
|
||||
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).openapi({ example: "up" }),
|
||||
statusWindowSize: z.number().openapi({ example: 5 }),
|
||||
statusWindowThreshold: z.number().openapi({ example: 3 }),
|
||||
ignoreTlsErrors: z.boolean().openapi({ example: false }),
|
||||
useAdvancedMatching: z.boolean().openapi({ example: false }),
|
||||
jsonPath: z.string().optional(),
|
||||
expectedValue: z.string().optional(),
|
||||
matchMethod: z.enum(MonitorMatchMethods).optional(),
|
||||
notifications: z.array(z.string()).openapi({ example: ["65f1c2a4d8b9e0123456789b"] }),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().openapi({ example: 90 }),
|
||||
memoryAlertThreshold: z.number().openapi({ example: 90 }),
|
||||
diskAlertThreshold: z.number().openapi({ example: 90 }),
|
||||
tempAlertThreshold: z.number().openapi({ example: 80 }),
|
||||
selectedDisks: z.array(z.string()).openapi({ example: [] }),
|
||||
gameId: z.string().optional(),
|
||||
grpcServiceName: z.string().optional(),
|
||||
group: z.string().nullable().optional(),
|
||||
geoCheckEnabled: z.boolean().openapi({ example: false }),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).openapi({ example: [] }),
|
||||
geoCheckInterval: z.number().openapi({ example: 300000 }),
|
||||
teamId: z.string().openapi({ example: "65f1c2a4d8b9e01234567890" }),
|
||||
userId: z.string().openapi({ example: "65f1c2a4d8b9e01234567891" }),
|
||||
createdAt: z.string().openapi({ example: "2026-04-01T10:00:00.000Z" }),
|
||||
updatedAt: z.string().openapi({ example: "2026-04-15T14:30:00.000Z" }),
|
||||
})
|
||||
.passthrough()
|
||||
.openapi("Monitor");
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/team",
|
||||
tags,
|
||||
summary: "List monitors for the caller's team",
|
||||
security: bearer,
|
||||
request: { query: getMonitorsByTeamIdQueryValidation },
|
||||
responses: { "200": okJson(z.array(monitorObject)), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/team/with-checks",
|
||||
tags,
|
||||
summary: "List team monitors with their most recent checks (paginated)",
|
||||
security: bearer,
|
||||
request: { query: getMonitorsWithChecksQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/team/groups",
|
||||
tags,
|
||||
summary: "List monitor groups for the caller's team",
|
||||
security: bearer,
|
||||
responses: { "200": okJson(z.array(z.string())), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/uptime/details/{monitorId}",
|
||||
tags,
|
||||
summary: "Get uptime details for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getUptimeDetailsByIdParamValidation, query: getUptimeDetailsByIdQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/hardware/details/{monitorId}",
|
||||
tags,
|
||||
summary: "Get hardware metrics detail for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getHardwareDetailsByIdParamValidation, query: getHardwareDetailsByIdQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/pagespeed/details/{monitorId}",
|
||||
tags,
|
||||
summary: "Get PageSpeed detail for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getHardwareDetailsByIdParamValidation, query: getHardwareDetailsByIdQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/{monitorId}/geo-checks",
|
||||
tags,
|
||||
summary: "Get geo check results for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getMonitorByIdParamValidation, query: getMonitorByIdQueryValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/monitors/pause/{monitorId}",
|
||||
tags,
|
||||
summary: "Toggle pause state for a monitor",
|
||||
security: bearer,
|
||||
request: { params: pauseMonitorParamValidation },
|
||||
responses: { "200": okJson(monitorObject), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/certificate/{monitorId}",
|
||||
tags,
|
||||
summary: "Get SSL certificate info for a monitor",
|
||||
security: bearer,
|
||||
request: { params: getCertificateParamValidation },
|
||||
responses: { "200": okJson(z.object({ certificateDate: z.string() })), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/monitors/notifications",
|
||||
tags,
|
||||
summary: "Bulk update notifications across monitors",
|
||||
security: bearer,
|
||||
request: { body: { content: json(updateNotificationsValidation) } },
|
||||
responses: { "200": okJson(z.object({ modifiedCount: z.number() })), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/monitors",
|
||||
tags,
|
||||
summary: "Create a new monitor",
|
||||
security: bearer,
|
||||
request: { body: { content: json(createMonitorBodyValidation) } },
|
||||
responses: { "200": okJson(monitorObject, "Monitor created"), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/monitors",
|
||||
tags,
|
||||
summary: "Delete every monitor (superadmin only)",
|
||||
security: bearer,
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/monitors/demo",
|
||||
tags,
|
||||
summary: "Insert preconfigured demo monitors",
|
||||
security: bearer,
|
||||
responses: { "200": okJson(z.number()), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/export/json",
|
||||
tags,
|
||||
summary: "Export all team monitors as JSON",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/monitors/import/json",
|
||||
tags,
|
||||
summary: "Import monitors from JSON",
|
||||
security: bearer,
|
||||
request: { body: { content: json(importMonitorsBodyValidation) } },
|
||||
responses: { "200": okJson(z.object({ imported: z.number() }).passthrough()), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/games",
|
||||
tags,
|
||||
summary: "List supported game-server types",
|
||||
security: bearer,
|
||||
responses: { "200": okJson(z.array(z.string())), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/{monitorId}",
|
||||
tags,
|
||||
summary: "Get a monitor by id",
|
||||
security: bearer,
|
||||
request: { params: getMonitorByIdParamValidation },
|
||||
responses: { "200": okJson(monitorObject), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/monitors/{monitorId}",
|
||||
tags,
|
||||
summary: "Edit a monitor",
|
||||
security: bearer,
|
||||
request: { params: getMonitorByIdParamValidation, body: { content: json(editMonitorBodyValidation) } },
|
||||
responses: { "200": okJson(monitorObject), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/monitors/{monitorId}",
|
||||
tags,
|
||||
summary: "Delete a monitor",
|
||||
security: bearer,
|
||||
request: { params: getMonitorByIdParamValidation },
|
||||
responses: { "200": okJson(monitorObject), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { z } from "zod";
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import {
|
||||
createNotificationBodyValidation,
|
||||
deleteNotificationParamValidation,
|
||||
getNotificationByIdParamValidation,
|
||||
editNotificationParamValidation,
|
||||
testAllNotificationsBodyValidation,
|
||||
} from "@/validation/notificationValidation.js";
|
||||
|
||||
const tags = ["notifications"];
|
||||
|
||||
// OpenAPI metadata for each notification variant. Keyed by the discriminator
|
||||
// value in createNotificationBodyValidation; build fails loudly if a variant
|
||||
// in the validator has no entry here, which doubles as drift detection.
|
||||
const notificationVariantMeta: Record<string, { component: string; example: Record<string, unknown> }> = {
|
||||
email: {
|
||||
component: "EmailNotification",
|
||||
example: { notificationName: "Ops on-call email", type: "email", address: "alerts@example.com" },
|
||||
},
|
||||
webhook: {
|
||||
component: "WebhookNotification",
|
||||
example: { notificationName: "Custom webhook", type: "webhook", address: "https://example.com/hooks/checkmate" },
|
||||
},
|
||||
slack: {
|
||||
component: "SlackNotification",
|
||||
example: { notificationName: "#alerts", type: "slack", address: "https://hooks.slack.com/services/T000/B000/XXXX" },
|
||||
},
|
||||
discord: {
|
||||
component: "DiscordNotification",
|
||||
example: { notificationName: "#status", type: "discord", address: "https://discord.com/api/webhooks/123/abc" },
|
||||
},
|
||||
pager_duty: {
|
||||
component: "PagerDutyNotification",
|
||||
example: { notificationName: "PagerDuty primary", type: "pager_duty", address: "R01XXXXXXXXXXXXXXXXXXXXXXX" },
|
||||
},
|
||||
matrix: {
|
||||
component: "MatrixNotification",
|
||||
example: {
|
||||
notificationName: "Matrix room",
|
||||
type: "matrix",
|
||||
homeserverUrl: "https://matrix.example.com",
|
||||
roomId: "!abc123:example.com",
|
||||
accessToken: "syt_xxx",
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
component: "TeamsNotification",
|
||||
example: { notificationName: "Teams ops channel", type: "teams", address: "https://outlook.office.com/webhook/..." },
|
||||
},
|
||||
telegram: {
|
||||
component: "TelegramNotification",
|
||||
example: { notificationName: "Telegram bot", type: "telegram", address: "-1001234567890", accessToken: "123456:ABC-DEF" },
|
||||
},
|
||||
pushover: {
|
||||
component: "PushoverNotification",
|
||||
example: { notificationName: "Pushover personal", type: "pushover", address: "u1234567890abcdef", accessToken: "a1234567890abcdef" },
|
||||
},
|
||||
twilio: {
|
||||
component: "TwilioNotification",
|
||||
example: {
|
||||
notificationName: "Twilio SMS",
|
||||
type: "twilio",
|
||||
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
accessToken: "your-auth-token",
|
||||
phone: "+15551234567",
|
||||
twilioPhoneNumber: "+15557654321",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const decoratedVariants = createNotificationBodyValidation.options.map((variant) => {
|
||||
const typeField = variant.shape.type as z.ZodLiteral<string>;
|
||||
const type = typeField.value;
|
||||
const meta = notificationVariantMeta[type];
|
||||
if (!meta) {
|
||||
throw new Error(`Missing OpenAPI metadata for notification variant "${type}". Add an entry in notificationVariantMeta.`);
|
||||
}
|
||||
return variant.openapi(meta.component, { example: meta.example });
|
||||
});
|
||||
|
||||
const notificationBody = z
|
||||
.discriminatedUnion("type", decoratedVariants as typeof createNotificationBodyValidation.options)
|
||||
.openapi("NotificationChannelBody");
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/notifications",
|
||||
tags,
|
||||
summary: "Create a notification channel",
|
||||
description: "Create a notification channel of any supported type. The `type` field discriminates the body shape.",
|
||||
security: bearer,
|
||||
request: { body: { content: json(notificationBody) } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/notifications/test/all",
|
||||
tags,
|
||||
summary: "Send a test alert through every notification channel for the team",
|
||||
security: bearer,
|
||||
request: { body: { content: json(testAllNotificationsBodyValidation) } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/notifications/test",
|
||||
tags,
|
||||
summary: "Send a test alert through a single notification channel",
|
||||
security: bearer,
|
||||
request: { body: { content: json(notificationBody) } },
|
||||
responses: { "200": okJson(z.object({ success: z.boolean() }).passthrough()), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/notifications/team",
|
||||
tags,
|
||||
summary: "List notification channels for the caller's team",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/notifications/{id}",
|
||||
tags,
|
||||
summary: "Get a notification channel by id",
|
||||
security: bearer,
|
||||
request: { params: getNotificationByIdParamValidation },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/notifications/{id}",
|
||||
tags,
|
||||
summary: "Delete a notification channel",
|
||||
security: bearer,
|
||||
request: { params: deleteNotificationParamValidation },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/notifications/{id}",
|
||||
tags,
|
||||
summary: "Edit a notification channel",
|
||||
security: bearer,
|
||||
request: { params: editNotificationParamValidation, body: { content: json(notificationBody) } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
|
||||
const tags = ["queue"];
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/queue/jobs",
|
||||
tags,
|
||||
summary: "List queued monitor jobs (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/queue/metrics",
|
||||
tags,
|
||||
summary: "Get queue runtime metrics (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/queue/all-metrics",
|
||||
tags,
|
||||
summary: "Get queue metrics across all monitors (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/queue/flush",
|
||||
tags,
|
||||
summary: "Flush the monitor job queue (admin/superadmin)",
|
||||
security: bearer,
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
||||
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
||||
|
||||
const tags = ["settings"];
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/settings",
|
||||
tags,
|
||||
summary: "Get application settings",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/settings",
|
||||
tags,
|
||||
summary: "Update application settings (admin/superadmin)",
|
||||
security: bearer,
|
||||
request: { body: { content: json(updateAppSettingsBodyValidation) } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/settings/test-email",
|
||||
tags,
|
||||
summary: "Send a test email using current SMTP settings (admin/superadmin)",
|
||||
security: bearer,
|
||||
request: { body: { content: json(sendTestEmailBodyValidation) } },
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, multipart, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation } from "@/validation/statusPageValidation.js";
|
||||
|
||||
const tags = ["status-page"];
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/status-page/team",
|
||||
tags,
|
||||
summary: "List status pages for the caller's team",
|
||||
security: bearer,
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/status-page",
|
||||
tags,
|
||||
summary: "Create a status page (logo upload optional)",
|
||||
security: bearer,
|
||||
request: { body: { content: multipart(createStatusPageBodyValidation.shape, "logo") } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/status-page/{id}",
|
||||
tags,
|
||||
summary: "Update a status page (logo upload optional)",
|
||||
security: bearer,
|
||||
request: { body: { content: multipart(createStatusPageBodyValidation.shape, "logo") } },
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/status-page/{url}",
|
||||
tags,
|
||||
summary: "Get a public status page by its URL slug",
|
||||
request: { params: getStatusPageParamValidation, query: getStatusPageQueryValidation },
|
||||
responses: { "200": okUnknown, "404": { description: "Status page not found" }, "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/status-page/{id}",
|
||||
tags,
|
||||
summary: "Delete a status page",
|
||||
security: bearer,
|
||||
responses: { "200": okJsonNoData(), ...standardErrors },
|
||||
});
|
||||
Generated
+41
@@ -46,6 +46,7 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.5.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
@@ -126,6 +127,19 @@
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@asteasolutions/zod-to-openapi": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz",
|
||||
"integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"openapi3-ts": "^4.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/sha256-browser": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
||||
@@ -851,6 +865,7 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -1470,6 +1485,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1514,6 +1530,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -4227,6 +4244,7 @@
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -4387,6 +4405,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -4624,6 +4643,7 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -5237,6 +5257,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5815,6 +5836,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -6708,6 +6730,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
||||
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.9",
|
||||
"lilconfig": "^3.1.3"
|
||||
@@ -7472,6 +7495,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7833,6 +7857,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -9501,6 +9526,7 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@@ -11944,6 +11970,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi3-ts": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz",
|
||||
"integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yaml": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -12335,6 +12371,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -14357,6 +14394,7 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -14525,6 +14563,7 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -14688,6 +14727,7 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -15522,6 +15562,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+4
-1
@@ -11,7 +11,9 @@
|
||||
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --no-coverage test/integration",
|
||||
"dev": "nodemon --exec tsx src/index.js",
|
||||
"start": "node --watch ./dist/index.js",
|
||||
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
|
||||
"clean": "rm -rf dist",
|
||||
"build": "npm run clean && npm run build:openapi && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/templates dist/templates",
|
||||
"build:openapi": "tsc --noEmit && tsx scripts/buildOpenApi.ts",
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint --fix .",
|
||||
"format": "prettier --write .",
|
||||
@@ -64,6 +66,7 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.5.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getOpenApiSpec } from "../openapi/index.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const outPath = path.resolve(__dirname, "..", "openapi.json");
|
||||
|
||||
writeFileSync(outPath, JSON.stringify(getOpenApiSpec(), null, 2) + "\n");
|
||||
console.log(`OpenAPI spec written to ${outPath}`);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"paths": {
|
||||
@@ -15,8 +14,9 @@
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "openapi", "scripts"],
|
||||
"exclude": ["tests", "dist", "node_modules"]
|
||||
}
|
||||
Reference in new issue
Block a user