feat: annotated client routes

This commit is contained in:
DecDuck
2025-08-10 15:51:10 +10:00
parent 824b4e708b
commit 7c234067a5
31 changed files with 479 additions and 389 deletions

View File

@ -1,12 +1,22 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { const AuthorizeBody = type({
const user = await sessionHandler.getSession(h3); id: "string",
if (!user) throw createError({ statusCode: 403 }); }).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 body = await readDropValidatedBody(h3, AuthorizeBody);
const clientId = body.id;
const client = await clientHandler.fetchClient(clientId); const client = await clientHandler.fetchClient(clientId);
if (!client) if (!client)
@ -15,7 +25,7 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Invalid or expired client ID.", statusMessage: "Invalid or expired client ID.",
}); });
if (client.userId != user.userId) if (client.userId != userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "Not allowed to authorize this client.", statusMessage: "Not allowed to authorize this client.",
@ -27,4 +37,5 @@ export default defineEventHandler(async (h3) => {
redirect: `drop://handshake/${clientId}/${token}`, redirect: `drop://handshake/${clientId}/${token}`,
token: `${clientId}/${token}`, token: `${clientId}/${token}`,
}; };
}); },
);

View File

@ -1,18 +1,23 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { const Query = type({
const user = await sessionHandler.getSession(h3); code: "string.upper",
if (!user) throw createError({ statusCode: 403 });
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); const clientId = await clientHandler.fetchClientIdByCode(code);
if (!clientId) if (!clientId)
throw createError({ statusCode: 400, statusMessage: "Invalid code." }); throw createError({ statusCode: 400, statusMessage: "Invalid code." });

View File

@ -1,12 +1,22 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { const CodeAuthorize = type({
const user = await sessionHandler.getSession(h3); id: "string",
if (!user) throw createError({ statusCode: 403 }); }).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 body = await readDropValidatedBody(h3, CodeAuthorize);
const clientId = body.id;
const client = await clientHandler.fetchClient(clientId); const client = await clientHandler.fetchClient(clientId);
if (!client) if (!client)
@ -15,7 +25,7 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Invalid or expired client ID.", statusMessage: "Invalid or expired client ID.",
}); });
if (client.userId != user.userId) if (client.userId != userId)
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "Not allowed to authorize this client.", statusMessage: "Not allowed to authorize this client.",
@ -32,4 +42,5 @@ export default defineEventHandler(async (h3) => {
await clientHandler.sendAuthToken(clientId, token); await clientHandler.sendAuthToken(clientId, token);
return; return;
}); },
);

View File

