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

View File

@ -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)

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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<CapabilityConfiguration> =
body.capabilities ?? {};
const platformRaw = body.platform;
const capabilities: Partial<CapabilityConfiguration> =
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;
},
);

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

View File

@ -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);

View File

@ -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);

View File

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

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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: {

View File

@ -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)

View File

@ -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)

View File

@ -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();

View File

@ -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)

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

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();