From 3df6818ffeadb89334bb57f334a16ce645b6f833 Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Sat, 10 May 2025 15:16:26 -0400 Subject: [PATCH] feat: openapi support plus more api validation --- nuxt.config.ts | 20 ++++++++- .../v1/admin/auth/invitation/index.delete.ts | 22 +++++++--- .../v1/admin/auth/invitation/index.post.ts | 41 ++++++++----------- .../api/v1/admin/game/image/index.delete.ts | 23 ++++++++--- server/api/v1/auth/signin/simple.post.ts | 4 +- server/api/v1/auth/signup/simple.post.ts | 5 ++- server/routes/auth/callback/oidc.get.ts | 8 ++++ server/routes/auth/oidc.get.ts | 8 ++++ server/routes/auth/signout.get.ts | 8 ++++ 9 files changed, 100 insertions(+), 39 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index e4e014d..440e1b5 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,5 +1,7 @@ import tailwindcss from "@tailwindcss/vite"; +const dropVersion = "0.3"; + // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ // Nuxt-only config @@ -31,24 +33,38 @@ export default defineNuxtConfig({ }, }, + appConfig: { + dropVersion: dropVersion, + }, + routeRules: { "/api/**": { cors: true }, }, nitro: { minify: true, + compressPublicAssets: true, experimental: { websocket: true, tasks: true, + openAPI: true, + }, + + openAPI: { + // tracking for dynamic openapi schema https://github.com/nitrojs/nitro/issues/2974 + meta: { + title: "Drop", + description: + "Drop is an open-source, self-hosted game distribution platform, creating a Steam-like experience for DRM-free games.", + version: dropVersion, + }, }, scheduledTasks: { "0 * * * *": ["cleanup:invitations", "cleanup:sessions"], }, - compressPublicAssets: true, - storage: { appCache: { driver: "lru-cache", diff --git a/server/api/v1/admin/auth/invitation/index.delete.ts b/server/api/v1/admin/auth/invitation/index.delete.ts index 381dea3..34d24bd 100644 --- a/server/api/v1/admin/auth/invitation/index.delete.ts +++ b/server/api/v1/admin/auth/invitation/index.delete.ts @@ -1,20 +1,30 @@ +import { type } from "arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; -export default defineEventHandler(async (h3) => { +const DeleteInvite = type({ + id: "string", +}); + +export default defineEventHandler<{ + body: typeof DeleteInvite.infer; +}>(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ "auth:simple:invitation:delete", ]); if (!allowed) throw createError({ statusCode: 403 }); - const body = await readBody(h3); - const id = body.id; - if (!id) + const body = DeleteInvite(await readBody(h3)); + if (body instanceof type.errors) { + // hover out.summary to see validation errors + console.error(body.summary); + throw createError({ statusCode: 400, - statusMessage: "id required for deletion", + statusMessage: body.summary, }); + } - await prisma.invitation.delete({ where: { id: id } }); + await prisma.invitation.delete({ where: { id: body.id } }); return {}; }); diff --git a/server/api/v1/admin/auth/invitation/index.post.ts b/server/api/v1/admin/auth/invitation/index.post.ts index 015557e..58305c2 100644 --- a/server/api/v1/admin/auth/invitation/index.post.ts +++ b/server/api/v1/admin/auth/invitation/index.post.ts @@ -1,40 +1,35 @@ +import { type } from "arktype"; import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; -export default defineEventHandler(async (h3) => { +const CreateInvite = type({ + isAdmin: "boolean", + username: "string", + email: "string.email", + expires: "string.date.iso.parse", +}); + +export default defineEventHandler<{ + body: typeof CreateInvite.infer; +}>(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ "auth:simple:invitation:new", ]); if (!allowed) throw createError({ statusCode: 403 }); - const body = await readBody(h3); - const isAdmin = body.isAdmin; - const username = body.username; - const email = body.email; - const expires = body.expires; + const body = CreateInvite(await readBody(h3)); + if (body instanceof type.errors) { + // hover out.summary to see validation errors + console.error(body.summary); - if (!expires) - throw createError({ statusCode: 400, statusMessage: "No expires field." }); - if (isAdmin !== undefined && typeof isAdmin !== "boolean") throw createError({ statusCode: 400, - statusMessage: "isAdmin must be a boolean", - }); - - const expiresDate = new Date(expires); - if (!(expiresDate instanceof Date && !isNaN(expiresDate.getTime()))) - throw createError({ - statusCode: 400, - statusMessage: "Invalid expires date", + statusMessage: body.summary, }); + } const invitation = await prisma.invitation.create({ - data: { - isAdmin: isAdmin, - username: username, - email: email, - expires: expiresDate, - }, + data: body, }); return invitation; diff --git a/server/api/v1/admin/game/image/index.delete.ts b/server/api/v1/admin/game/image/index.delete.ts index 3558584..9d9754e 100644 --- a/server/api/v1/admin/game/image/index.delete.ts +++ b/server/api/v1/admin/game/image/index.delete.ts @@ -1,20 +1,31 @@ import aclManager from "~/server/internal/acls"; import prisma from "~/server/internal/db/database"; import objectHandler from "~/server/internal/objects"; +import { type } from "arktype"; -export default defineEventHandler(async (h3) => { +const ModifyGameImage = type({ + gameId: "string", + imageId: "string", +}); + +export default defineEventHandler<{ + body: typeof ModifyGameImage.infer; +}>(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, ["game:image:delete"]); if (!allowed) throw createError({ statusCode: 403 }); - const body = await readBody(h3); - const gameId = body.gameId; - const imageId = body.imageId; + const body = ModifyGameImage(await readBody(h3)); + if (body instanceof type.errors) { + // hover out.summary to see validation errors + console.error(body.summary); - if (!gameId || !imageId) throw createError({ statusCode: 400, - statusMessage: "Missing gameId or imageId in body", + statusMessage: body.summary, }); + } + const gameId = body.gameId; + const imageId = body.imageId; const game = await prisma.game.findUnique({ where: { diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts index 42eacdf..f184ca8 100644 --- a/server/api/v1/auth/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -15,7 +15,9 @@ const signinValidator = type({ "rememberMe?": "boolean | undefined", }); -export default defineEventHandler(async (h3) => { +export default defineEventHandler<{ + body: typeof signinValidator.infer; +}>(async (h3) => { if (!enabledAuthManagers.Simple) throw createError({ statusCode: 403, diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index ade27c1..5541ec3 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -7,13 +7,16 @@ import { type } from "arktype"; import { randomUUID } from "node:crypto"; const userValidator = type({ + invitation: "string", username: "string >= 5", email: "string.email", password: "string >= 14", "displayName?": "string | undefined", }); -export default defineEventHandler(async (h3) => { +export default defineEventHandler<{ + body: typeof userValidator.infer; +}>(async (h3) => { const body = await readBody(h3); const invitationId = body.invitation; diff --git a/server/routes/auth/callback/oidc.get.ts b/server/routes/auth/callback/oidc.get.ts index e2a6854..3d7d27f 100644 --- a/server/routes/auth/callback/oidc.get.ts +++ b/server/routes/auth/callback/oidc.get.ts @@ -1,6 +1,14 @@ import sessionHandler from "~/server/internal/session"; import { enabledAuthManagers } from "~/server/plugins/04.auth-init"; +defineRouteMeta({ + openAPI: { + tags: ["Auth"], + description: "OIDC Signin callback", + parameters: [], + }, +}); + export default defineEventHandler(async (h3) => { if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin"); diff --git a/server/routes/auth/oidc.get.ts b/server/routes/auth/oidc.get.ts index be3cf94..d655fce 100644 --- a/server/routes/auth/oidc.get.ts +++ b/server/routes/auth/oidc.get.ts @@ -1,5 +1,13 @@ import { enabledAuthManagers } from "~/server/plugins/04.auth-init"; +defineRouteMeta({ + openAPI: { + tags: ["Auth"], + description: "OIDC Signin redirect", + parameters: [], + }, +}); + export default defineEventHandler((h3) => { if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin"); diff --git a/server/routes/auth/signout.get.ts b/server/routes/auth/signout.get.ts index ebeb6f7..3f0b277 100644 --- a/server/routes/auth/signout.get.ts +++ b/server/routes/auth/signout.get.ts @@ -1,5 +1,13 @@ import sessionHandler from "../../internal/session"; +defineRouteMeta({ + openAPI: { + tags: ["Auth"], + description: "Tells server to deauthorize this session", + parameters: [], + }, +}); + export default defineEventHandler(async (h3) => { await sessionHandler.signout(h3);