@ -1,6 +1,10 @@
import type { FetchError } from "ofetch"; import type { FetchError } from "ofetch";
import clientHandler from "~/server/internal/clients/handler"; 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({ export default defineWebSocketHandler({
async open(peer) { async open(peer) {
try { try {

View File

@ -1,15 +1,21 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import { useCertificateAuthority } from "~/server/plugins/ca"; import { useCertificateAuthority } from "~/server/plugins/ca";
export default defineEventHandler(async (h3) => { const HandshakeBody = type({
const body = await readBody(h3); clientId: "string",
token: "string",
}).configure(throwingArktype);
/**
* 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 clientId = body.clientId;
const token = body.token; const token = body.token;
if (!clientId || !token)
throw createError({
statusCode: 400,
statusMessage: "Missing token or client ID from body",
});
const metadata = await clientHandler.fetchClient(clientId); const metadata = await clientHandler.fetchClient(clientId);
if (!metadata) if (!metadata)
@ -42,4 +48,5 @@ export default defineEventHandler(async (h3) => {
certificate: bundle.cert, certificate: bundle.cert,
id: client.id, id: client.id,
}; };
}); },
);

View File

@ -1,18 +1,23 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => { const Query = type({
const user = await sessionHandler.getSession(h3); id: "string",
if (!user) throw createError({ statusCode: 403 });
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); const client = await clientHandler.fetchClient(providedClientId);
if (!client) if (!client)
throw createError({ throw createError({
@ -20,13 +25,13 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Request not found.", statusMessage: "Request not found.",
}); });
if (client.userId && user.userId !== client.userId) if (client.userId && userId !== client.userId)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Client already claimed.", statusMessage: "Client already claimed.",
}); });
await clientHandler.attachUserId(providedClientId, user.userId); await clientHandler.attachUserId(providedClientId, userId);
return client.data; return client.data;
}); });

View File

@ -17,7 +17,12 @@ const ClientAuthInitiate = type({
mode: type.valueOf(AuthMode).default(AuthMode.Callback), mode: type.valueOf(AuthMode).default(AuthMode.Callback),
}).configure(throwingArktype); }).configure(throwingArktype);
export default defineEventHandler(async (h3) => { /**
* 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 body = await readDropValidatedBody(h3, ClientAuthInitiate);
const platformRaw = body.platform; const platformRaw = body.platform;
@ -68,4 +73,5 @@ export default defineEventHandler(async (h3) => {
}); });
return result; return result;
}); },
);

View File

@ -1 +0,0 @@
export default defineEventHandler((_h3) => {});

View File

@ -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 {};
},
);

View File

@ -1,3 +1,4 @@
import { ArkErrors, type } from "arktype";
import cacheHandler from "~/server/internal/cache"; import cacheHandler from "~/server/internal/cache";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
@ -10,12 +11,27 @@ const gameLookupCache = cacheHandler.createCache<{
libraryPath: string; libraryPath: string;
}>("downloadGameLookupCache"); }>("downloadGameLookupCache");
export default defineClientEventHandler(async (h3) => { const Query = type({
const query = getQuery(h3); id: "string",
const gameId = query.id?.toString(); version: "string",
const versionName = query.version?.toString(); name: "string",
const filename = query.name?.toString(); chunk: "string.numeric.parse",
const chunkIndex = parseInt(query.chunk?.toString() ?? "?"); });
/**
* v1 download API
* @deprecated
* @response Raw binary data (`application/octet-stream`)
*/
export default defineClientEventHandler<unknown, { query: typeof Query.infer }>(
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)) if (!gameId || !versionName || !filename || Number.isNaN(chunkIndex))
throw createError({ throw createError({
@ -35,7 +51,10 @@ export default defineClientEventHandler(async (h3) => {
}, },
}); });
if (!game || !game.libraryId) if (!game || !game.libraryId)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); throw createError({
statusCode: 400,
statusMessage: "Invalid game ID",
});
await gameLookupCache.setItem(gameId, game); await gameLookupCache.setItem(gameId, game);
} }
@ -80,4 +99,5 @@ export default defineClientEventHandler(async (h3) => {
}); });
return sendStream(h3, gameReadStream); return sendStream(h3, gameReadStream);
}); },
);

View File

@ -1,7 +1,17 @@
import { type } from "arktype";
import { throwingArktype } from "~/server/arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; 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 user = await fetchUser();
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");
@ -11,10 +21,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
statusMessage: "ID required in route params", statusMessage: "ID required in route params",
}); });
const body = await readBody(h3);
const gameId = body.id; const gameId = body.id;
if (!gameId)
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
const successful = await userLibraryManager.collectionRemove( const successful = await userLibraryManager.collectionRemove(
gameId, gameId,
@ -27,4 +34,4 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
statusMessage: "Collection not found", statusMessage: "Collection not found",
}); });
return {}; return {};
}); }, EntryRemove);

View File

@ -1,7 +1,17 @@
import { type } from "arktype";
import { throwingArktype } from "~/server/arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; 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 user = await fetchUser();
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id");
@ -11,10 +21,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
statusMessage: "ID required in route params", statusMessage: "ID required in route params",
}); });
const body = await readBody(h3);
const gameId = body.id; const gameId = body.id;
if (!gameId)
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
return await userLibraryManager.collectionAdd(gameId, id, user.id); return await userLibraryManager.collectionAdd(gameId, id, user.id);
}); }, AddEntry);

View File

@ -1,15 +1,14 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
/**
* Delete collection by ID
* @param id Collection ID
*/
export default defineClientEventHandler(async (h3, { fetchUser }) => { export default defineClientEventHandler(async (h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id")!;
if (!id)
throw createError({
statusCode: 400,
statusMessage: "ID required in route params",
});
// Verify collection exists and user owns it // Verify collection exists and user owns it
// Will not return the default collection // Will not return the default collection
@ -27,5 +26,5 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
}); });
await userLibraryManager.deleteCollection(id); await userLibraryManager.deleteCollection(id);
return { success: true }; return;
}); });

View File

