diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 98ac615..cdcb8ef 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -26,6 +26,8 @@ "@fastify/helmet": "^13.0.2", "@fastify/rate-limit": "^10.3.0", "@fastify/static": "^9.0.0", + "@fastify/swagger": "^9.8.0", + "@fastify/swagger-ui": "^6.1.0", "@fluxcore/config": "workspace:*", "@fluxcore/database": "workspace:*", "@fluxcore/i18n": "workspace:*", diff --git a/apps/dashboard/src/server/features/actions/routes.ts b/apps/dashboard/src/server/features/actions/routes.ts index 28592b1..4d71123 100644 --- a/apps/dashboard/src/server/features/actions/routes.ts +++ b/apps/dashboard/src/server/features/actions/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { createRule, @@ -115,11 +116,68 @@ function validateRuleBody( return null; } +const ruleRequestBodySchema = { + type: "object", + properties: { + name: { type: "string", maxLength: 50 }, + eventType: { type: "string" }, + actions: { + type: "array", + items: { + type: "object", + properties: { + type: { type: "string" }, + webhook: { + type: "object", + properties: { url: { type: "string" } }, + }, + }, + additionalProperties: true, + }, + }, + steps: { + type: "array", + maxItems: 10, + items: { + type: "object", + properties: { + id: { type: "string" }, + type: { type: "string" }, + }, + additionalProperties: true, + }, + }, + entryStepId: { type: "string" }, + conditions: { type: "object", additionalProperties: true }, + priority: { type: "integer" }, + enabled: { type: "boolean" }, + }, + additionalProperties: true, +} as const; + export function registerActionRoutes(app: FastifyInstance): void { // --- Constants (for frontend dropdowns) --- app.get( "/api/actions/constants", - { preHandler: [requireAuth] }, + { + preHandler: [requireAuth], + schema: withDocs(undefined, { + tag: "Actions", + response: { + 200: { + type: "object", + properties: { + eventTypes: { type: "object", additionalProperties: true }, + actionTypes: { type: "object", additionalProperties: true }, + maxActionsPerRule: { type: "integer" }, + actionTypeFields: { type: "object", additionalProperties: true }, + eventTypeVariables: { type: "object", additionalProperties: true }, + templateVariables: { type: "object", additionalProperties: true }, + }, + }, + }, + }), + }, async (_request, reply) => { reply.send({ eventTypes: EVENT_TYPES, @@ -135,7 +193,13 @@ export function registerActionRoutes(app: FastifyInstance): void { // --- Rules CRUD --- app.get( "/api/guilds/:guildId/actions/rules", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { + tag: "Actions", + response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } }, + }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const [rules, lastFiredMap] = await Promise.all([ @@ -152,7 +216,19 @@ export function registerActionRoutes(app: FastifyInstance): void { app.post( "/api/guilds/:guildId/actions/rules", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: ruleRequestBodySchema, + }, + { + tag: "Actions", + response: { 201: { type: "object", additionalProperties: true } }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const body = request.body as RuleRequestBody; @@ -194,7 +270,19 @@ export function registerActionRoutes(app: FastifyInstance): void { app.put( "/api/guilds/:guildId/actions/rules/:ruleId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: ruleRequestBodySchema, + }, + { + tag: "Actions", + response: { 200: { type: "object", additionalProperties: true } }, + }, + ), + }, async (request, reply) => { const { guildId, ruleId } = request.params as { guildId: string; @@ -246,7 +334,13 @@ export function registerActionRoutes(app: FastifyInstance): void { app.delete( "/api/guilds/:guildId/actions/rules/:ruleId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { + tag: "Actions", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }), + }, async (request, reply) => { const { guildId, ruleId } = request.params as { guildId: string; @@ -263,7 +357,38 @@ export function registerActionRoutes(app: FastifyInstance): void { // --- Bulk Operations --- app.patch( "/api/guilds/:guildId/actions/rules/bulk", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.execute")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.execute")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["ruleIds", "action"], + properties: { + ruleIds: { + type: "array", + maxItems: 50, + items: { type: "integer" }, + }, + action: { type: "string", enum: ["enable", "disable", "delete"] }, + }, + }, + }, + { + tag: "Actions", + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + count: { type: "integer" }, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const body = request.body as { @@ -307,7 +432,22 @@ export function registerActionRoutes(app: FastifyInstance): void { // --- Per-Rule Analytics --- app.get( "/api/guilds/:guildId/actions/rules/:ruleId/analytics", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + querystring: { + type: "object", + properties: { days: { type: "string" } }, + }, + }, + { + tag: "Actions", + response: { 200: { type: "object", additionalProperties: true } }, + }, + ), + }, async (request, reply) => { const { guildId, ruleId } = request.params as { guildId: string; @@ -323,7 +463,22 @@ export function registerActionRoutes(app: FastifyInstance): void { // --- Settings --- app.get( "/api/guilds/:guildId/actions/settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { + tag: "Actions", + response: { + 200: { + type: "object", + properties: { + maxRules: { type: "integer" }, + globalEnabled: { type: "boolean" }, + logChannelId: { type: ["string", "null"] }, + }, + }, + }, + }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; reply.send(getGuildSettingsOrDefault(guildId)); @@ -332,7 +487,26 @@ export function registerActionRoutes(app: FastifyInstance): void { app.put( "/api/guilds/:guildId/actions/settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + maxRules: { type: "integer" }, + globalEnabled: { type: "boolean" }, + logChannelId: { type: ["string", "null"] }, + }, + }, + }, + { + tag: "Actions", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const body = request.body as { @@ -373,7 +547,22 @@ export function registerActionRoutes(app: FastifyInstance): void { // --- Analytics --- app.get( "/api/guilds/:guildId/actions/analytics", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + querystring: { + type: "object", + properties: { days: { type: "string" } }, + }, + }, + { + tag: "Actions", + response: { 200: { type: "object", additionalProperties: true } }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { days?: string }; @@ -386,7 +575,25 @@ export function registerActionRoutes(app: FastifyInstance): void { // --- Logs --- app.get( "/api/guilds/:guildId/actions/logs", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + querystring: { + type: "object", + properties: { + ruleName: { type: "string" }, + limit: { type: "string" }, + }, + }, + }, + { + tag: "Actions", + response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { ruleName?: string; limit?: string }; diff --git a/apps/dashboard/src/server/features/auth/routes.ts b/apps/dashboard/src/server/features/auth/routes.ts index d12dc34..6995b20 100644 --- a/apps/dashboard/src/server/features/auth/routes.ts +++ b/apps/dashboard/src/server/features/auth/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { config } from "@fluxcore/config"; import { buildCallbackUrl, @@ -20,7 +21,7 @@ const authRateLimit = { }; export function registerAuthRoutes(app: FastifyInstance): void { - app.get("/auth/login", { ...authRateLimit }, async (_request, reply) => { + app.get("/auth/login", { ...authRateLimit, schema: withDocs(undefined, { tag: "Auth", secure: false }) }, async (_request, reply) => { const callbackUrl = buildCallbackUrl(config.dashboardPublicUrl); const { url, state } = getAuthorizationUrl(callbackUrl); reply @@ -35,7 +36,7 @@ export function registerAuthRoutes(app: FastifyInstance): void { .redirect(url); }); - app.get("/auth/callback", { ...authRateLimit }, async (request, reply) => { + app.get("/auth/callback", { ...authRateLimit, schema: withDocs(undefined, { tag: "Auth", secure: false }) }, async (request, reply) => { const { code, state } = request.query as { code?: string; state?: string; @@ -100,7 +101,7 @@ export function registerAuthRoutes(app: FastifyInstance): void { } }); - app.get("/auth/csrf", async (_request, reply) => { + app.get("/auth/csrf", { schema: withDocs(undefined, { tag: "Auth", secure: false, response: { 200: { type: "object", properties: { token: { type: "string" } } } } }) }, async (_request, reply) => { const token = generateCsrfToken(); reply .setCookie("csrf_token", token, { @@ -113,7 +114,7 @@ export function registerAuthRoutes(app: FastifyInstance): void { .send({ token }); }); - app.get("/auth/logout", async (request, reply) => { + app.get("/auth/logout", { schema: withDocs(undefined, { tag: "Auth", secure: false }) }, async (request, reply) => { const sessionCookie = request.cookies?.session; if (sessionCookie) { const unsigned = request.unsignCookie(sessionCookie); @@ -124,7 +125,7 @@ export function registerAuthRoutes(app: FastifyInstance): void { reply.clearCookie("session", { path: "/" }).redirect("/"); }); - app.get("/auth/me", async (request, reply) => { + app.get("/auth/me", { schema: withDocs(undefined, { tag: "Auth", secure: false, response: { 200: { type: "object", properties: { userId: { type: "string" }, username: { type: "string" }, avatar: { type: ["string", "null"] } } } } }) }, async (request, reply) => { const sessionCookie = request.cookies?.session; if (!sessionCookie) { reply.code(401).send({ error: "Not authenticated" }); diff --git a/apps/dashboard/src/server/features/commands/routes.ts b/apps/dashboard/src/server/features/commands/routes.ts index c7e1876..8eb52a6 100644 --- a/apps/dashboard/src/server/features/commands/routes.ts +++ b/apps/dashboard/src/server/features/commands/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import safeRegex from "safe-regex"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { @@ -37,7 +38,10 @@ export function registerCustomCommandRoutes(app: FastifyInstance): void { // GET list custom commands app.get( "/api/guilds/:guildId/custom-commands", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("commands.list.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("commands.list.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "CustomCommands", response: { 200: { type: "array", items: {} } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const commands = await getCustomCommands(guildId); @@ -58,7 +62,8 @@ export function registerCustomCommandRoutes(app: FastifyInstance): void { (req as { session?: { userId?: string } }).session?.userId ?? req.ip, }, }, - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", required: ["name", "triggerType"], @@ -107,7 +112,7 @@ export function registerCustomCommandRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "CustomCommands", response: { 201: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -170,7 +175,8 @@ export function registerCustomCommandRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/custom-commands/:id", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("commands.list.manage")], - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", properties: { @@ -218,7 +224,7 @@ export function registerCustomCommandRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "CustomCommands", response: { 200: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; @@ -263,7 +269,10 @@ export function registerCustomCommandRoutes(app: FastifyInstance): void { // DELETE custom command app.delete( "/api/guilds/:guildId/custom-commands/:id", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("commands.list.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("commands.list.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "CustomCommands", response: { 200: { type: "object", properties: { success: { type: "boolean" } } } } }), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const commandId = parseIntParam(id); diff --git a/apps/dashboard/src/server/features/discord/routes.ts b/apps/dashboard/src/server/features/discord/routes.ts index 9c32296..c4632b5 100644 --- a/apps/dashboard/src/server/features/discord/routes.ts +++ b/apps/dashboard/src/server/features/discord/routes.ts @@ -7,6 +7,7 @@ import { } from "../../shared/discordApi.js"; import { forceRefreshSessionGuilds } from "../../shared/session.js"; import { logger } from "@fluxcore/utils"; +import { withDocs } from "../../shared/openapi-schemas.js"; // Discord channel type constants const GuildText = 0; @@ -16,7 +17,28 @@ const GuildCategory = 4; export function registerDiscordRoutes(app: FastifyInstance): void { app.get( "/api/guilds/:guildId/channels", - { preHandler: [requireAuth, requireGuildAdmin] }, + { + preHandler: [requireAuth, requireGuildAdmin], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Discord", + response: { + 200: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + type: { type: "integer" }, + }, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; try { @@ -49,7 +71,28 @@ export function registerDiscordRoutes(app: FastifyInstance): void { app.get( "/api/guilds/:guildId/roles", - { preHandler: [requireAuth, requireGuildAdmin] }, + { + preHandler: [requireAuth, requireGuildAdmin], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Discord", + response: { + 200: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + color: { type: "string" }, + }, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; try { @@ -80,6 +123,41 @@ export function registerDiscordRoutes(app: FastifyInstance): void { { preHandler: [requireAuth, requireGuildAdmin], config: { rateLimit: { max: 3, timeWindow: "1 minute" } }, + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Discord", + response: { + 200: { + type: "object", + properties: { + channels: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + type: { type: "integer" }, + }, + }, + }, + roles: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + color: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/giveaways/routes.ts b/apps/dashboard/src/server/features/giveaways/routes.ts index ddb5831..5579f6b 100644 --- a/apps/dashboard/src/server/features/giveaways/routes.ts +++ b/apps/dashboard/src/server/features/giveaways/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { createGiveaway, @@ -24,7 +25,10 @@ export function registerGiveawayRoutes(app: FastifyInstance): void { // GET list giveaways app.get( "/api/guilds/:guildId/giveaways", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("giveaways.list.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("giveaways.list.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, { tag: "Giveaways", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { active?: string; page?: string; limit?: string }; @@ -58,7 +62,8 @@ export function registerGiveawayRoutes(app: FastifyInstance): void { (req as { session?: { userId?: string } }).session?.userId ?? req.ip, }, }, - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", required: ["channelId", "prize", "winners", "durationMs"], @@ -74,7 +79,7 @@ export function registerGiveawayRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "Giveaways", response: { 201: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -117,7 +122,10 @@ export function registerGiveawayRoutes(app: FastifyInstance): void { // PUT end giveaway early app.put( "/api/guilds/:guildId/giveaways/:id/end", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("giveaways.list.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("giveaways.list.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Giveaways", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const giveawayId = parseIntParam(id); @@ -146,7 +154,10 @@ export function registerGiveawayRoutes(app: FastifyInstance): void { // POST reroll giveaway app.post( "/api/guilds/:guildId/giveaways/:id/reroll", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("giveaways.list.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("giveaways.list.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Giveaways", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const giveawayId = parseIntParam(id); diff --git a/apps/dashboard/src/server/features/guilds/routes.ts b/apps/dashboard/src/server/features/guilds/routes.ts index 7043953..5740bd0 100644 --- a/apps/dashboard/src/server/features/guilds/routes.ts +++ b/apps/dashboard/src/server/features/guilds/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth } from "../../shared/middleware.js"; import { isBotInGuild } from "../../shared/discordApi.js"; import { canManageGuild } from "../../shared/guildPermissions.js"; @@ -33,7 +34,25 @@ async function buildManageableGuilds(guilds: OAuthGuild[]) { export function registerGuildRoutes(app: FastifyInstance): void { app.get( "/api/guilds", - { preHandler: [requireAuth] }, + { + preHandler: [requireAuth], + schema: withDocs(undefined, { + tag: "Guilds", + response: { + 200: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + icon: { type: ["string", "null"] }, + }, + }, + }, + }, + }), + }, async (request, reply) => { const session = request.session!; reply.send(await buildManageableGuilds(session.guilds)); @@ -48,6 +67,22 @@ export function registerGuildRoutes(app: FastifyInstance): void { { preHandler: [requireAuth], config: { rateLimit: { max: 20, timeWindow: "1 minute" } }, + schema: withDocs(undefined, { + tag: "Guilds", + response: { + 200: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string" }, + icon: { type: ["string", "null"] }, + }, + }, + }, + }, + }), }, async (request, reply) => { const guilds = await forceRefreshSessionGuilds(request.sessionId!); diff --git a/apps/dashboard/src/server/features/leveling/routes.ts b/apps/dashboard/src/server/features/leveling/routes.ts index a4d0cb9..73260b5 100644 --- a/apps/dashboard/src/server/features/leveling/routes.ts +++ b/apps/dashboard/src/server/features/leveling/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getLevelSettings, @@ -24,7 +25,13 @@ export function registerLevelingRoutes(app: FastifyInstance): void { // GET leaderboard app.get( "/api/guilds/:guildId/leaderboard", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.leaderboard.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.leaderboard.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, + { tag: "Leveling", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { page?: string; limit?: string }; @@ -43,7 +50,10 @@ export function registerLevelingRoutes(app: FastifyInstance): void { // GET user level info app.get( "/api/guilds/:guildId/levels/:userId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.leaderboard.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.leaderboard.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Leveling", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId, userId } = request.params as { guildId: string; userId: string }; const userLevel = await getUserLevel(guildId, userId); @@ -71,16 +81,20 @@ export function registerLevelingRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/levels/:userId", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.users.manage")], - schema: { - body: { - type: "object", - required: ["xp"], - properties: { - xp: { type: "integer", minimum: 0 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["xp"], + properties: { + xp: { type: "integer", minimum: 0 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Leveling", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, userId } = request.params as { guildId: string; userId: string }; @@ -93,7 +107,10 @@ export function registerLevelingRoutes(app: FastifyInstance): void { // GET level settings app.get( "/api/guilds/:guildId/level-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.settings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.settings.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Leveling", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await getLevelSettings(guildId); @@ -106,25 +123,50 @@ export function registerLevelingRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/level-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.settings.manage")], - schema: { - body: { - type: "object", - properties: { - enabled: { type: "boolean" }, - xpPerMessage: { type: "integer", minimum: 1, maximum: 1000 }, - xpCooldownSeconds: { type: "integer", minimum: 0, maximum: 3600 }, - voiceXpPerMinute: { type: "integer", minimum: 0, maximum: 100 }, - voiceXpEnabled: { type: "boolean" }, - announceChannel: { type: ["string", "null"] }, - announceMessage: { type: "string", minLength: 1, maxLength: 500 }, - announceEnabled: { type: "boolean" }, - noXpChannels: { type: "array", items: { type: "string" } }, - noXpRoles: { type: "array", items: { type: "string" } }, - xpMultipliers: { type: "object" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + enabled: { type: "boolean" }, + xpPerMessage: { type: "integer", minimum: 1, maximum: 1000 }, + xpCooldownSeconds: { type: "integer", minimum: 0, maximum: 3600 }, + voiceXpPerMinute: { type: "integer", minimum: 0, maximum: 100 }, + voiceXpEnabled: { type: "boolean" }, + announceChannel: { type: ["string", "null"] }, + announceMessage: { type: "string", minLength: 1, maxLength: 500 }, + announceEnabled: { type: "boolean" }, + noXpChannels: { type: "array", items: { type: "string" } }, + noXpRoles: { type: "array", items: { type: "string" } }, + xpMultipliers: { type: "object", additionalProperties: true }, + }, + additionalProperties: false, + }, + }, + { + tag: "Leveling", + response: { + 200: { + type: "object", + properties: { + guildId: { type: "string" }, + enabled: { type: "boolean" }, + xpPerMessage: { type: "integer" }, + xpCooldownSeconds: { type: "integer" }, + voiceXpPerMinute: { type: "integer" }, + voiceXpEnabled: { type: "boolean" }, + announceChannel: { type: ["string", "null"] }, + announceMessage: { type: ["string", "null"] }, + announceEnabled: { type: "boolean" }, + noXpChannels: { type: "array", items: { type: "string" } }, + noXpRoles: { type: "array", items: { type: "string" } }, + xpMultipliers: { type: "object", additionalProperties: true }, + }, + }, }, - additionalProperties: false, }, - }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -150,7 +192,10 @@ export function registerLevelingRoutes(app: FastifyInstance): void { // GET level rewards app.get( "/api/guilds/:guildId/level-rewards", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.rewards.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.rewards.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Leveling", response: { 200: { type: "array", items: {} } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const rewards = await getLevelRewards(guildId); @@ -163,17 +208,34 @@ export function registerLevelingRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/level-rewards", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.rewards.manage")], - schema: { - body: { - type: "object", - required: ["level", "roleId"], - properties: { - level: { type: "integer", minimum: 1, maximum: 100 }, - roleId: { type: "string", minLength: 1 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["level", "roleId"], + properties: { + level: { type: "integer", minimum: 1, maximum: 100 }, + roleId: { type: "string", minLength: 1 }, + }, + additionalProperties: false, + }, + }, + { + tag: "Leveling", + response: { + 201: { + type: "object", + properties: { + id: { type: "integer" }, + guildId: { type: "string" }, + level: { type: "integer" }, + roleId: { type: "string" }, + }, + }, }, - additionalProperties: false, }, - }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -191,7 +253,10 @@ export function registerLevelingRoutes(app: FastifyInstance): void { // DELETE level reward app.delete( "/api/guilds/:guildId/level-rewards/:id", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.rewards.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("leveling.rewards.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Leveling", response: { 200: { type: "object", properties: { success: { type: "boolean" } } } } }), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const rewardId = parseIntParam(id); diff --git a/apps/dashboard/src/server/features/logging/routes.ts b/apps/dashboard/src/server/features/logging/routes.ts index 3f302c2..2cf1b81 100644 --- a/apps/dashboard/src/server/features/logging/routes.ts +++ b/apps/dashboard/src/server/features/logging/routes.ts @@ -4,12 +4,19 @@ import { loadLogConfigs, upsertLogConfig } from "@fluxcore/systems/logging/confi import { getLogEntries, cleanOldLogEntries } from "@fluxcore/systems/logging/persistence"; import { LOG_CATEGORIES, EVENT_TYPES_BY_CATEGORY } from "@fluxcore/systems/logging/constants"; import type { LogCategory } from "@fluxcore/systems/logging/types"; +import { withDocs } from "../../shared/openapi-schemas.js"; export function registerLoggingRoutes(app: FastifyInstance): void { // GET log entries for a guild with optional filters app.get( "/api/guilds/:guildId/logs", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.entries.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.entries.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, + { tag: "Logging", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { @@ -44,7 +51,25 @@ export function registerLoggingRoutes(app: FastifyInstance): void { // GET all category configs for a guild app.get( "/api/guilds/:guildId/log-config", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.config.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.config.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Logging", + response: { + 200: { + type: "object", + properties: { + configs: { type: "array", items: { type: "object", additionalProperties: true } }, + categories: { type: "array", items: { type: "string" } }, + eventTypes: { type: "object", additionalProperties: true }, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const configs = await loadLogConfigs(guildId); @@ -57,20 +82,24 @@ export function registerLoggingRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/log-config/:category", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.config.manage")], - schema: { - body: { - type: "object", - required: ["channelId"], - properties: { - channelId: { type: "string", minLength: 1 }, - enabled: { type: "boolean" }, - ignoredChannels: { type: "array", items: { type: "string" } }, - ignoredRoles: { type: "array", items: { type: "string" } }, - enabledEvents: { type: "array", items: { type: "string" } }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["channelId"], + properties: { + channelId: { type: "string", minLength: 1 }, + enabled: { type: "boolean" }, + ignoredChannels: { type: "array", items: { type: "string" } }, + ignoredRoles: { type: "array", items: { type: "string" } }, + enabledEvents: { type: "array", items: { type: "string" } }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Logging", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, category } = request.params as { guildId: string; category: string }; @@ -103,7 +132,16 @@ export function registerLoggingRoutes(app: FastifyInstance): void { // DELETE purge old logs app.delete( "/api/guilds/:guildId/logs", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.entries.purge")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("logging.entries.purge")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Logging", + response: { 200: { type: "object", properties: { purged: { type: "integer" } } } }, + }, + ), + }, async (_request, reply) => { const count = await cleanOldLogEntries(); reply.send({ purged: count }); diff --git a/apps/dashboard/src/server/features/moderation/routes.ts b/apps/dashboard/src/server/features/moderation/routes.ts index 4265b5e..81553dc 100644 --- a/apps/dashboard/src/server/features/moderation/routes.ts +++ b/apps/dashboard/src/server/features/moderation/routes.ts @@ -9,6 +9,7 @@ import { upsertModSettings, } from "@fluxcore/systems/moderation/persistence"; import { VALID_MOD_ACTIONS, MAX_REASON_LENGTH } from "@fluxcore/systems/moderation/constants"; +import { withDocs } from "../../shared/openapi-schemas.js"; function parseIntParam(value: string): number | null { const n = parseInt(value, 10); @@ -19,7 +20,13 @@ export function registerModerationRoutes(app: FastifyInstance): void { // GET /api/guilds/:guildId/cases — list cases app.get( "/api/guilds/:guildId/cases", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, + { tag: "Moderation", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { @@ -51,7 +58,13 @@ export function registerModerationRoutes(app: FastifyInstance): void { // GET /api/guilds/:guildId/cases/:caseId — get single case app.get( "/api/guilds/:guildId/cases/:caseId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Moderation", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId, caseId } = request.params as { guildId: string; caseId: string }; const caseIdNum = parseIntParam(caseId); @@ -75,16 +88,20 @@ export function registerModerationRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/cases/:caseId", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.manage")], - schema: { - body: { - type: "object", - required: ["reason"], - properties: { - reason: { type: "string", minLength: 1, maxLength: MAX_REASON_LENGTH }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["reason"], + properties: { + reason: { type: "string", minLength: 1, maxLength: MAX_REASON_LENGTH }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Moderation", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, caseId } = request.params as { guildId: string; caseId: string }; @@ -110,7 +127,16 @@ export function registerModerationRoutes(app: FastifyInstance): void { // DELETE /api/guilds/:guildId/cases/:caseId — delete case app.delete( "/api/guilds/:guildId/cases/:caseId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.cases.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Moderation", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + }, async (request, reply) => { const { guildId, caseId } = request.params as { guildId: string; caseId: string }; const caseIdNum = parseIntParam(caseId); @@ -133,7 +159,13 @@ export function registerModerationRoutes(app: FastifyInstance): void { // GET /api/guilds/:guildId/mod-settings app.get( "/api/guilds/:guildId/mod-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.settings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.settings.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Moderation", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await getModSettings(guildId); @@ -146,16 +178,20 @@ export function registerModerationRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/mod-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.settings.manage")], - schema: { - body: { - type: "object", - properties: { - dmOnPunishment: { type: "boolean" }, - modLogChannelId: { type: ["string", "null"] }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + dmOnPunishment: { type: "boolean" }, + modLogChannelId: { type: ["string", "null"] }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Moderation", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/moderation/warnings-routes.ts b/apps/dashboard/src/server/features/moderation/warnings-routes.ts index 82b5575..3b0058d 100644 --- a/apps/dashboard/src/server/features/moderation/warnings-routes.ts +++ b/apps/dashboard/src/server/features/moderation/warnings-routes.ts @@ -14,6 +14,7 @@ import { removePunishment, } from "@fluxcore/systems/warnings/config"; import { MAX_REASON_LENGTH, VALID_ESCALATION_ACTIONS } from "@fluxcore/systems/warnings/constants"; +import { withDocs } from "../../shared/openapi-schemas.js"; function parseIntParam(value: string): number | null { const n = parseInt(value, 10); @@ -24,7 +25,13 @@ export function registerWarningRoutes(app: FastifyInstance): void { // GET warnings for a guild (filterable by userId) app.get( "/api/guilds/:guildId/warnings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, + { tag: "Warnings", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { userId?: string; page?: string; limit?: string }; @@ -46,17 +53,21 @@ export function registerWarningRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/warnings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.manage")], - schema: { - body: { - type: "object", - required: ["userId", "reason"], - properties: { - userId: { type: "string", minLength: 1 }, - reason: { type: "string", minLength: 1, maxLength: MAX_REASON_LENGTH }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["userId", "reason"], + properties: { + userId: { type: "string", minLength: 1 }, + reason: { type: "string", minLength: 1, maxLength: MAX_REASON_LENGTH }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Warnings", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -76,7 +87,16 @@ export function registerWarningRoutes(app: FastifyInstance): void { // DELETE a specific warning app.delete( "/api/guilds/:guildId/warnings/:warningId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Warnings", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + }, async (request, reply) => { const { guildId, warningId } = request.params as { guildId: string; warningId: string }; const id = parseIntParam(warningId); @@ -92,7 +112,21 @@ export function registerWarningRoutes(app: FastifyInstance): void { // DELETE all warnings for a user app.delete( "/api/guilds/:guildId/warnings/user/:userId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.warnings.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Warnings", + response: { + 200: { + type: "object", + properties: { success: { type: "boolean" }, count: { type: "integer" } }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId, userId } = request.params as { guildId: string; userId: string }; const count = await deleteAllWarnings(guildId, userId); @@ -103,7 +137,13 @@ export function registerWarningRoutes(app: FastifyInstance): void { // GET punishment config app.get( "/api/guilds/:guildId/warn-punishments", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Warnings", response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const punishments = await getPunishments(guildId); @@ -116,18 +156,22 @@ export function registerWarningRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/warn-punishments", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")], - schema: { - body: { - type: "object", - required: ["threshold", "action"], - properties: { - threshold: { type: "integer", minimum: 1 }, - action: { type: "string", enum: [...VALID_ESCALATION_ACTIONS] }, - duration: { type: ["integer", "null"], minimum: 1 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["threshold", "action"], + properties: { + threshold: { type: "integer", minimum: 1 }, + action: { type: "string", enum: [...VALID_ESCALATION_ACTIONS] }, + duration: { type: ["integer", "null"], minimum: 1 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Warnings", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -145,7 +189,16 @@ export function registerWarningRoutes(app: FastifyInstance): void { // DELETE a punishment app.delete( "/api/guilds/:guildId/warn-punishments/:id", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Warnings", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const punishmentId = parseIntParam(id); @@ -161,7 +214,13 @@ export function registerWarningRoutes(app: FastifyInstance): void { // GET warn settings app.get( "/api/guilds/:guildId/warn-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Warnings", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await getWarnSettings(guildId); @@ -174,17 +233,21 @@ export function registerWarningRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/warn-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("moderation.punishments.manage")], - schema: { - body: { - type: "object", - properties: { - dmOnWarn: { type: "boolean" }, - reasonRequired: { type: "boolean" }, - maxWarnings: { type: "integer", minimum: 0 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + dmOnWarn: { type: "boolean" }, + reasonRequired: { type: "boolean" }, + maxWarnings: { type: "integer", minimum: 0 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Warnings", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/music/routes.ts b/apps/dashboard/src/server/features/music/routes.ts index 799c249..03184f7 100644 --- a/apps/dashboard/src/server/features/music/routes.ts +++ b/apps/dashboard/src/server/features/music/routes.ts @@ -22,6 +22,7 @@ import { MAX_TRACKS_PER_ALBUM, } from "@fluxcore/systems/music/constants"; import { notifyCacheInvalidation } from "@fluxcore/systems/actions/persistence"; +import { withDocs } from "../../shared/openapi-schemas.js"; function parseIntParam(value: string): number | null { const n = parseInt(value, 10); @@ -32,7 +33,13 @@ export function registerMusicRoutes(app: FastifyInstance): void { // GET music settings for a guild app.get( "/api/guilds/:guildId/music/settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.settings.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.settings.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Music", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await fetchMusicSettings(guildId); @@ -45,21 +52,25 @@ export function registerMusicRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/music/settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.settings.manage")], - schema: { - body: { - type: "object", - properties: { - mode: { type: "string", enum: ["open", "library"] }, - djRoleId: { type: ["string", "null"] }, - defaultVolume: { type: "integer", minimum: 0, maximum: 100 }, - maxQueueSize: { type: "integer", minimum: 1, maximum: MAX_QUEUE_SIZE_LIMIT }, - autoDisconnectSecs: { type: "integer", minimum: 0, maximum: 3600 }, - twentyFourSeven: { type: "boolean" }, - lastChannelId: { type: ["string", "null"] }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + mode: { type: "string", enum: ["open", "library"] }, + djRoleId: { type: ["string", "null"] }, + defaultVolume: { type: "integer", minimum: 0, maximum: 100 }, + maxQueueSize: { type: "integer", minimum: 1, maximum: MAX_QUEUE_SIZE_LIMIT }, + autoDisconnectSecs: { type: "integer", minimum: 0, maximum: 3600 }, + twentyFourSeven: { type: "boolean" }, + lastChannelId: { type: ["string", "null"] }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Music", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -92,7 +103,13 @@ export function registerMusicRoutes(app: FastifyInstance): void { // GET all albums for a guild app.get( "/api/guilds/:guildId/music/library", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Music", response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const albums = await getAlbums(guildId); @@ -113,16 +130,20 @@ export function registerMusicRoutes(app: FastifyInstance): void { (req as { session?: { userId?: string } }).session?.userId ?? req.ip, }, }, - schema: { - body: { - type: "object", - required: ["name"], - properties: { - name: { type: "string", minLength: 1 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["name"], + properties: { + name: { type: "string", minLength: 1 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Music", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -146,7 +167,16 @@ export function registerMusicRoutes(app: FastifyInstance): void { // DELETE an album app.delete( "/api/guilds/:guildId/music/library/:albumId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Music", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + }, async (request, reply) => { const { guildId, albumId } = request.params as { guildId: string; albumId: string }; const albumIdNum = parseIntParam(albumId); @@ -167,7 +197,13 @@ export function registerMusicRoutes(app: FastifyInstance): void { // GET tracks in an album app.get( "/api/guilds/:guildId/music/library/:albumId/tracks", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Music", response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } } }, + ), + }, async (request, reply) => { const { guildId, albumId } = request.params as { guildId: string; albumId: string }; const albumIdNum = parseIntParam(albumId); @@ -190,18 +226,22 @@ export function registerMusicRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/music/library/:albumId/tracks", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.manage")], - schema: { - body: { - type: "object", - required: ["title", "sourceUrl"], - properties: { - title: { type: "string", minLength: 1 }, - sourceUrl: { type: "string", minLength: 1 }, - duration: { type: ["integer", "null"] }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["title", "sourceUrl"], + properties: { + title: { type: "string", minLength: 1 }, + sourceUrl: { type: "string", minLength: 1 }, + duration: { type: ["integer", "null"] }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Music", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, albumId } = request.params as { guildId: string; albumId: string }; @@ -250,7 +290,16 @@ export function registerMusicRoutes(app: FastifyInstance): void { // DELETE a track app.delete( "/api/guilds/:guildId/music/library/:albumId/tracks/:trackId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("music.library.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Music", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + }, async (request, reply) => { const { guildId, albumId, trackId } = request.params as { guildId: string; diff --git a/apps/dashboard/src/server/features/permissions/roles-routes.ts b/apps/dashboard/src/server/features/permissions/roles-routes.ts index 33093b3..2eac01e 100644 --- a/apps/dashboard/src/server/features/permissions/roles-routes.ts +++ b/apps/dashboard/src/server/features/permissions/roles-routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { getPrisma } from "@fluxcore/database"; import { ALL_PERMISSION_KEYS, @@ -33,7 +34,13 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { // GET all dashboard roles for a guild app.get( "/api/guilds/:guildId/dashboard-roles", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.view")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardRoles", response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.view")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const prisma = getPrisma(); @@ -65,23 +72,27 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/dashboard-roles", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], - schema: { - body: { - type: "object", - required: ["name", "permissions"], - properties: { - name: { type: "string", minLength: 1, maxLength: NAME_MAX_LENGTH }, - color: { type: ["string", "null"] }, - isDefault: { type: "boolean" }, - permissions: { - type: "array", - items: { type: "string" }, - maxItems: MAX_PERMISSIONS_PER_ROLE, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["name", "permissions"], + properties: { + name: { type: "string", minLength: 1, maxLength: NAME_MAX_LENGTH }, + color: { type: ["string", "null"] }, + isDefault: { type: "boolean" }, + permissions: { + type: "array", + items: { type: "string" }, + maxItems: MAX_PERMISSIONS_PER_ROLE, + }, }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "DashboardRoles", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -183,23 +194,27 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/dashboard-roles/:roleId", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], - schema: { - body: { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: NAME_MAX_LENGTH }, - color: { type: ["string", "null"] }, - position: { type: "integer", minimum: 0 }, - isDefault: { type: "boolean" }, - permissions: { - type: "array", - items: { type: "string" }, - maxItems: MAX_PERMISSIONS_PER_ROLE, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: NAME_MAX_LENGTH }, + color: { type: ["string", "null"] }, + position: { type: "integer", minimum: 0 }, + isDefault: { type: "boolean" }, + permissions: { + type: "array", + items: { type: "string" }, + maxItems: MAX_PERMISSIONS_PER_ROLE, + }, }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "DashboardRoles", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, roleId } = request.params as { guildId: string; roleId: string }; @@ -292,7 +307,16 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { // DELETE a dashboard role app.delete( "/api/guilds/:guildId/dashboard-roles/:roleId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "DashboardRoles", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], + }, async (request, reply) => { const { guildId, roleId } = request.params as { guildId: string; roleId: string }; const session = request.session!; @@ -328,7 +352,13 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { // GET members of a dashboard role app.get( "/api/guilds/:guildId/dashboard-roles/:roleId/members", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.view")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardRoles", response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.view")], + }, async (request, reply) => { const { guildId, roleId } = request.params as { guildId: string; roleId: string }; const prisma = getPrisma(); @@ -360,16 +390,20 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/dashboard-roles/:roleId/members", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], - schema: { - body: { - type: "object", - required: ["userId"], - properties: { - userId: { type: "string", minLength: 1 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["userId"], + properties: { + userId: { type: "string", minLength: 1 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "DashboardRoles", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, roleId } = request.params as { guildId: string; roleId: string }; @@ -420,7 +454,16 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { // DELETE remove a user from a role app.delete( "/api/guilds/:guildId/dashboard-roles/:roleId/members/:userId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "DashboardRoles", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], + }, async (request, reply) => { const { guildId, roleId, userId } = request.params as { guildId: string; @@ -459,7 +502,13 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { // GET available role presets app.get( "/api/guilds/:guildId/dashboard-roles/presets", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardRoles", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], + }, async (_request, reply) => { reply.send(ROLE_PRESETS); }, @@ -470,16 +519,20 @@ export function registerDashboardRoleRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/dashboard-roles/from-preset", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], - schema: { - body: { - type: "object", - required: ["preset"], - properties: { - preset: { type: "string", enum: Object.keys(ROLE_PRESETS) }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["preset"], + properties: { + preset: { type: "string", enum: Object.keys(ROLE_PRESETS) }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "DashboardRoles", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/permissions/routes.ts b/apps/dashboard/src/server/features/permissions/routes.ts index c82d92b..e660e80 100644 --- a/apps/dashboard/src/server/features/permissions/routes.ts +++ b/apps/dashboard/src/server/features/permissions/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { getPrisma } from "@fluxcore/database"; import { PERMISSION_REGISTRY, @@ -19,7 +20,13 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { // GET current user's resolved permissions for this guild app.get( "/api/guilds/:guildId/my-permissions", - { preHandler: [requireAuth, requireGuildAdmin] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardPermissions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const resolved = request.resolvedPermissions!; @@ -42,7 +49,13 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { // GET permission registry (available permissions) app.get( "/api/guilds/:guildId/permission-registry", - { preHandler: [requireAuth, requireGuildAdmin] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardPermissions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin], + }, async (_request, reply) => { reply.send(PERMISSION_REGISTRY); }, @@ -53,7 +66,13 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { // GET a user's direct permission overrides app.get( "/api/guilds/:guildId/user-permissions/:userId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardPermissions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], + }, async (request, reply) => { const { guildId, userId } = request.params as { guildId: string; userId: string }; const prisma = getPrisma(); @@ -84,20 +103,35 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/user-permissions/:userId", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], - schema: { - body: { - type: "object", - required: ["permissions"], - properties: { - permissions: { - type: "array", - items: { type: "string" }, - maxItems: 100, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["permissions"], + properties: { + permissions: { + type: "array", + items: { type: "string" }, + maxItems: 100, + }, }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { + tag: "DashboardPermissions", + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + permissions: { type: "array", items: { type: "string" } }, + }, + }, + }, + }, + ), }, async (request, reply) => { const { guildId, userId } = request.params as { guildId: string; userId: string }; @@ -177,7 +211,16 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { // DELETE clear all permission overrides for a user app.delete( "/api/guilds/:guildId/user-permissions/:userId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "DashboardPermissions", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.roles.manage")], + }, async (request, reply) => { const { guildId, userId } = request.params as { guildId: string; userId: string }; const session = request.session!; @@ -204,7 +247,13 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { // GET dashboard settings app.get( "/api/guilds/:guildId/dashboard-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.settings.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "DashboardPermissions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.settings.manage")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const prisma = getPrisma(); @@ -228,16 +277,20 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/dashboard-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.settings.manage")], - schema: { - body: { - type: "object", - properties: { - auditRetentionDays: { type: "integer", minimum: 7, maximum: 365 }, - requirePermissions: { type: "boolean" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + auditRetentionDays: { type: "integer", minimum: 7, maximum: 365 }, + requirePermissions: { type: "boolean" }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "DashboardPermissions", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -287,7 +340,16 @@ export function registerDashboardPermissionRoutes(app: FastifyInstance): void { // GET audit log entries app.get( "/api/guilds/:guildId/dashboard-audit", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.audit.view")] }, + { + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } }, + }, + { tag: "DashboardPermissions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("dashboard.audit.view")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { diff --git a/apps/dashboard/src/server/features/roles/routes.ts b/apps/dashboard/src/server/features/roles/routes.ts index 58464db..bc849b2 100644 --- a/apps/dashboard/src/server/features/roles/routes.ts +++ b/apps/dashboard/src/server/features/roles/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getRolePanels, @@ -22,7 +23,10 @@ export function registerRolePanelRoutes(app: FastifyInstance): void { // GET all panels for a guild app.get( "/api/guilds/:guildId/role-panels", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "RolePanels", response: { 200: { type: "array", items: {} } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const panels = await getRolePanels(guildId); @@ -35,38 +39,62 @@ export function registerRolePanelRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/role-panels", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.manage")], - schema: { - body: { - type: "object", - required: ["name", "type", "channelId"], - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - type: { type: "string", enum: [...VALID_PANEL_TYPES] }, - mode: { type: "string", enum: [...VALID_PANEL_MODES] }, - channelId: { type: "string", minLength: 1 }, - embed: { type: "string" }, - roles: { - type: "array", - maxItems: MAX_ROLES_PER_PANEL, - items: { - type: "object", - required: ["roleId", "label"], - properties: { - roleId: { type: "string", minLength: 1 }, - label: { type: "string", minLength: 1, maxLength: 80 }, - emoji: { type: "string" }, - description: { type: "string", maxLength: 100 }, - style: { type: "integer", minimum: 1, maximum: 4 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["name", "type", "channelId"], + properties: { + name: { type: "string", minLength: 1, maxLength: 100 }, + type: { type: "string", enum: [...VALID_PANEL_TYPES] }, + mode: { type: "string", enum: [...VALID_PANEL_MODES] }, + channelId: { type: "string", minLength: 1 }, + embed: { type: "string" }, + roles: { + type: "array", + maxItems: MAX_ROLES_PER_PANEL, + items: { + type: "object", + required: ["roleId", "label"], + properties: { + roleId: { type: "string", minLength: 1 }, + label: { type: "string", minLength: 1, maxLength: 80 }, + emoji: { type: "string" }, + description: { type: "string", maxLength: 100 }, + style: { type: "integer", minimum: 1, maximum: 4 }, + }, + additionalProperties: false, }, - additionalProperties: false, + }, + maxRoles: { type: ["integer", "null"], minimum: 1 }, + minRoles: { type: ["integer", "null"], minimum: 0 }, + }, + additionalProperties: false, + }, + }, + { + tag: "RolePanels", + response: { + 201: { + type: "object", + properties: { + id: { type: "integer" }, + guildId: { type: "string" }, + channelId: { type: "string" }, + name: { type: "string" }, + type: { type: "string" }, + mode: { type: "string" }, + embed: { type: ["string", "null"] }, + roles: { type: "array", items: {} }, + maxRoles: { type: ["integer", "null"] }, + minRoles: { type: ["integer", "null"] }, + createdBy: { type: "string" }, }, }, - maxRoles: { type: ["integer", "null"], minimum: 1 }, - minRoles: { type: ["integer", "null"], minimum: 0 }, }, - additionalProperties: false, }, - }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -109,37 +137,61 @@ export function registerRolePanelRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/role-panels/:panelId", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.manage")], - schema: { - body: { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - type: { type: "string", enum: [...VALID_PANEL_TYPES] }, - mode: { type: "string", enum: [...VALID_PANEL_MODES] }, - channelId: { type: "string", minLength: 1 }, - embed: { type: "string" }, - roles: { - type: "array", - maxItems: MAX_ROLES_PER_PANEL, - items: { - type: "object", - required: ["roleId", "label"], - properties: { - roleId: { type: "string", minLength: 1 }, - label: { type: "string", minLength: 1, maxLength: 80 }, - emoji: { type: "string" }, - description: { type: "string", maxLength: 100 }, - style: { type: "integer", minimum: 1, maximum: 4 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 100 }, + type: { type: "string", enum: [...VALID_PANEL_TYPES] }, + mode: { type: "string", enum: [...VALID_PANEL_MODES] }, + channelId: { type: "string", minLength: 1 }, + embed: { type: "string" }, + roles: { + type: "array", + maxItems: MAX_ROLES_PER_PANEL, + items: { + type: "object", + required: ["roleId", "label"], + properties: { + roleId: { type: "string", minLength: 1 }, + label: { type: "string", minLength: 1, maxLength: 80 }, + emoji: { type: "string" }, + description: { type: "string", maxLength: 100 }, + style: { type: "integer", minimum: 1, maximum: 4 }, + }, + additionalProperties: false, }, - additionalProperties: false, + }, + maxRoles: { type: ["integer", "null"], minimum: 1 }, + minRoles: { type: ["integer", "null"], minimum: 0 }, + }, + additionalProperties: false, + }, + }, + { + tag: "RolePanels", + response: { + 200: { + type: "object", + properties: { + id: { type: "integer" }, + guildId: { type: "string" }, + channelId: { type: "string" }, + name: { type: "string" }, + type: { type: "string" }, + mode: { type: "string" }, + embed: { type: ["string", "null"] }, + roles: { type: "array", items: {} }, + maxRoles: { type: ["integer", "null"] }, + minRoles: { type: ["integer", "null"] }, + createdBy: { type: "string" }, }, }, - maxRoles: { type: ["integer", "null"], minimum: 1 }, - minRoles: { type: ["integer", "null"], minimum: 0 }, }, - additionalProperties: false, }, - }, + ), }, async (request, reply) => { const { guildId, panelId } = request.params as { guildId: string; panelId: string }; @@ -189,7 +241,10 @@ export function registerRolePanelRoutes(app: FastifyInstance): void { // DELETE a panel app.delete( "/api/guilds/:guildId/role-panels/:panelId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "RolePanels", response: { 200: { type: "object", properties: { success: { type: "boolean" } } } } }), + }, async (request, reply) => { const { guildId, panelId } = request.params as { guildId: string; panelId: string }; const id = parseIntParam(panelId); @@ -211,7 +266,25 @@ export function registerRolePanelRoutes(app: FastifyInstance): void { // POST send/resend panel message app.post( "/api/guilds/:guildId/role-panels/:panelId/send", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("roles.panels.manage")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "RolePanels", + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" }, + panel: {}, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId, panelId } = request.params as { guildId: string; panelId: string }; const id = parseIntParam(panelId); diff --git a/apps/dashboard/src/server/features/scheduled/routes.ts b/apps/dashboard/src/server/features/scheduled/routes.ts index 17c33f9..48c6fde 100644 --- a/apps/dashboard/src/server/features/scheduled/routes.ts +++ b/apps/dashboard/src/server/features/scheduled/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getScheduledMessages, @@ -18,7 +19,13 @@ export function registerScheduledMessageRoutes(app: FastifyInstance): void { // GET list scheduled messages app.get( "/api/guilds/:guildId/scheduled-messages", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, + { tag: "ScheduledMessages", response: { 200: { type: "object", additionalProperties: true } } }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { page?: string; limit?: string }; @@ -39,29 +46,33 @@ export function registerScheduledMessageRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/scheduled-messages", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.manage")], - schema: { - body: { - type: "object", - required: ["channelId", "name", "message", "cronExpr"], - properties: { - channelId: { type: "string", minLength: 1 }, - name: { type: "string", minLength: 1, maxLength: 100 }, - message: { - type: "object", - required: ["type"], - properties: { - type: { type: "string", enum: ["text", "embed"] }, - content: { type: "string", maxLength: 2000 }, - embed: { type: "object" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["channelId", "name", "message", "cronExpr"], + properties: { + channelId: { type: "string", minLength: 1 }, + name: { type: "string", minLength: 1, maxLength: 100 }, + message: { + type: "object", + required: ["type"], + properties: { + type: { type: "string", enum: ["text", "embed"] }, + content: { type: "string", maxLength: 2000 }, + embed: { type: "object", additionalProperties: true }, + }, }, + cronExpr: { type: "string", minLength: 1 }, + timezone: { type: "string" }, + enabled: { type: "boolean" }, }, - cronExpr: { type: "string", minLength: 1 }, - timezone: { type: "string" }, - enabled: { type: "boolean" }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "ScheduledMessages", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -112,27 +123,31 @@ export function registerScheduledMessageRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/scheduled-messages/:id", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.manage")], - schema: { - body: { - type: "object", - properties: { - channelId: { type: "string", minLength: 1 }, - name: { type: "string", minLength: 1, maxLength: 100 }, - message: { - type: "object", - properties: { - type: { type: "string", enum: ["text", "embed"] }, - content: { type: "string", maxLength: 2000 }, - embed: { type: "object" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + channelId: { type: "string", minLength: 1 }, + name: { type: "string", minLength: 1, maxLength: 100 }, + message: { + type: "object", + properties: { + type: { type: "string", enum: ["text", "embed"] }, + content: { type: "string", maxLength: 2000 }, + embed: { type: "object", additionalProperties: true }, + }, }, + cronExpr: { type: "string", minLength: 1 }, + timezone: { type: "string" }, + enabled: { type: "boolean" }, }, - cronExpr: { type: "string", minLength: 1 }, - timezone: { type: "string" }, - enabled: { type: "boolean" }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "ScheduledMessages", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; @@ -172,7 +187,10 @@ export function registerScheduledMessageRoutes(app: FastifyInstance): void { // DELETE scheduled message app.delete( "/api/guilds/:guildId/scheduled-messages/:id", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "ScheduledMessages", response: { 200: { type: "object", properties: { success: { type: "boolean" } } } } }), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const msgId = parseIntParam(id); @@ -193,7 +211,25 @@ export function registerScheduledMessageRoutes(app: FastifyInstance): void { // POST test send a scheduled message app.post( "/api/guilds/:guildId/scheduled-messages/:id/test", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.execute")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.execute")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "ScheduledMessages", + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + channelId: { type: "string" }, + message: {}, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const msgId = parseIntParam(id); @@ -222,6 +258,18 @@ export function registerScheduledMessageRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/scheduled-messages/preview-cron", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("scheduled.messages.view")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "ScheduledMessages", + response: { + 200: { + type: "object", + properties: { nextRuns: { type: "array", items: { type: "string" } } }, + }, + }, + }, + ), config: { rateLimit: { max: 5, diff --git a/apps/dashboard/src/server/features/security/routes.ts b/apps/dashboard/src/server/features/security/routes.ts index 4802872..0001a0d 100644 --- a/apps/dashboard/src/server/features/security/routes.ts +++ b/apps/dashboard/src/server/features/security/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getAntiRaidConfig, @@ -12,7 +13,10 @@ export function registerAntiRaidRoutes(app: FastifyInstance): void { // GET anti-raid config app.get( "/api/guilds/:guildId/antiraid-config", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("security.config.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("security.config.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "AntiRaid", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const config = await getAntiRaidConfig(guildId); @@ -25,7 +29,8 @@ export function registerAntiRaidRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/antiraid-config", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("security.config.manage")], - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", properties: { @@ -43,7 +48,7 @@ export function registerAntiRaidRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "AntiRaid", response: { 200: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -69,7 +74,10 @@ export function registerAntiRaidRoutes(app: FastifyInstance): void { // GET raid events (paginated) app.get( "/api/guilds/:guildId/raid-events", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("security.events.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("security.events.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, { tag: "AntiRaid", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { page?: string; limit?: string }; diff --git a/apps/dashboard/src/server/features/starboard/routes.ts b/apps/dashboard/src/server/features/starboard/routes.ts index ef80349..15094a3 100644 --- a/apps/dashboard/src/server/features/starboard/routes.ts +++ b/apps/dashboard/src/server/features/starboard/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getStarboardSettings, upsertStarboardSettings } from "@fluxcore/systems/starboard/config"; import { getStarboardEntries } from "@fluxcore/systems/starboard/persistence"; @@ -8,7 +9,16 @@ export function registerStarboardRoutes(app: FastifyInstance): void { // GET starred messages app.get( "/api/guilds/:guildId/starboard", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("starboard.entries.view")] }, + { + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } }, + }, + { tag: "Starboard", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("starboard.entries.view")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { page?: string; limit?: string }; @@ -27,7 +37,13 @@ export function registerStarboardRoutes(app: FastifyInstance): void { // GET starboard settings app.get( "/api/guilds/:guildId/starboard-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("starboard.settings.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Starboard", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("starboard.settings.manage")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await getStarboardSettings(guildId); @@ -40,21 +56,25 @@ export function registerStarboardRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/starboard-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("starboard.settings.manage")], - schema: { - body: { - type: "object", - properties: { - enabled: { type: "boolean" }, - channelId: { type: ["string", "null"] }, - emoji: { type: "string", minLength: 1, maxLength: 100 }, - threshold: { type: "integer", minimum: 1, maximum: 100 }, - selfStar: { type: "boolean" }, - ignoredChannels: { type: "array", items: { type: "string" } }, - nsfwHandling: { type: "string", enum: ["ignore", "separate"] }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + enabled: { type: "boolean" }, + channelId: { type: ["string", "null"] }, + emoji: { type: "string", minLength: 1, maxLength: 100 }, + threshold: { type: "integer", minimum: 1, maximum: 100 }, + selfStar: { type: "boolean" }, + ignoredChannels: { type: "array", items: { type: "string" } }, + nsfwHandling: { type: "string", enum: ["ignore", "separate"] }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Starboard", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/suggestions/routes.ts b/apps/dashboard/src/server/features/suggestions/routes.ts index 40fdd27..32ec013 100644 --- a/apps/dashboard/src/server/features/suggestions/routes.ts +++ b/apps/dashboard/src/server/features/suggestions/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getSuggestionSettings, @@ -17,7 +18,16 @@ export function registerSuggestionRoutes(app: FastifyInstance): void { // GET suggestions list app.get( "/api/guilds/:guildId/suggestions", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.list.view")] }, + { + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } }, + }, + { tag: "Suggestions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.list.view")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { status?: string; page?: string; limit?: string }; @@ -42,17 +52,21 @@ export function registerSuggestionRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/suggestions", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.list.manage")], - schema: { - body: { - type: "object", - required: ["content", "userId"], - properties: { - content: { type: "string", minLength: 1, maxLength: 2000 }, - userId: { type: "string", minLength: 1 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["content", "userId"], + properties: { + content: { type: "string", minLength: 1, maxLength: 2000 }, + userId: { type: "string", minLength: 1 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Suggestions", response: { 201: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -68,17 +82,21 @@ export function registerSuggestionRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/suggestions/:id/status", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.list.manage")], - schema: { - body: { - type: "object", - required: ["status"], - properties: { - status: { type: "string", enum: [...VALID_STATUSES] }, - reason: { type: "string", maxLength: 500 }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["status"], + properties: { + status: { type: "string", enum: [...VALID_STATUSES] }, + reason: { type: "string", maxLength: 500 }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Suggestions", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; @@ -111,7 +129,16 @@ export function registerSuggestionRoutes(app: FastifyInstance): void { // DELETE suggestion app.delete( "/api/guilds/:guildId/suggestions/:id", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.list.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Suggestions", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.list.manage")], + }, async (request, reply) => { const { guildId, id } = request.params as { guildId: string; id: string }; const suggestionId = parseInt(id, 10); @@ -133,7 +160,13 @@ export function registerSuggestionRoutes(app: FastifyInstance): void { // GET suggestion settings app.get( "/api/guilds/:guildId/suggestion-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.settings.manage")] }, + { + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { tag: "Suggestions", response: { 200: { type: "object", additionalProperties: true } } }, + ), + preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.settings.manage")], + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await getSuggestionSettings(guildId); @@ -146,20 +179,24 @@ export function registerSuggestionRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/suggestion-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("suggestions.settings.manage")], - schema: { - body: { - type: "object", - properties: { - enabled: { type: "boolean" }, - channelId: { type: ["string", "null"] }, - reviewChannelId: { type: ["string", "null"] }, - dmOnStatusChange: { type: "boolean" }, - autoThread: { type: "boolean" }, - anonymousMode: { type: "boolean" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + enabled: { type: "boolean" }, + channelId: { type: ["string", "null"] }, + reviewChannelId: { type: ["string", "null"] }, + dmOnStatusChange: { type: "boolean" }, + autoThread: { type: "boolean" }, + anonymousMode: { type: "boolean" }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { tag: "Suggestions", response: { 200: { type: "object", additionalProperties: true } } }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/tempvoice/routes.ts b/apps/dashboard/src/server/features/tempvoice/routes.ts index dc2c1a4..bdaeca3 100644 --- a/apps/dashboard/src/server/features/tempvoice/routes.ts +++ b/apps/dashboard/src/server/features/tempvoice/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getGuildConfigs, @@ -15,7 +16,13 @@ export function registerTempVoiceRoutes(app: FastifyInstance): void { // GET all configs for a guild app.get( "/api/guilds/:guildId/tempvoice", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { + tag: "TempVoice", + response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } }, + }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const configs = getGuildConfigs(guildId); @@ -26,7 +33,27 @@ export function registerTempVoiceRoutes(app: FastifyInstance): void { // POST create a new config app.post( "/api/guilds/:guildId/tempvoice", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.manage")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + required: ["hubChannelId"], + properties: { + hubChannelId: { type: "string" }, + categoryId: { type: ["string", "null"] }, + nameTemplate: { type: "string" }, + }, + }, + }, + { + tag: "TempVoice", + response: { 201: { type: "object", additionalProperties: true } }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const body = request.body as { @@ -86,7 +113,26 @@ export function registerTempVoiceRoutes(app: FastifyInstance): void { // PUT update an existing config app.put( "/api/guilds/:guildId/tempvoice/:configId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.manage")], + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + hubChannelId: { type: "string" }, + categoryId: { type: ["string", "null"] }, + nameTemplate: { type: "string" }, + }, + }, + }, + { + tag: "TempVoice", + response: { 200: { type: "object", additionalProperties: true } }, + }, + ), + }, async (request, reply) => { const { guildId, configId } = request.params as { guildId: string; @@ -148,7 +194,13 @@ export function registerTempVoiceRoutes(app: FastifyInstance): void { // DELETE a specific config app.delete( "/api/guilds/:guildId/tempvoice/:configId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tempvoice.config.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { + tag: "TempVoice", + response: { 200: { type: "object", properties: { success: { type: "boolean" } } } }, + }), + }, async (request, reply) => { const { guildId, configId } = request.params as { guildId: string; diff --git a/apps/dashboard/src/server/features/tickets/routes.ts b/apps/dashboard/src/server/features/tickets/routes.ts index fcf1257..daf408e 100644 --- a/apps/dashboard/src/server/features/tickets/routes.ts +++ b/apps/dashboard/src/server/features/tickets/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getTicketSettings, @@ -28,7 +29,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // GET list tickets app.get( "/api/guilds/:guildId/tickets", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.list.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.list.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, querystring: { type: "object", properties: { page: { type: "integer", minimum: 1, default: 1 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }, sort: { type: "string" } } } }, { tag: "Tickets", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const query = request.query as { @@ -62,7 +66,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // GET ticket by ID app.get( "/api/guilds/:guildId/tickets/:ticketId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.list.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.list.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Tickets", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId, ticketId } = request.params as { guildId: string; ticketId: string }; const id = parseIntParam(ticketId); @@ -84,7 +91,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // DELETE force-close ticket app.delete( "/api/guilds/:guildId/tickets/:ticketId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.list.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.list.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Tickets", response: { 200: { type: "object", properties: { success: { type: "boolean" } } } } }), + }, async (request, reply) => { const { guildId, ticketId } = request.params as { guildId: string; ticketId: string }; const id = parseIntParam(ticketId); @@ -109,7 +119,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // GET list panels app.get( "/api/guilds/:guildId/ticket-panels", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Tickets", response: { 200: { type: "array", items: {} } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const panels = await getTicketPanels(guildId); @@ -122,7 +135,8 @@ export function registerTicketRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/ticket-panels", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")], - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", required: ["channelId", "name"], @@ -163,7 +177,7 @@ export function registerTicketRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "Tickets", response: { 201: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -199,7 +213,8 @@ export function registerTicketRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/ticket-panels/:panelId", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")], - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", properties: { @@ -239,7 +254,7 @@ export function registerTicketRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "Tickets", response: { 200: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId, panelId } = request.params as { guildId: string; panelId: string }; @@ -270,7 +285,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // DELETE panel app.delete( "/api/guilds/:guildId/ticket-panels/:panelId", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Tickets", response: { 200: { type: "object", properties: { success: { type: "boolean" } } } } }), + }, async (request, reply) => { const { guildId, panelId } = request.params as { guildId: string; panelId: string }; const id = parseIntParam(panelId); @@ -287,7 +305,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // POST send panel message (placeholder — actual sending requires bot client) app.post( "/api/guilds/:guildId/ticket-panels/:panelId/send", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.panels.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Tickets", response: { 200: { type: "object", properties: { success: { type: "boolean" }, panelId: { type: "integer" } } } } }), + }, async (request, reply) => { const { guildId, panelId } = request.params as { guildId: string; panelId: string }; const id = parseIntParam(panelId); @@ -313,7 +334,10 @@ export function registerTicketRoutes(app: FastifyInstance): void { // GET settings app.get( "/api/guilds/:guildId/ticket-settings", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.settings.manage")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.settings.manage")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Tickets", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const settings = await getTicketSettings(guildId); @@ -326,7 +350,8 @@ export function registerTicketRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/ticket-settings", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("tickets.settings.manage")], - schema: { + schema: withDocs({ + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, body: { type: "object", properties: { @@ -338,7 +363,7 @@ export function registerTicketRoutes(app: FastifyInstance): void { }, additionalProperties: false, }, - }, + }, { tag: "Tickets", response: { 200: { type: "object", additionalProperties: true } } }), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; diff --git a/apps/dashboard/src/server/features/welcome/routes.ts b/apps/dashboard/src/server/features/welcome/routes.ts index 666dd4a..f5eb094 100644 --- a/apps/dashboard/src/server/features/welcome/routes.ts +++ b/apps/dashboard/src/server/features/welcome/routes.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from "fastify"; +import { withDocs } from "../../shared/openapi-schemas.js"; import { randomUUID } from "node:crypto"; import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js"; import { getWelcomeConfig, upsertWelcomeConfig } from "@fluxcore/systems/welcome/config"; @@ -21,7 +22,10 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { // GET full welcome config app.get( "/api/guilds/:guildId/welcome", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.config.view")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.config.view")], + schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, { tag: "Welcome", response: { 200: { type: "object", additionalProperties: true } } }), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const config = await getWelcomeConfig(guildId); @@ -49,27 +53,54 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/welcome", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.config.manage")], - schema: { - body: { - type: "object", - properties: { - welcomeEnabled: { type: "boolean" }, - welcomeChannelId: { type: ["string", "null"] }, - welcomeMessage: { type: "object" }, - farewellEnabled: { type: "boolean" }, - farewellChannelId: { type: ["string", "null"] }, - farewellMessage: { type: "object" }, - dmEnabled: { type: "boolean" }, - dmMessage: { type: "object" }, - autoRoleIds: { type: "array", items: { type: "string" } }, - welcomeImageEnabled: { type: "boolean" }, - welcomeImageConfig: { type: "object" }, - farewellImageEnabled: { type: "boolean" }, - farewellImageConfig: { type: "object" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + welcomeEnabled: { type: "boolean" }, + welcomeChannelId: { type: ["string", "null"] }, + welcomeMessage: { type: "object", additionalProperties: true }, + farewellEnabled: { type: "boolean" }, + farewellChannelId: { type: ["string", "null"] }, + farewellMessage: { type: "object", additionalProperties: true }, + dmEnabled: { type: "boolean" }, + dmMessage: { type: "object", additionalProperties: true }, + autoRoleIds: { type: "array", items: { type: "string" } }, + welcomeImageEnabled: { type: "boolean" }, + welcomeImageConfig: { type: "object", additionalProperties: true }, + farewellImageEnabled: { type: "boolean" }, + farewellImageConfig: { type: "object", additionalProperties: true }, + }, + additionalProperties: false, }, - additionalProperties: false, }, - }, + { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { + guildId: { type: "string" }, + welcomeEnabled: { type: "boolean" }, + welcomeChannelId: { type: ["string", "null"] }, + welcomeMessage: { type: "object", additionalProperties: true }, + farewellEnabled: { type: "boolean" }, + farewellChannelId: { type: ["string", "null"] }, + farewellMessage: { type: "object", additionalProperties: true }, + dmEnabled: { type: "boolean" }, + dmMessage: { type: "object", additionalProperties: true }, + autoRoleIds: { type: "array", items: { type: "string" } }, + welcomeImageEnabled: { type: "boolean" }, + welcomeImageConfig: { type: "object", additionalProperties: true }, + farewellImageEnabled: { type: "boolean" }, + farewellImageConfig: { type: "object", additionalProperties: true }, + }, + }, + }, + }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -114,7 +145,24 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { // POST test welcome message app.post( "/api/guilds/:guildId/welcome/test", - { preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.test.execute")] }, + { + preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.test.execute")], + schema: withDocs( + { params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, + { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + channelId: { type: "string" }, + }, + }, + }, + }, + ), + }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; const config = await getWelcomeConfig(guildId); @@ -140,16 +188,23 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/welcome/image/preview", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.config.view")], - schema: { - body: { - type: "object", - properties: { - settings: { type: "object" }, - type: { type: "string", enum: ["welcome", "farewell"] }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + settings: { type: "object", additionalProperties: true }, + type: { type: "string", enum: ["welcome", "farewell"] }, + }, + required: ["settings"], }, - required: ["settings"], }, - }, + { + tag: "Welcome", + response: { 200: { type: "string", format: "binary" } }, + }, + ), }, async (request, reply) => { const body = request.body as { settings: unknown; type?: string }; @@ -193,16 +248,28 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/welcome/image/background", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.config.manage")], - schema: { - body: { - type: "object", - properties: { - data: { type: "string" }, - contentType: { type: "string" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + data: { type: "string" }, + contentType: { type: "string" }, + }, + required: ["data", "contentType"], }, - required: ["data", "contentType"], }, - }, + { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { key: { type: "string" } }, + }, + }, + }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -258,15 +325,27 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { "/api/guilds/:guildId/welcome/image/background", { preHandler: [requireAuth, requireGuildAdmin, requirePermission("welcome.config.manage")], - schema: { - body: { - type: "object", - properties: { - key: { type: "string" }, + schema: withDocs( + { + params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] }, + body: { + type: "object", + properties: { + key: { type: "string" }, + }, + required: ["key"], + }, + }, + { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { success: { type: "boolean" } }, + }, }, - required: ["key"], }, - }, + ), }, async (request, reply) => { const { guildId } = request.params as { guildId: string }; @@ -286,7 +365,18 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { // GET available templates app.get( "/api/welcome/templates", - { preHandler: [requireAuth] }, + { + preHandler: [requireAuth], + schema: withDocs(undefined, { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { templates: { type: "array", items: {} } }, + }, + }, + }), + }, async (_request, reply) => { reply.send({ templates: getAllTemplates().map((t) => ({ @@ -302,7 +392,18 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { // GET available fonts app.get( "/api/welcome/fonts", - { preHandler: [requireAuth] }, + { + preHandler: [requireAuth], + schema: withDocs(undefined, { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { fonts: { type: "array", items: {} } }, + }, + }, + }), + }, async (_request, reply) => { reply.send({ fonts: getAvailableFonts() }); }, @@ -311,7 +412,18 @@ export function registerWelcomeRoutes(app: FastifyInstance): void { // GET preset backgrounds app.get( "/api/welcome/presets", - { preHandler: [requireAuth] }, + { + preHandler: [requireAuth], + schema: withDocs(undefined, { + tag: "Welcome", + response: { + 200: { + type: "object", + properties: { backgrounds: {} }, + }, + }, + }), + }, async (_request, reply) => { reply.send({ backgrounds: PRESET_BACKGROUNDS }); }, diff --git a/apps/dashboard/src/server/index.ts b/apps/dashboard/src/server/index.ts index 725aaf3..6ff59e2 100644 --- a/apps/dashboard/src/server/index.ts +++ b/apps/dashboard/src/server/index.ts @@ -1,4 +1,4 @@ -import Fastify from "fastify"; +import Fastify, { type FastifyInstance } from "fastify"; import fastifyCookie from "@fastify/cookie"; import fastifyHelmet from "@fastify/helmet"; import fastifyRateLimit from "@fastify/rate-limit"; @@ -37,8 +37,15 @@ import { registerDashboardPermissionRoutes } from "./features/permissions/routes import { registerI18n } from "./shared/i18n.js"; import { requireCsrf } from "./shared/csrf.js"; import { helmetOptions } from "./shared/security.js"; - -async function main(): Promise { +import { registerOpenApi } from "./shared/openapi.js"; +import { withDocs } from "./shared/openapi-schemas.js"; + +/** + * Build the fully-configured Fastify application (plugins, routes, OpenAPI + * docs) without starting the HTTP listener. Exported so tests and the + * production entrypoint share one setup path. + */ +export async function createApp(): Promise { if (!config.dashboardClientSecret) { logger.error("DASHBOARD_CLIENT_SECRET is required"); process.exit(1); @@ -71,6 +78,10 @@ async function main(): Promise { } }); + // Register OpenAPI/Swagger BEFORE any routes so every route (including + // i18n, bot-info and all feature routes) is captured by the generator. + await registerOpenApi(app); + const __dirname = dirname(fileURLToPath(import.meta.url)); // In production, serve the built React SPA @@ -86,7 +97,25 @@ async function main(): Promise { await registerI18n(app); // Public endpoint — no auth required - app.get("/api/bot-info", async (_request, reply) => { + app.get( + "/api/bot-info", + { + schema: withDocs(undefined, { + tag: "Meta", + secure: false, + response: { + 200: { + type: "object", + properties: { + clientId: { type: "string" }, + inviteUrl: { type: "string" }, + latency: { type: ["number", "null"] }, + }, + }, + }, + }), + }, + async (_request, reply) => { // Measure round-trip latency to Discord's API let latency: number | null = null; try { @@ -150,6 +179,12 @@ async function main(): Promise { }); } + return app; +} + +async function main(): Promise { + const app = await createApp(); + // Clean up expired sessions every hour const SESSION_CLEANUP_INTERVAL = 60 * 60 * 1000; const cleanupTimer = setInterval(async () => { @@ -184,8 +219,13 @@ async function main(): Promise { process.on("SIGTERM", () => void shutdown()); } -main().catch((error: unknown) => { - const err = error instanceof Error ? error : new Error(String(error)); - logger.error("Failed to start dashboard", err); - process.exit(1); -}); +// Only auto-start when this module is the process entrypoint +// (e.g. `node dist/server/index.js` or `tsx src/server/index.ts`). +// When imported (tests, `createApp` reuse) we must NOT call listen(). +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch((error: unknown) => { + const err = error instanceof Error ? error : new Error(String(error)); + logger.error("Failed to start dashboard", err); + process.exit(1); + }); +} diff --git a/apps/dashboard/src/server/shared/i18n.ts b/apps/dashboard/src/server/shared/i18n.ts index 4af1f49..9be9ea4 100644 --- a/apps/dashboard/src/server/shared/i18n.ts +++ b/apps/dashboard/src/server/shared/i18n.ts @@ -9,6 +9,7 @@ import { } from "@fluxcore/i18n/server"; import { supportedLanguageCodes } from "@fluxcore/i18n"; import type { TFunction } from "i18next"; +import { withDocs } from "./openapi-schemas.js"; declare module "fastify" { interface FastifyRequest { @@ -35,7 +36,30 @@ export async function registerI18n(app: FastifyInstance): Promise { // Serve translation files for the client app.get<{ Params: { lng: string; ns: string }; - }>("/api/i18n/:lng/:ns", async (request, reply) => { + }>( + "/api/i18n/:lng/:ns", + { + schema: withDocs( + { + params: { + type: "object", + properties: { + lng: { type: "string" }, + ns: { type: "string" }, + }, + required: ["lng", "ns"], + }, + }, + { + tag: "Meta", + secure: false, + response: { + 200: { type: "object", additionalProperties: true }, + }, + }, + ), + }, + async (request, reply) => { const { lng, ns } = request.params; // Validate language and namespace diff --git a/apps/dashboard/src/server/shared/openapi-schemas.ts b/apps/dashboard/src/server/shared/openapi-schemas.ts new file mode 100644 index 0000000..9f4a38e --- /dev/null +++ b/apps/dashboard/src/server/shared/openapi-schemas.ts @@ -0,0 +1,63 @@ +import type { FastifySchema } from "fastify"; + +export interface DocsOptions { + /** Module tag, e.g. "Welcome". Also used to group routes in Swagger UI. */ + tag: string; + /** Attach the session-cookie security requirement. Default: true. */ + secure?: boolean; + /** Success response schemas keyed by status code, e.g. `{ 200: { ... } }`. */ + response?: Record; +} + +/** + * Inline error-response schema shared by all routes. Inlined (rather than a + * `$ref`) so route schemas stay self-contained and resolve even in test apps + * that do not register the shared component schemas. + * + * `additionalProperties: true` keeps it serialization-safe: Fastify still + * serializes responses using these schemas, so anything extra the handler + * sends (e.g. `details`, `required`) is preserved instead of stripped. + */ +export const ErrorResponseInline = { + type: "object", + properties: { + error: { type: "string" }, + errorKey: { type: "string" }, + required: { type: "string" }, + details: { type: "object", additionalProperties: true }, + }, + additionalProperties: true, +} as const; + +/** + * Build a route `schema` with the standard documentation metadata: + * module tag, optional session-cookie security, and the common error + * responses (400/401/403/404/500). + * + * The base object may contain `body`, `params`, `querystring`, etc. + */ +export function withDocs( + base: Record | undefined, + opts: DocsOptions, +): FastifySchema { + const secure = opts.secure !== false; + const schema: Record = { + ...(base ?? {}), + tags: [opts.tag], + }; + + if (secure) { + schema.security = [{ sessionCookie: [] }]; + } + + schema.response = { + ...(opts.response ?? {}), + 400: ErrorResponseInline, + 401: ErrorResponseInline, + 403: ErrorResponseInline, + 404: ErrorResponseInline, + 500: ErrorResponseInline, + }; + + return schema as FastifySchema; +} diff --git a/apps/dashboard/src/server/shared/openapi.ts b/apps/dashboard/src/server/shared/openapi.ts new file mode 100644 index 0000000..f9d40a0 --- /dev/null +++ b/apps/dashboard/src/server/shared/openapi.ts @@ -0,0 +1,68 @@ +import type { FastifyInstance } from "fastify"; +import fastifySwagger from "@fastify/swagger"; +import fastifySwaggerUi from "@fastify/swagger-ui"; + +const MODULE_TAGS: { name: string; description: string }[] = [ + { name: "Meta", description: "Server-level metadata and health endpoints." }, + { name: "Auth", description: "Discord OAuth login, callback, session and logout." }, + { name: "Guilds", description: "Guild listing, metadata and manual refresh." }, + { name: "TempVoice", description: "Temporary voice channel configuration." }, + { name: "Actions", description: "Event-driven automation rules and actions." }, + { name: "Discord", description: "Discord-side lookups (channels, roles, guild state)." }, + { name: "Music", description: "Music player settings and queue management." }, + { name: "Logging", description: "Audit/event logging configuration." }, + { name: "Moderation", description: "Moderation settings and case management." }, + { name: "Warnings", description: "Warning issuance, listing and removal." }, + { name: "Welcome", description: "Welcome/farewell messages, images and auto-roles." }, + { name: "RolePanels", description: "Reaction/button role panel management." }, + { name: "Leveling", description: "XP/leveling configuration and rewards." }, + { name: "ScheduledMessages", description: "Scheduled announcement messages." }, + { name: "CustomCommands", description: "Server-defined custom slash commands." }, + { name: "AntiRaid", description: "Anti-raid / raid-protection configuration." }, + { name: "Tickets", description: "Support ticket system configuration." }, + { name: "Giveaways", description: "Giveaway creation and management." }, + { name: "Suggestions", description: "Suggestion system configuration." }, + { name: "Starboard", description: "Starboard configuration." }, + { name: "DashboardPermissions", description: "Dashboard staff permissions and roles." }, + { name: "DashboardRoles", description: "Dashboard role definitions and assignments." }, +]; + +/** + * Register the official Fastify Swagger plugins and the shared schema + * components. Must be called BEFORE routes are registered so the swagger + * `onRoute` hook captures every route. + */ +export async function registerOpenApi(app: FastifyInstance): Promise { + await app.register(fastifySwagger, { + openapi: { + info: { + title: "FluxCore Dashboard API", + description: + "HTTP API for the FluxCore Discord bot admin dashboard.\n\n" + + "Authentication uses a signed `session` cookie. Interactive \"Try it out\" " + + "for mutating endpoints may be blocked by CSRF protection; use GET endpoints " + + "to explore the API.", + version: "1.0.0", + }, + tags: MODULE_TAGS, + components: { + securitySchemes: { + sessionCookie: { + type: "apiKey", + in: "cookie", + name: "session", + }, + }, + }, + }, + }); + + await app.register(fastifySwaggerUi, { + routePrefix: "/docs", + uiConfig: { + docExpansion: "list", + deepLinking: true, + tryItOutEnabled: true, + }, + }); +} diff --git a/apps/dashboard/tests/server/openapi.test.ts b/apps/dashboard/tests/server/openapi.test.ts new file mode 100644 index 0000000..d1a8fe4 --- /dev/null +++ b/apps/dashboard/tests/server/openapi.test.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, vi, beforeAll } from "vitest"; +import Fastify, { type FastifyInstance } from "fastify"; + +vi.mock("@fluxcore/utils", () => ({ + logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +vi.mock("@fluxcore/config", () => ({ + config: { + token: "test-token", + clientId: "test-client-id", + dashboardClientSecret: "test-secret", + dashboardSessionSecret: "session-secret", + dashboardCallbackUrl: "http://localhost:3000/auth/callback", + dashboardPublicUrl: "http://localhost:3000", + dashboardPort: 3000, + }, +})); + +import { registerOpenApi } from "../../src/server/shared/openapi.js"; +import { registerAuthRoutes } from "../../src/server/features/auth/routes.js"; +import { registerGuildRoutes } from "../../src/server/features/guilds/routes.js"; +import { registerTempVoiceRoutes } from "../../src/server/features/tempvoice/routes.js"; +import { registerActionRoutes } from "../../src/server/features/actions/routes.js"; +import { registerDiscordRoutes } from "../../src/server/features/discord/routes.js"; +import { registerMusicRoutes } from "../../src/server/features/music/routes.js"; +import { registerLoggingRoutes } from "../../src/server/features/logging/routes.js"; +import { registerWarningRoutes } from "../../src/server/features/moderation/warnings-routes.js"; +import { registerModerationRoutes } from "../../src/server/features/moderation/routes.js"; +import { registerWelcomeRoutes } from "../../src/server/features/welcome/routes.js"; +import { registerRolePanelRoutes } from "../../src/server/features/roles/routes.js"; +import { registerLevelingRoutes } from "../../src/server/features/leveling/routes.js"; +import { registerScheduledMessageRoutes } from "../../src/server/features/scheduled/routes.js"; +import { registerCustomCommandRoutes } from "../../src/server/features/commands/routes.js"; +import { registerAntiRaidRoutes } from "../../src/server/features/security/routes.js"; +import { registerTicketRoutes } from "../../src/server/features/tickets/routes.js"; +import { registerGiveawayRoutes } from "../../src/server/features/giveaways/routes.js"; +import { registerSuggestionRoutes } from "../../src/server/features/suggestions/routes.js"; +import { registerStarboardRoutes } from "../../src/server/features/starboard/routes.js"; +import { registerDashboardRoleRoutes } from "../../src/server/features/permissions/roles-routes.js"; +import { registerDashboardPermissionRoutes } from "../../src/server/features/permissions/routes.js"; + +const EXPECTED_TAGS = [ + "Meta", "Auth", "Guilds", "TempVoice", "Actions", "Discord", "Music", + "Logging", "Moderation", "Warnings", "Welcome", "RolePanels", "Leveling", + "ScheduledMessages", "CustomCommands", "AntiRaid", "Tickets", "Giveaways", + "Suggestions", "Starboard", "DashboardPermissions", "DashboardRoles", +]; + +let app: FastifyInstance; + +beforeAll(async () => { + app = Fastify(); + await registerOpenApi(app); + registerAuthRoutes(app); + registerGuildRoutes(app); + registerTempVoiceRoutes(app); + registerActionRoutes(app); + registerDiscordRoutes(app); + registerMusicRoutes(app); + registerLoggingRoutes(app); + registerWarningRoutes(app); + registerModerationRoutes(app); + registerWelcomeRoutes(app); + registerRolePanelRoutes(app); + registerLevelingRoutes(app); + registerScheduledMessageRoutes(app); + registerCustomCommandRoutes(app); + registerAntiRaidRoutes(app); + registerTicketRoutes(app); + registerGiveawayRoutes(app); + registerSuggestionRoutes(app); + registerStarboardRoutes(app); + registerDashboardRoleRoutes(app); + registerDashboardPermissionRoutes(app); + await app.ready(); +}); + +describe("OpenAPI documentation", () => { + it("serves the Swagger UI at /docs", async () => { + const res = await app.inject({ method: "GET", url: "/docs" }); + expect(res.statusCode).toBe(200); + }); + + it("exposes a valid OpenAPI document at /docs/json", async () => { + const res = await app.inject({ method: "GET", url: "/docs/json" }); + expect(res.statusCode).toBe(200); + const doc = res.json(); + expect(doc.openapi).toBeDefined(); + expect(doc.paths).toBeDefined(); + }); + + it("declares the session-cookie security scheme", async () => { + const doc = (await app.inject({ method: "GET", url: "/docs/json" })).json(); + expect(doc.components?.securitySchemes?.sessionCookie).toMatchObject({ + type: "apiKey", + in: "cookie", + name: "session", + }); + }); + + it("documents every module under its own tag", async () => { + const doc = (await app.inject({ method: "GET", url: "/docs/json" })).json(); + const tagNames = doc.tags.map((t: { name: string }) => t.name).sort(); + for (const tag of EXPECTED_TAGS) { + expect(tagNames).toContain(tag); + } + }); + + it("tags every route and assigns no route to a missing tag", async () => { + const doc = (await app.inject({ method: "GET", url: "/docs/json" })).json(); + const declared = new Set(doc.tags.map((t: { name: string }) => t.name)); + const untagged: string[] = []; + for (const [path, methods] of Object.entries< + Record + >(doc.paths)) { + for (const op of Object.values(methods)) { + const tags = op.tags ?? []; + if (tags.length === 0) untagged.push(path); + for (const t of tags) { + if (!declared.has(t)) untagged.push(`${path} (${t})`); + } + } + } + expect(untagged).toEqual([]); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0238fd..7ae0732 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 25.3.0 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0)) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -76,7 +76,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0) + version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) apps/dashboard: dependencies: @@ -92,6 +92,12 @@ importers: '@fastify/static': specifier: ^9.0.0 version: 9.0.0 + '@fastify/swagger': + specifier: ^9.8.0 + version: 9.8.0 + '@fastify/swagger-ui': + specifier: ^6.1.0 + version: 6.1.0 '@fluxcore/config': specifier: workspace:* version: link:../../packages/config @@ -221,7 +227,7 @@ importers: version: 1.59.1 '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.2.1(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + version: 4.2.1(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.9.1 @@ -242,10 +248,10 @@ importers: version: 1.1.6 '@vitejs/plugin-react': specifier: ^4.5.2 - version: 4.7.0(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + version: 4.7.0(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0)) concurrently: specifier: ^9.1.2 version: 9.2.1 @@ -263,10 +269,10 @@ importers: version: 5.9.3 vite: specifier: ^6.3.5 - version: 6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + version: 6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0) + version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) packages/config: dependencies: @@ -366,13 +372,13 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)) + version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0) + version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) packages/types: dependencies: @@ -959,6 +965,15 @@ packages: '@fastify/static@9.0.0': resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==} + '@fastify/static@9.3.0': + resolution: {integrity: sha512-9YMYRpCOtMBrqKYWcqiw7ykOrn4D0jogHpJrFS0KGeSuOwzKMM5/mjj7B0CFLVoQ6htqKYw//Zs7APn9DBq05w==} + + '@fastify/swagger-ui@6.1.0': + resolution: {integrity: sha512-vbhHlJvzXujGco+6yumjt4cwptzb+0ZezPvApFSI+62IdJCfHtjpJrFIT+ln3BCB+KVFiUkI1Y+L9adIGmvdHQ==} + + '@fastify/swagger@9.8.0': + resolution: {integrity: sha512-GdRkUboXu++nChrmWM22SNDkabB529TL7cQ4RDsPDP76ro1kSW1cLIEZ9S8ZUbJKYUciHgLAgiX8SHzCnqZdnQ==} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2508,6 +2523,9 @@ packages: fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + fastify-plugin@6.0.0: + resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} + fastify@5.10.0: resolution: {integrity: sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==} @@ -2721,6 +2739,10 @@ packages: json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-resolver@3.0.0: + resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==} + engines: {node: '>=20'} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -2818,10 +2840,6 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - lru-cache@11.2.6: - resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} - engines: {node: 20 || >=22} - lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -2945,6 +2963,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -3782,6 +3803,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4269,6 +4295,33 @@ snapshots: fastq: 1.20.1 glob: 13.0.6 + '@fastify/static@9.3.0': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 1.0.1 + fastify-plugin: 6.0.0 + fastq: 1.20.1 + glob: 13.0.6 + + '@fastify/swagger-ui@6.1.0': + dependencies: + '@fastify/static': 9.3.0 + fastify-plugin: 6.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.9.0 + + '@fastify/swagger@9.8.0': + dependencies: + fastify-plugin: 6.0.0 + json-schema-resolver: 3.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.9.0 + transitivePeerDependencies: + - supports-color + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -5119,12 +5172,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 - '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + vite: 6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) '@tanstack/history@1.161.4': {} @@ -5296,7 +5349,7 @@ snapshots: dependencies: '@types/node': 25.3.0 - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -5304,11 +5357,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + vite: 6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -5320,7 +5373,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0) + vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) '@vitest/expect@4.0.18': dependencies: @@ -5331,13 +5384,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -5861,6 +5914,8 @@ snapshots: fastify-plugin@5.1.0: {} + fastify-plugin@6.0.0: {} + fastify@5.10.0: dependencies: '@fastify/ajv-compiler': 4.0.5 @@ -6079,6 +6134,14 @@ snapshots: dependencies: dequal: 2.0.3 + json-schema-resolver@3.0.0: + dependencies: + debug: 4.4.3 + fast-uri: 4.1.0 + rfdc: 1.4.1 + transitivePeerDependencies: + - supports-color + json-schema-traverse@1.0.0: {} json5@2.2.3: {} @@ -6148,8 +6211,6 @@ snapshots: long@5.3.2: {} - lru-cache@11.2.6: {} - lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -6256,6 +6317,8 @@ snapshots: wrappy: 1.0.2 optional: true + openapi-types@12.1.3: {} + parse5@8.0.1: dependencies: entities: 8.0.0 @@ -6264,7 +6327,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.2.6 + lru-cache: 11.5.2 minipass: 7.1.3 pathe@2.0.3: {} @@ -6887,7 +6950,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): + vite@6.4.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -6901,8 +6964,9 @@ snapshots: jiti: 2.6.1 lightningcss: 1.31.1 tsx: 4.21.0 + yaml: 2.9.0 - vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): + vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -6916,11 +6980,12 @@ snapshots: jiti: 2.6.1 lightningcss: 1.31.1 tsx: 4.21.0 + yaml: 2.9.0 - vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0): + vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.1.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -6937,7 +7002,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.3.0 @@ -7010,6 +7075,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: