diff --git a/server/api/v1/client/auth/callback/index.post.ts b/server/api/v1/client/auth/callback/index.post.ts index bc63be7..c46b16c 100644 --- a/server/api/v1/client/auth/callback/index.post.ts +++ b/server/api/v1/client/auth/callback/index.post.ts @@ -1,30 +1,41 @@ +import { type } from "arktype"; +import { readDropValidatedBody, throwingArktype } from "~/server/arktype"; +import aclManager from "~/server/internal/acls"; import clientHandler from "~/server/internal/clients/handler"; -import sessionHandler from "~/server/internal/session"; -export default defineEventHandler(async (h3) => { - const user = await sessionHandler.getSession(h3); - if (!user) throw createError({ statusCode: 403 }); +const AuthorizeBody = type({ + id: "string", +}).configure(throwingArktype); - const body = await readBody(h3); - const clientId = await body.id; +/** + * Finalize the authorization for a client + */ +export default defineEventHandler<{ body: typeof AuthorizeBody.infer }>( + async (h3) => { + const userId = await aclManager.getUserIdACL(h3, []); + if (!userId) throw createError({ statusCode: 403 }); - const client = await clientHandler.fetchClient(clientId); - if (!client) - throw createError({ - statusCode: 400, - statusMessage: "Invalid or expired client ID.", - }); + const body = await readDropValidatedBody(h3, AuthorizeBody); + const clientId = body.id; - if (client.userId != user.userId) - throw createError({ - statusCode: 403, - statusMessage: "Not allowed to authorize this client.", - }); + const client = await clientHandler.fetchClient(clientId); + if (!client) + throw createError({ + statusCode: 400, + statusMessage: "Invalid or expired client ID.", + }); - const token = await clientHandler.generateAuthToken(clientId); + if (client.userId != userId) + throw createError({ + statusCode: 403, + statusMessage: "Not allowed to authorize this client.", + }); - return { - redirect: `drop://handshake/${clientId}/${token}`, - token: `${clientId}/${token}`, - }; -}); + const token = await clientHandler.generateAuthToken(clientId); + + return { + redirect: `drop://handshake/${clientId}/${token}`, + token: `${clientId}/${token}`, + }; + }, +); diff --git a/server/api/v1/client/auth/code/index.get.ts b/server/api/v1/client/auth/code/index.get.ts index 74bc6e8..6dea3fd 100644 --- a/server/api/v1/client/auth/code/index.get.ts +++ b/server/api/v1/client/auth/code/index.get.ts @@ -1,17 +1,22 @@ +import { ArkErrors, type } from "arktype"; +import aclManager from "~/server/internal/acls"; import clientHandler from "~/server/internal/clients/handler"; -import sessionHandler from "~/server/internal/session"; -export default defineEventHandler(async (h3) => { - const user = await sessionHandler.getSession(h3); - if (!user) throw createError({ statusCode: 403 }); +const Query = type({ + code: "string.upper", +}); - const query = getQuery(h3); - const code = query.code?.toString()?.toUpperCase(); - if (!code) - throw createError({ - statusCode: 400, - statusMessage: "Code required in query params.", - }); +/** + * Fetch client ID by authorize code + */ +export default defineEventHandler<{ query: typeof Query.infer }>(async (h3) => { + const userId = await aclManager.getUserIdACL(h3, []); + if (!userId) throw createError({ statusCode: 403 }); + + const query = Query(getQuery(h3)); + if (query instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: query.summary }); + const code = query.code; const clientId = await clientHandler.fetchClientIdByCode(code); if (!clientId) diff --git a/server/api/v1/client/auth/code/index.post.ts b/server/api/v1/client/auth/code/index.post.ts index 593b773..8ca7a3e 100644 --- a/server/api/v1/client/auth/code/index.post.ts +++ b/server/api/v1/client/auth/code/index.post.ts @@ -1,35 +1,46 @@ +import { type } from "arktype"; +import { readDropValidatedBody, throwingArktype } from "~/server/arktype"; +import aclManager from "~/server/internal/acls"; import clientHandler from "~/server/internal/clients/handler"; -import sessionHandler from "~/server/internal/session"; -export default defineEventHandler(async (h3) => { - const user = await sessionHandler.getSession(h3); - if (!user) throw createError({ statusCode: 403 }); +const CodeAuthorize = type({ + id: "string", +}).configure(throwingArktype); - const body = await readBody(h3); - const clientId = await body.id; +/** + * Authorize code by client ID, and send token via WS to client + */ +export default defineEventHandler<{ body: typeof CodeAuthorize.infer }>( + async (h3) => { + const userId = await aclManager.getUserIdACL(h3, []); + if (!userId) throw createError({ statusCode: 403 }); - const client = await clientHandler.fetchClient(clientId); - if (!client) - throw createError({ - statusCode: 400, - statusMessage: "Invalid or expired client ID.", - }); + const body = await readDropValidatedBody(h3, CodeAuthorize); + const clientId = body.id; - if (client.userId != user.userId) - throw createError({ - statusCode: 403, - statusMessage: "Not allowed to authorize this client.", - }); + const client = await clientHandler.fetchClient(clientId); + if (!client) + throw createError({ + statusCode: 400, + statusMessage: "Invalid or expired client ID.", + }); - if (!client.peer) - throw createError({ - statusCode: 500, - statusMessage: "No client listening for authorization.", - }); + if (client.userId != userId) + throw createError({ + statusCode: 403, + statusMessage: "Not allowed to authorize this client.", + }); - const token = await clientHandler.generateAuthToken(clientId); + if (!client.peer) + throw createError({ + statusCode: 500, + statusMessage: "No client listening for authorization.", + }); - await clientHandler.sendAuthToken(clientId, token); + const token = await clientHandler.generateAuthToken(clientId); - return; -}); + await clientHandler.sendAuthToken(clientId, token); + + return; + }, +); diff --git a/server/api/v1/client/auth/code/ws.get.ts b/server/api/v1/client/auth/code/ws.get.ts index 637d8df..569fea2 100644 --- a/server/api/v1/client/auth/code/ws.get.ts +++ b/server/api/v1/client/auth/code/ws.get.ts @@ -1,6 +1,10 @@ import type { FetchError } from "ofetch"; import clientHandler from "~/server/internal/clients/handler"; +/** + * Client route to listen for code authorization. + * @request Pass the code in the `Authorization` header + */ export default defineWebSocketHandler({ async open(peer) { try { diff --git a/server/api/v1/client/auth/handshake.post.ts b/server/api/v1/client/auth/handshake.post.ts index c154c8c..af1613f 100644 --- a/server/api/v1/client/auth/handshake.post.ts +++ b/server/api/v1/client/auth/handshake.post.ts @@ -1,45 +1,52 @@ +import { type } from "arktype"; +import { readDropValidatedBody, throwingArktype } from "~/server/arktype"; import clientHandler from "~/server/internal/clients/handler"; import { useCertificateAuthority } from "~/server/plugins/ca"; -export default defineEventHandler(async (h3) => { - const body = await readBody(h3); - const clientId = body.clientId; - const token = body.token; - if (!clientId || !token) - throw createError({ - statusCode: 400, - statusMessage: "Missing token or client ID from body", - }); +const HandshakeBody = type({ + clientId: "string", + token: "string", +}).configure(throwingArktype); - const metadata = await clientHandler.fetchClient(clientId); - if (!metadata) - throw createError({ - statusCode: 403, - statusMessage: "Invalid client ID", - }); - if (!metadata.authToken || !metadata.userId) - throw createError({ - statusCode: 400, - statusMessage: "Un-authorized client ID", - }); - if (metadata.authToken !== token) - throw createError({ - statusCode: 403, - statusMessage: "Invalid token", - }); +/** + * Client route to complete handshake, after the user has authorize it. + */ +export default defineEventHandler<{ body: typeof HandshakeBody.infer }>( + async (h3) => { + const body = await readDropValidatedBody(h3, HandshakeBody); + const clientId = body.clientId; + const token = body.token; - const certificateAuthority = useCertificateAuthority(); - const bundle = await certificateAuthority.generateClientCertificate( - clientId, - metadata.data.name, - ); + const metadata = await clientHandler.fetchClient(clientId); + if (!metadata) + throw createError({ + statusCode: 403, + statusMessage: "Invalid client ID", + }); + if (!metadata.authToken || !metadata.userId) + throw createError({ + statusCode: 400, + statusMessage: "Un-authorized client ID", + }); + if (metadata.authToken !== token) + throw createError({ + statusCode: 403, + statusMessage: "Invalid token", + }); - const client = await clientHandler.finialiseClient(clientId); - await certificateAuthority.storeClientCertificate(clientId, bundle); + const certificateAuthority = useCertificateAuthority(); + const bundle = await certificateAuthority.generateClientCertificate( + clientId, + metadata.data.name, + ); - return { - private: bundle.priv, - certificate: bundle.cert, - id: client.id, - }; -}); + const client = await clientHandler.finialiseClient(clientId); + await certificateAuthority.storeClientCertificate(clientId, bundle); + + return { + private: bundle.priv, + certificate: bundle.cert, + id: client.id, + }; + }, +); diff --git a/server/api/v1/client/auth/index.get.ts b/server/api/v1/client/auth/index.get.ts index 92cf16c..a48713b 100644 --- a/server/api/v1/client/auth/index.get.ts +++ b/server/api/v1/client/auth/index.get.ts @@ -1,17 +1,22 @@ +import { ArkErrors, type } from "arktype"; +import aclManager from "~/server/internal/acls"; import clientHandler from "~/server/internal/clients/handler"; -import sessionHandler from "~/server/internal/session"; -export default defineEventHandler(async (h3) => { - const user = await sessionHandler.getSession(h3); - if (!user) throw createError({ statusCode: 403 }); +const Query = type({ + id: "string", +}); - const query = getQuery(h3); - const providedClientId = query.id?.toString(); - if (!providedClientId) - throw createError({ - statusCode: 400, - statusMessage: "Provide client ID in request params as 'id'", - }); +/** + * Fetch details about an authorization request, and claim it for the current user + */ +export default defineEventHandler<{ query: typeof Query.infer }>(async (h3) => { + const userId = await aclManager.getUserIdACL(h3, []); + if (!userId) throw createError({ statusCode: 403 }); + + const query = Query(getQuery(h3)); + if (query instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: query.summary }); + const providedClientId = query.id; const client = await clientHandler.fetchClient(providedClientId); if (!client) @@ -20,13 +25,13 @@ export default defineEventHandler(async (h3) => { statusMessage: "Request not found.", }); - if (client.userId && user.userId !== client.userId) + if (client.userId && userId !== client.userId) throw createError({ statusCode: 400, statusMessage: "Client already claimed.", }); - await clientHandler.attachUserId(providedClientId, user.userId); + await clientHandler.attachUserId(providedClientId, userId); return client.data; }); diff --git a/server/api/v1/client/auth/initiate.post.ts b/server/api/v1/client/auth/initiate.post.ts index 64186b5..f3070ca 100644 --- a/server/api/v1/client/auth/initiate.post.ts +++ b/server/api/v1/client/auth/initiate.post.ts @@ -17,55 +17,61 @@ const ClientAuthInitiate = type({ mode: type.valueOf(AuthMode).default(AuthMode.Callback), }).configure(throwingArktype); -export default defineEventHandler(async (h3) => { - const body = await readDropValidatedBody(h3, ClientAuthInitiate); +/** + * Client route to initiate authorization flow. + * @response The requested callback or code. + */ +export default defineEventHandler<{ body: typeof ClientAuthInitiate.infer }>( + async (h3) => { + const body = await readDropValidatedBody(h3, ClientAuthInitiate); - const platformRaw = body.platform; - const capabilities: Partial = - body.capabilities ?? {}; + const platformRaw = body.platform; + const capabilities: Partial = + body.capabilities ?? {}; - const platform = parsePlatform(platformRaw); - if (!platform) - throw createError({ - statusCode: 400, - statusMessage: "Invalid or unsupported platform", + const platform = parsePlatform(platformRaw); + if (!platform) + throw createError({ + statusCode: 400, + statusMessage: "Invalid or unsupported platform", + }); + + const capabilityIterable = Object.entries(capabilities) as Array< + [InternalClientCapability, object] + >; + if ( + capabilityIterable.length > 0 && + capabilityIterable + .map(([capability]) => validCapabilities.find((v) => capability == v)) + .filter((e) => e).length == 0 + ) + throw createError({ + statusCode: 400, + statusMessage: "Invalid capabilities.", + }); + + if ( + capabilityIterable.length > 0 && + capabilityIterable.filter( + ([capability, configuration]) => + !capabilityManager.validateCapabilityConfiguration( + capability, + configuration, + ), + ).length > 0 + ) + throw createError({ + statusCode: 400, + statusMessage: "Invalid capability configuration.", + }); + + const result = await clientHandler.initiate({ + name: body.name, + platform, + capabilities, + mode: body.mode, }); - const capabilityIterable = Object.entries(capabilities) as Array< - [InternalClientCapability, object] - >; - if ( - capabilityIterable.length > 0 && - capabilityIterable - .map(([capability]) => validCapabilities.find((v) => capability == v)) - .filter((e) => e).length == 0 - ) - throw createError({ - statusCode: 400, - statusMessage: "Invalid capabilities.", - }); - - if ( - capabilityIterable.length > 0 && - capabilityIterable.filter( - ([capability, configuration]) => - !capabilityManager.validateCapabilityConfiguration( - capability, - configuration, - ), - ).length > 0 - ) - throw createError({ - statusCode: 400, - statusMessage: "Invalid capability configuration.", - }); - - const result = await clientHandler.initiate({ - name: body.name, - platform, - capabilities, - mode: body.mode, - }); - - return result; -}); + return result; + }, +); diff --git a/server/api/v1/client/auth/session.post.ts b/server/api/v1/client/auth/session.post.ts deleted file mode 100644 index 1ff0ebd..0000000 --- a/server/api/v1/client/auth/session.post.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler((_h3) => {}); diff --git a/server/api/v1/client/capability/index.post.ts b/server/api/v1/client/capability/index.post.ts deleted file mode 100644 index a33f690..0000000 --- a/server/api/v1/client/capability/index.post.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { InternalClientCapability } from "~/server/internal/clients/capabilities"; -import capabilityManager, { - validCapabilities, -} from "~/server/internal/clients/capabilities"; -import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; -import notificationSystem from "~/server/internal/notifications"; - -export default defineClientEventHandler( - async (h3, { clientId, fetchClient, fetchUser }) => { - const body = await readBody(h3); - const rawCapability = body.capability; - const configuration = body.configuration; - - if (!rawCapability || typeof rawCapability !== "string") - throw createError({ - statusCode: 400, - statusMessage: "capability must be a string", - }); - - if (!configuration || typeof configuration !== "object") - throw createError({ - statusCode: 400, - statusMessage: "configuration must be an object", - }); - - const capability = rawCapability as InternalClientCapability; - - if (!validCapabilities.includes(capability)) - throw createError({ - statusCode: 400, - statusMessage: "Invalid capability.", - }); - - const isValid = await capabilityManager.validateCapabilityConfiguration( - capability, - configuration, - ); - if (!isValid) - throw createError({ - statusCode: 400, - statusMessage: "Invalid capability configuration.", - }); - - await capabilityManager.upsertClientCapability( - capability, - configuration, - clientId, - ); - - const client = await fetchClient(); - const user = await fetchUser(); - - await notificationSystem.push(user.id, { - nonce: `capability-${clientId}-${capability}`, - title: `"${client.name}" can now access ${capability}`, - description: `A device called "${client.name}" now has access to your ${capability}.`, - actions: ["Review|/account/devices"], - acls: ["user:clients:read"], - }); - - return {}; - }, -); diff --git a/server/api/v1/client/chunk.get.ts b/server/api/v1/client/chunk.get.ts index 030b796..adebad6 100644 --- a/server/api/v1/client/chunk.get.ts +++ b/server/api/v1/client/chunk.get.ts @@ -1,3 +1,4 @@ +import { ArkErrors, type } from "arktype"; import cacheHandler from "~/server/internal/cache"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import prisma from "~/server/internal/db/database"; @@ -10,74 +11,93 @@ const gameLookupCache = cacheHandler.createCache<{ libraryPath: string; }>("downloadGameLookupCache"); -export default defineClientEventHandler(async (h3) => { - const query = getQuery(h3); - const gameId = query.id?.toString(); - const versionName = query.version?.toString(); - const filename = query.name?.toString(); - const chunkIndex = parseInt(query.chunk?.toString() ?? "?"); - - if (!gameId || !versionName || !filename || Number.isNaN(chunkIndex)) - throw createError({ - statusCode: 400, - statusMessage: "Invalid chunk arguments", - }); - - let game = await gameLookupCache.getItem(gameId); - if (!game) { - game = await prisma.game.findUnique({ - where: { - id: gameId, - }, - select: { - libraryId: true, - libraryPath: true, - }, - }); - if (!game || !game.libraryId) - throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); - - await gameLookupCache.setItem(gameId, game); - } - - if (!game.libraryId) - throw createError({ - statusCode: 500, - statusMessage: "Somehow, we got here.", - }); - - const peek = await libraryManager.peekFile( - game.libraryId, - game.libraryPath, - versionName, - filename, - ); - if (!peek) - throw createError({ status: 400, statusMessage: "Failed to peek file" }); - - const start = chunkIndex * chunkSize; - const end = Math.min((chunkIndex + 1) * chunkSize, peek.size); - const currentChunkSize = end - start; - setHeader(h3, "Content-Length", currentChunkSize); - - if (start >= end) - throw createError({ - statusCode: 400, - statusMessage: "Invalid chunk index", - }); - - const gameReadStream = await libraryManager.readFile( - game.libraryId, - game.libraryPath, - versionName, - filename, - { start, end }, - ); - if (!gameReadStream) - throw createError({ - statusCode: 400, - statusMessage: "Failed to create stream", - }); - - return sendStream(h3, gameReadStream); +const Query = type({ + id: "string", + version: "string", + name: "string", + chunk: "string.numeric.parse", }); + +/** + * v1 download API + * @deprecated + * @response Raw binary data (`application/octet-stream`) + */ +export default defineClientEventHandler( + async (h3) => { + const query = Query(getQuery(h3)); + if (query instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: query.summary }); + const gameId = query.id; + const versionName = query.version; + const filename = query.name; + const chunkIndex = query.chunk; + + if (!gameId || !versionName || !filename || Number.isNaN(chunkIndex)) + throw createError({ + statusCode: 400, + statusMessage: "Invalid chunk arguments", + }); + + let game = await gameLookupCache.getItem(gameId); + if (!game) { + game = await prisma.game.findUnique({ + where: { + id: gameId, + }, + select: { + libraryId: true, + libraryPath: true, + }, + }); + if (!game || !game.libraryId) + throw createError({ + statusCode: 400, + statusMessage: "Invalid game ID", + }); + + await gameLookupCache.setItem(gameId, game); + } + + if (!game.libraryId) + throw createError({ + statusCode: 500, + statusMessage: "Somehow, we got here.", + }); + + const peek = await libraryManager.peekFile( + game.libraryId, + game.libraryPath, + versionName, + filename, + ); + if (!peek) + throw createError({ status: 400, statusMessage: "Failed to peek file" }); + + const start = chunkIndex * chunkSize; + const end = Math.min((chunkIndex + 1) * chunkSize, peek.size); + const currentChunkSize = end - start; + setHeader(h3, "Content-Length", currentChunkSize); + + if (start >= end) + throw createError({ + statusCode: 400, + statusMessage: "Invalid chunk index", + }); + + const gameReadStream = await libraryManager.readFile( + game.libraryId, + game.libraryPath, + versionName, + filename, + { start, end }, + ); + if (!gameReadStream) + throw createError({ + statusCode: 400, + statusMessage: "Failed to create stream", + }); + + return sendStream(h3, gameReadStream); + }, +); diff --git a/server/api/v1/client/collection/[id]/entry.delete.ts b/server/api/v1/client/collection/[id]/entry.delete.ts index 204e133..0a8a000 100644 --- a/server/api/v1/client/collection/[id]/entry.delete.ts +++ b/server/api/v1/client/collection/[id]/entry.delete.ts @@ -1,7 +1,17 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; -export default defineClientEventHandler(async (h3, { fetchUser }) => { +const EntryRemove = type({ + id: "string", +}).configure(throwingArktype); + +/** + * Remove entry from user's collection + * @param id Collection ID + */ +export default defineClientEventHandler(async (h3, { fetchUser, body }) => { const user = await fetchUser(); const id = getRouterParam(h3, "id"); @@ -11,10 +21,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { statusMessage: "ID required in route params", }); - const body = await readBody(h3); const gameId = body.id; - if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); const successful = await userLibraryManager.collectionRemove( gameId, @@ -27,4 +34,4 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { statusMessage: "Collection not found", }); return {}; -}); +}, EntryRemove); diff --git a/server/api/v1/client/collection/[id]/entry.post.ts b/server/api/v1/client/collection/[id]/entry.post.ts index 357e236..b10b93a 100644 --- a/server/api/v1/client/collection/[id]/entry.post.ts +++ b/server/api/v1/client/collection/[id]/entry.post.ts @@ -1,7 +1,17 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; -export default defineClientEventHandler(async (h3, { fetchUser }) => { +const AddEntry = type({ + id: "string", +}).configure(throwingArktype); + +/** + * Add entry to collection by game ID + * @param id Collection ID + */ +export default defineClientEventHandler(async (h3, { fetchUser, body }) => { const user = await fetchUser(); const id = getRouterParam(h3, "id"); @@ -11,10 +21,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { statusMessage: "ID required in route params", }); - const body = await readBody(h3); const gameId = body.id; - if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); return await userLibraryManager.collectionAdd(gameId, id, user.id); -}); +}, AddEntry); diff --git a/server/api/v1/client/collection/[id]/index.delete.ts b/server/api/v1/client/collection/[id]/index.delete.ts index ffe6b71..8c5f2d7 100644 --- a/server/api/v1/client/collection/[id]/index.delete.ts +++ b/server/api/v1/client/collection/[id]/index.delete.ts @@ -1,15 +1,14 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; +/** + * Delete collection by ID + * @param id Collection ID + */ export default defineClientEventHandler(async (h3, { fetchUser }) => { const user = await fetchUser(); - const id = getRouterParam(h3, "id"); - if (!id) - throw createError({ - statusCode: 400, - statusMessage: "ID required in route params", - }); + const id = getRouterParam(h3, "id")!; // Verify collection exists and user owns it // Will not return the default collection @@ -27,5 +26,5 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { }); await userLibraryManager.deleteCollection(id); - return { success: true }; + return; }); diff --git a/server/api/v1/client/collection/[id]/index.get.ts b/server/api/v1/client/collection/[id]/index.get.ts index 9267f69..e7f609f 100644 --- a/server/api/v1/client/collection/[id]/index.get.ts +++ b/server/api/v1/client/collection/[id]/index.get.ts @@ -1,15 +1,14 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; +/** + * Fetch collection by ID + * @param id Collection ID + */ export default defineClientEventHandler(async (h3, { fetchUser }) => { const user = await fetchUser(); - const id = getRouterParam(h3, "id"); - if (!id) - throw createError({ - statusCode: 400, - statusMessage: "ID required in route params", - }); + const id = getRouterParam(h3, "id")!; // Fetch specific collection // Will not return the default collection diff --git a/server/api/v1/client/collection/default/entry.delete.ts b/server/api/v1/client/collection/default/entry.delete.ts index 774c1bd..150d941 100644 --- a/server/api/v1/client/collection/default/entry.delete.ts +++ b/server/api/v1/client/collection/default/entry.delete.ts @@ -1,15 +1,20 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; -export default defineClientEventHandler(async (h3, { fetchUser }) => { +const EntryDelete = type({ + id: "string", +}).configure(throwingArktype); + +/** + * Remove game from user's library + */ +export default defineClientEventHandler(async (h3, { fetchUser, body }) => { const user = await fetchUser(); - const body = await readBody(h3); - const gameId = body.id; - if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); await userLibraryManager.libraryRemove(gameId, user.id); return {}; -}); +}, EntryDelete); diff --git a/server/api/v1/client/collection/default/entry.post.ts b/server/api/v1/client/collection/default/entry.post.ts index 5e7f603..ce901d4 100644 --- a/server/api/v1/client/collection/default/entry.post.ts +++ b/server/api/v1/client/collection/default/entry.post.ts @@ -1,15 +1,21 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; -export default defineClientEventHandler(async (h3, { fetchUser }) => { +const AddEntry = type({ + id: "string", +}).configure(throwingArktype); + +/** + * Add game to user's library + */ +export default defineClientEventHandler(async (h3, { fetchUser, body }) => { const user = await fetchUser(); - const body = await readBody(h3); const gameId = body.id; - if (!gameId) - throw createError({ statusCode: 400, statusMessage: "Game ID required" }); // Add the game to the default collection await userLibraryManager.libraryAdd(gameId, user.id); return {}; -}); +}, AddEntry); diff --git a/server/api/v1/client/collection/default/index.get.ts b/server/api/v1/client/collection/default/index.get.ts index fdb5da6..61f2706 100644 --- a/server/api/v1/client/collection/default/index.get.ts +++ b/server/api/v1/client/collection/default/index.get.ts @@ -1,6 +1,9 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; +/** + * Fetch user's library + */ export default defineClientEventHandler(async (h3, { fetchUser }) => { const user = await fetchUser(); diff --git a/server/api/v1/client/collection/index.get.ts b/server/api/v1/client/collection/index.get.ts index 9b07931..a43d5fd 100644 --- a/server/api/v1/client/collection/index.get.ts +++ b/server/api/v1/client/collection/index.get.ts @@ -1,6 +1,9 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; +/** + * Fetch all of user's collections + */ export default defineClientEventHandler(async (h3, { fetchUser }) => { const user = await fetchUser(); diff --git a/server/api/v1/client/collection/index.post.ts b/server/api/v1/client/collection/index.post.ts index c08d355..e2126b6 100644 --- a/server/api/v1/client/collection/index.post.ts +++ b/server/api/v1/client/collection/index.post.ts @@ -1,14 +1,19 @@ +import { type } from "arktype"; +import { throwingArktype } from "~/server/arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; -export default defineClientEventHandler(async (h3, { fetchUser }) => { +const CreateCollection = type({ + name: "string", +}).configure(throwingArktype); + +/** + * Create new collection + */ +export default defineClientEventHandler(async (h3, { fetchUser, body }) => { const user = await fetchUser(); - const body = await readBody(h3); - const name = body.name; - if (!name) - throw createError({ statusCode: 400, statusMessage: "Requires name" }); // Create the collection using the manager const newCollection = await userLibraryManager.collectionCreate( @@ -16,4 +21,4 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => { user.id, ); return newCollection; -}); +}, CreateCollection); diff --git a/server/api/v1/client/game/[id]/index.get.ts b/server/api/v1/client/game/[id]/index.get.ts index 13e06fb..d2a4006 100644 --- a/server/api/v1/client/game/[id]/index.get.ts +++ b/server/api/v1/client/game/[id]/index.get.ts @@ -1,10 +1,12 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import prisma from "~/server/internal/db/database"; +/** + * Fetch game by ID + * @param id Game ID + */ export default defineClientEventHandler(async (h3) => { - const id = getRouterParam(h3, "id"); - if (!id) - throw createError({ statusCode: 400, statusMessage: "No ID in route" }); + const id = getRouterParam(h3, "id")!; const game = await prisma.game.findUnique({ where: { diff --git a/server/api/v1/client/game/manifest.get.ts b/server/api/v1/client/game/manifest.get.ts index 80535e5..275babf 100644 --- a/server/api/v1/client/game/manifest.get.ts +++ b/server/api/v1/client/game/manifest.get.ts @@ -1,15 +1,23 @@ +import { ArkErrors, type } from "arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import manifestGenerator from "~/server/internal/downloads/manifest"; +const Query = type({ + id: "string", + version: "string", +}); + +/** + * Fetch Droplet manifest from game ID and version + * @request `id` and `version` query params are required. + */ export default defineClientEventHandler(async (h3) => { - const query = getQuery(h3); - const id = query.id?.toString(); - const version = query.version?.toString(); - if (!id || !version) - throw createError({ - statusCode: 400, - statusMessage: "Missing id or version in query", - }); + const query = Query(getQuery(h3)); + if (query instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: query.summary }); + + const id = query.id; + const version = query.version; const manifest = await manifestGenerator.generateManifest(id, version); if (!manifest) diff --git a/server/api/v1/client/game/version.get.ts b/server/api/v1/client/game/version.get.ts index e9cf38e..b1a2d02 100644 --- a/server/api/v1/client/game/version.get.ts +++ b/server/api/v1/client/game/version.get.ts @@ -1,15 +1,22 @@ +import { ArkErrors, type } from "arktype"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import prisma from "~/server/internal/db/database"; +const Query = type({ + id: "string", + version: "string", +}); + +/** + * Fetch version metadata from game ID and version name + * @request `id` and `version` query params are required. + */ export default defineClientEventHandler(async (h3) => { - const query = getQuery(h3); - const id = query.id?.toString(); - const version = query.version?.toString(); - if (!id || !version) - throw createError({ - statusCode: 400, - statusMessage: "Missing id or version in query", - }); + const query = Query(getQuery(h3)); + if (query instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: query.summary }); + const id = query.id; + const version = query.version; const gameVersion = await prisma.gameVersion.findUnique({ where: { @@ -18,6 +25,9 @@ export default defineClientEventHandler(async (h3) => { versionName: version, }, }, + omit: { + dropletManifest: true, + }, }); if (!gameVersion) diff --git a/server/api/v1/client/game/versions.get.ts b/server/api/v1/client/game/versions.get.ts index fab014a..1067b76 100644 --- a/server/api/v1/client/game/versions.get.ts +++ b/server/api/v1/client/game/versions.get.ts @@ -1,6 +1,10 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import prisma from "~/server/internal/db/database"; +/** + * Fetch all versions for game ID + * @request `id` required in query params + */ export default defineClientEventHandler(async (h3) => { const query = getQuery(h3); const id = query.id?.toString(); diff --git a/server/api/v1/client/news/[id]/index.get.ts b/server/api/v1/client/news/[id]/index.get.ts index c7019c9..c62e877 100644 --- a/server/api/v1/client/news/[id]/index.get.ts +++ b/server/api/v1/client/news/[id]/index.get.ts @@ -1,13 +1,12 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import newsManager from "~/server/internal/news"; +/** + * Fetch new article by ID + * @param id Article ID + */ export default defineClientEventHandler(async (h3) => { - const id = h3.context.params?.id; - if (!id) - throw createError({ - statusCode: 400, - message: "Missing news ID", - }); + const id = getRouterParam(h3, "id")!; const news = await newsManager.fetchById(id); if (!news) diff --git a/server/api/v1/client/news/index.get.ts b/server/api/v1/client/news/index.get.ts index 8ab6e13..b0f31a6 100644 --- a/server/api/v1/client/news/index.get.ts +++ b/server/api/v1/client/news/index.get.ts @@ -1,29 +1,45 @@ +import { ArkErrors, type } from "arktype"; +import { getQuery } from "h3"; +import aclManager from "~/server/internal/acls"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import newsManager from "~/server/internal/news"; -export default defineClientEventHandler(async (h3) => { - const query = getQuery(h3); - - const orderBy = query.order as "asc" | "desc"; - if (orderBy) { - if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) - throw createError({ statusCode: 400, statusMessage: "Invalid order" }); - } - - const tags = query.tags as string[] | undefined; - if (tags) { - if (typeof tags !== "object" || !Array.isArray(tags)) - throw createError({ statusCode: 400, statusMessage: "Invalid tags" }); - } - - const options = { - take: parseInt(query.limit as string), - skip: parseInt(query.skip as string), - orderBy: orderBy, - ...(tags && { tags: tags.map((e) => e.toString()) }), - search: query.search as string, - }; - - const news = await newsManager.fetch(options); - return news; +const NewsFetch = type({ + "order?": "'asc' | 'desc'", + "tags?": "string[]", + "limit?": "string.numeric.parse", + "skip?": "string.numeric.parse", + "search?": "string", }); + +/** + * Fetch instance news articles + */ +export default defineClientEventHandler, { query: typeof NewsFetch.infer }>( + async (h3) => { + const userId = await aclManager.getUserIdACL(h3, ["news:read"]); + if (!userId) + throw createError({ + statusCode: 403, + statusMessage: "Requires authentication", + }); + + const query = NewsFetch(getQuery(h3)); + if (query instanceof ArkErrors) + throw createError({ statusCode: 400, statusMessage: query.summary }); + + const orderBy = query.order; + const tags = query.tags; + + const options = { + take: Math.min(query.limit ?? 10, 10), + skip: query.skip ?? 0, + orderBy: orderBy, + ...(tags && { tags: tags.map((e) => e.toString()) }), + search: query.search, + }; + + const news = await newsManager.fetch(options); + return news; + }, +); diff --git a/server/api/v1/client/object/[id]/index.get.ts b/server/api/v1/client/object/[id]/index.get.ts index 962d30d..440a02e 100644 --- a/server/api/v1/client/object/[id]/index.get.ts +++ b/server/api/v1/client/object/[id]/index.get.ts @@ -1,9 +1,12 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import objectHandler from "~/server/internal/objects"; +/** + * Fetch object by ID + * @param id Object ID + */ export default defineClientEventHandler(async (h3, utils) => { - const id = getRouterParam(h3, "id"); - if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" }); + const id = getRouterParam(h3, "id")!; const user = await utils.fetchUser(); diff --git a/server/api/v1/client/user/index.get.ts b/server/api/v1/client/user/index.get.ts index 79ba08c..1927980 100644 --- a/server/api/v1/client/user/index.get.ts +++ b/server/api/v1/client/user/index.get.ts @@ -1,5 +1,8 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +/** + * Fetch user data for this client + */ export default defineClientEventHandler(async (h3, { fetchUser }) => { const user = await fetchUser(); diff --git a/server/api/v1/client/user/library.get.ts b/server/api/v1/client/user/library.get.ts index 4870e2c..8974c28 100644 --- a/server/api/v1/client/user/library.get.ts +++ b/server/api/v1/client/user/library.get.ts @@ -1,6 +1,9 @@ import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import userLibraryManager from "~/server/internal/userlibrary"; +/** + * Fetch library for user + */ export default defineClientEventHandler(async (_h3, { fetchUser }) => { const user = await fetchUser(); const library = await userLibraryManager.fetchLibrary(user.id); diff --git a/server/api/v1/client/user/webtoken.post.ts b/server/api/v1/client/user/webtoken.post.ts index eda744f..2762838 100644 --- a/server/api/v1/client/user/webtoken.post.ts +++ b/server/api/v1/client/user/webtoken.post.ts @@ -4,6 +4,9 @@ import type { UserACL } from "~/server/internal/acls"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import prisma from "~/server/internal/db/database"; +/** + * Generate API token with limited API scopes to render store in client + */ export default defineClientEventHandler( async (h3, { fetchUser, fetchClient, clientId }) => { const user = await fetchUser(); diff --git a/server/api/v1/news/index.get.ts b/server/api/v1/news/index.get.ts index 2d64c48..864f916 100644 --- a/server/api/v1/news/index.get.ts +++ b/server/api/v1/news/index.get.ts @@ -14,7 +14,7 @@ const NewsFetch = type({ /** * Fetch instance news articles */ -export default defineEventHandler<{ query: typeof NewsFetch.infer }>( +export default defineEventHandler<{ query: typeof NewsFetch.infer }, ReturnType>( async (h3) => { const userId = await aclManager.getUserIdACL(h3, ["news:read"]); if (!userId) diff --git a/server/internal/clients/event-handler.ts b/server/internal/clients/event-handler.ts index 896c2ec..d0c5e0d 100644 --- a/server/internal/clients/event-handler.ts +++ b/server/internal/clients/event-handler.ts @@ -16,9 +16,9 @@ type ClientUtils = { const NONCE_LENIENCE = 30_000; -type ClientEventHandlerRequest = { - body: T; - query: { [key: string]: string | string[] }; +type ClientEventHandlerRequest = { + body?: T; + query?: Q; }; interface ClientEventHandler<