@ -1,15 +1,14 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
/**
* Fetch collection by ID
* @param id Collection ID
*/
export default defineClientEventHandler(async (h3, { fetchUser }) => { export default defineClientEventHandler(async (h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id")!;
if (!id)
throw createError({
statusCode: 400,
statusMessage: "ID required in route params",
});
// Fetch specific collection // Fetch specific collection
// Will not return the default collection // Will not return the default collection

View File

@ -1,15 +1,20 @@
import { type } from "arktype";
import { throwingArktype } from "~/server/arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; 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 user = await fetchUser();
const body = await readBody(h3);
const gameId = body.id; const gameId = body.id;
if (!gameId)
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
await userLibraryManager.libraryRemove(gameId, user.id); await userLibraryManager.libraryRemove(gameId, user.id);
return {}; return {};
}); }, EntryDelete);

View File

@ -1,15 +1,21 @@
import { type } from "arktype";
import { throwingArktype } from "~/server/arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; 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 user = await fetchUser();
const body = await readBody(h3);
const gameId = body.id; const gameId = body.id;
if (!gameId)
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
// Add the game to the default collection // Add the game to the default collection
await userLibraryManager.libraryAdd(gameId, user.id); await userLibraryManager.libraryAdd(gameId, user.id);
return {}; return {};
}); }, AddEntry);

View File

@ -1,6 +1,9 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
/**
* Fetch user's library
*/
export default defineClientEventHandler(async (h3, { fetchUser }) => { export default defineClientEventHandler(async (h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();

View File

@ -1,6 +1,9 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
/**
* Fetch all of user's collections
*/
export default defineClientEventHandler(async (h3, { fetchUser }) => { export default defineClientEventHandler(async (h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();

View File

@ -1,14 +1,19 @@
import { type } from "arktype";
import { throwingArktype } from "~/server/arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; 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 user = await fetchUser();
const body = await readBody(h3);
const name = body.name; const name = body.name;
if (!name)
throw createError({ statusCode: 400, statusMessage: "Requires name" });
// Create the collection using the manager // Create the collection using the manager
const newCollection = await userLibraryManager.collectionCreate( const newCollection = await userLibraryManager.collectionCreate(
@ -16,4 +21,4 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
user.id, user.id,
); );
return newCollection; return newCollection;
}); }, CreateCollection);

View File

@ -1,10 +1,12 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
/**
* Fetch game by ID
* @param id Game ID
*/
export default defineClientEventHandler(async (h3) => { export default defineClientEventHandler(async (h3) => {
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id")!;
if (!id)
throw createError({ statusCode: 400, statusMessage: "No ID in route" });
const game = await prisma.game.findUnique({ const game = await prisma.game.findUnique({
where: { where: {

View File

@ -1,16 +1,24 @@
import { ArkErrors, type } from "arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import manifestGenerator from "~/server/internal/downloads/manifest"; import manifestGenerator from "~/server/internal/downloads/manifest";
export default defineClientEventHandler(async (h3) => { const Query = type({
const query = getQuery(h3); id: "string",
const id = query.id?.toString(); version: "string",
const version = query.version?.toString();
if (!id || !version)
throw createError({
statusCode: 400,
statusMessage: "Missing id or version in query",
}); });
/**
* Fetch Droplet manifest from game ID and version
* @request `id` and `version` query params are required.
*/
export default defineClientEventHandler(async (h3) => {
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); const manifest = await manifestGenerator.generateManifest(id, version);
if (!manifest) if (!manifest)
throw createError({ throw createError({

View File

@ -1,16 +1,23 @@
import { ArkErrors, type } from "arktype";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineClientEventHandler(async (h3) => { const Query = type({
const query = getQuery(h3); id: "string",
const id = query.id?.toString(); version: "string",
const version = query.version?.toString();
if (!id || !version)
throw createError({
statusCode: 400,
statusMessage: "Missing id or version in query",
}); });
/**
* Fetch version metadata from game ID and version name
* @request `id` and `version` query params are required.
*/
export default defineClientEventHandler(async (h3) => {
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({ const gameVersion = await prisma.gameVersion.findUnique({
where: { where: {
gameId_versionName: { gameId_versionName: {
@ -18,6 +25,9 @@ export default defineClientEventHandler(async (h3) => {
versionName: version, versionName: version,
}, },
}, },
omit: {
dropletManifest: true,
},
}); });
if (!gameVersion) if (!gameVersion)

View File

@ -1,6 +1,10 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
/**
* Fetch all versions for game ID
* @request `id` required in query params
*/
export default defineClientEventHandler(async (h3) => { export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3); const query = getQuery(h3);
const id = query.id?.toString(); const id = query.id?.toString();

View File

@ -1,13 +1,12 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import newsManager from "~/server/internal/news"; import newsManager from "~/server/internal/news";
/**
* Fetch new article by ID
* @param id Article ID
*/
export default defineClientEventHandler(async (h3) => { export default defineClientEventHandler(async (h3) => {
const id = h3.context.params?.id; const id = getRouterParam(h3, "id")!;
if (!id)
throw createError({
statusCode: 400,
message: "Missing news ID",
});
const news = await newsManager.fetchById(id); const news = await newsManager.fetchById(id);
if (!news) if (!news)

View File

@ -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 { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import newsManager from "~/server/internal/news"; import newsManager from "~/server/internal/news";
export default defineClientEventHandler(async (h3) => { const NewsFetch = type({
const query = getQuery(h3); "order?": "'asc' | 'desc'",
"tags?": "string[]",
"limit?": "string.numeric.parse",
"skip?": "string.numeric.parse",
"search?": "string",
});
const orderBy = query.order as "asc" | "desc"; /**
if (orderBy) { * Fetch instance news articles
if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy)) */
throw createError({ statusCode: 400, statusMessage: "Invalid order" }); export default defineClientEventHandler<ReturnType<typeof newsManager.fetch>, { query: typeof NewsFetch.infer }>(
} async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
if (!userId)
throw createError({
statusCode: 403,
statusMessage: "Requires authentication",
});
const tags = query.tags as string[] | undefined; const query = NewsFetch(getQuery(h3));
if (tags) { if (query instanceof ArkErrors)
if (typeof tags !== "object" || !Array.isArray(tags)) throw createError({ statusCode: 400, statusMessage: query.summary });
throw createError({ statusCode: 400, statusMessage: "Invalid tags" });
} const orderBy = query.order;
const tags = query.tags;
const options = { const options = {
take: parseInt(query.limit as string), take: Math.min(query.limit ?? 10, 10),
skip: parseInt(query.skip as string), skip: query.skip ?? 0,
orderBy: orderBy, orderBy: orderBy,
...(tags && { tags: tags.map((e) => e.toString()) }), ...(tags && { tags: tags.map((e) => e.toString()) }),
search: query.search as string, search: query.search,
}; };
const news = await newsManager.fetch(options); const news = await newsManager.fetch(options);
return news; return news;
}); },
);

View File

@ -1,9 +1,12 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import objectHandler from "~/server/internal/objects"; import objectHandler from "~/server/internal/objects";
/**
* Fetch object by ID
* @param id Object ID
*/
export default defineClientEventHandler(async (h3, utils) => { export default defineClientEventHandler(async (h3, utils) => {
const id = getRouterParam(h3, "id"); const id = getRouterParam(h3, "id")!;
if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
const user = await utils.fetchUser(); const user = await utils.fetchUser();

View File

@ -1,5 +1,8 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
/**
* Fetch user data for this client
*/
export default defineClientEventHandler(async (h3, { fetchUser }) => { export default defineClientEventHandler(async (h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();

View File

@ -1,6 +1,9 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
/**
* Fetch library for user
*/
export default defineClientEventHandler(async (_h3, { fetchUser }) => { export default defineClientEventHandler(async (_h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();
const library = await userLibraryManager.fetchLibrary(user.id); const library = await userLibraryManager.fetchLibrary(user.id);

View File

@ -4,6 +4,9 @@ import type { UserACL } from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
/**
* Generate API token with limited API scopes to render store in client
*/
export default defineClientEventHandler( export default defineClientEventHandler(
async (h3, { fetchUser, fetchClient, clientId }) => { async (h3, { fetchUser, fetchClient, clientId }) => {
const user = await fetchUser(); const user = await fetchUser();

View File

@ -14,7 +14,7 @@ const NewsFetch = type({
/** /**
* Fetch instance news articles * Fetch instance news articles
*/ */
export default defineEventHandler<{ query: typeof NewsFetch.infer }>( export default defineEventHandler<{ query: typeof NewsFetch.infer }, ReturnType<typeof newsManager.fetch>>(
async (h3) => { async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["news:read"]); const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
if (!userId) if (!userId)

View File

@ -16,9 +16,9 @@ type ClientUtils<R> = {
const NONCE_LENIENCE = 30_000; const NONCE_LENIENCE = 30_000;
type ClientEventHandlerRequest<T> = { type ClientEventHandlerRequest<T, Q = {[key: string]: any }> = {
body: T; body?: T;
query: { [key: string]: string | string[] }; query?: Q;
}; };
interface ClientEventHandler< interface ClientEventHandler<