+ class="relative transform overflow-hidden rounded-lg bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm">
-
+
-
@@ -164,7 +120,7 @@ if (!token)
});
const bearerToken = `Bearer ${token}`;
-const allowed = await $dropFetch("/api/v1/admin", {
+const allowed = await $dropFetch("/api/v1/admin/setup", {
headers: { Authorization: bearerToken },
});
if (!allowed)
diff --git a/server/api/v1/admin/index.get.ts b/server/api/v1/admin/setup.get.ts
similarity index 61%
rename from server/api/v1/admin/index.get.ts
rename to server/api/v1/admin/setup.get.ts
index 5ef72c0..fda603d 100644
--- a/server/api/v1/admin/index.get.ts
+++ b/server/api/v1/admin/setup.get.ts
@@ -1,10 +1,10 @@
import aclManager from "~/server/internal/acls";
/**
- * Check if we are an admin/system
+ * Check if we are a setup token
*/
export default defineEventHandler(async (h3) => {
- const allowed = await aclManager.allowSystemACL(h3, []);
+ const allowed = await aclManager.allowSystemACL(h3, ["setup"]);
if (!allowed) return false;
return true;
});
diff --git a/server/api/v1/auth/index.get.ts b/server/api/v1/auth/index.get.ts
index 1582f1b..e3766f1 100644
--- a/server/api/v1/auth/index.get.ts
+++ b/server/api/v1/auth/index.get.ts
@@ -1,5 +1,8 @@
import authManager from "~/server/internal/auth";
+/**
+ * Fetch public authentication provider mechanisms
+ */
export default defineEventHandler(() => {
return authManager.getEnabledAuthProviders();
});
diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts
index 3e4ca03..cf2b975 100644
--- a/server/api/v1/auth/signin/simple.post.ts
+++ b/server/api/v1/auth/signin/simple.post.ts
@@ -15,6 +15,9 @@ const signinValidator = type({
"rememberMe?": "boolean | undefined",
});
+/**
+ * Sign in as a session using the "Simple" authentication mechanism. Not recommended for third-party applications.
+ */
export default defineEventHandler<{
body: typeof signinValidator.infer;
}>(async (h3) => {
diff --git a/server/api/v1/auth/signup/simple.get.ts b/server/api/v1/auth/signup/simple.get.ts
index ad6c496..9037bd4 100644
--- a/server/api/v1/auth/signup/simple.get.ts
+++ b/server/api/v1/auth/signup/simple.get.ts
@@ -1,8 +1,16 @@
import prisma from "~/server/internal/db/database";
import taskHandler from "~/server/internal/tasks";
import authManager from "~/server/internal/auth";
+import { ArkErrors, type } from "arktype";
-export default defineEventHandler(async (h3) => {
+const Query = type({
+ id: "string",
+});
+
+/**
+ * Fetch invitation details for pre-filling
+ */
+export default defineEventHandler<{ query: typeof Query.infer }>(async (h3) => {
const t = await useTranslation(h3);
if (!authManager.getAuthProviders().Simple)
@@ -11,13 +19,13 @@ export default defineEventHandler(async (h3) => {
statusMessage: t("errors.auth.method.signinDisabled"),
});
- const query = getQuery(h3);
- const id = query.id?.toString();
- if (!id)
+ const query = Query(getQuery(h3));
+ if (query instanceof ArkErrors)
throw createError({
statusCode: 400,
- statusMessage: t("errors.auth.inviteIdRequired"),
+ statusMessage: "Invalid query: " + query.summary,
});
+ const id = query.id;
taskHandler.runTaskGroupByName("cleanup:invitations");
const invitation = await prisma.invitation.findUnique({ where: { id: id } });
diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts
index 8434f65..dd0a244 100644
--- a/server/api/v1/auth/signup/simple.post.ts
+++ b/server/api/v1/auth/signup/simple.post.ts
@@ -18,6 +18,9 @@ const CreateUserValidator = SharedRegisterValidator.and({
"displayName?": "string | undefined",
}).configure(throwingArktype);
+/**
+ * Create user from invitation
+ */
export default defineEventHandler<{
body: typeof CreateUserValidator.infer;
}>(async (h3) => {
diff --git a/server/api/v1/collection/[id]/entry.delete.ts b/server/api/v1/collection/[id]/entry.delete.ts
index d0a4faf..dabc227 100644
--- a/server/api/v1/collection/[id]/entry.delete.ts
+++ b/server/api/v1/collection/[id]/entry.delete.ts
@@ -1,34 +1,44 @@
+import { type } from "arktype";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, ["collections:remove"]);
- if (!userId)
- throw createError({
- statusCode: 403,
- });
+const RemoveEntry = type({
+ id: "string",
+}).configure(throwingArktype);
- const id = getRouterParam(h3, "id");
- if (!id)
- throw createError({
- statusCode: 400,
- statusMessage: "ID required in route params",
- });
+/**
+ * Remove entry from collection
+ * @param id Collection ID
+ */
+export default defineEventHandler<{ body: typeof RemoveEntry.infer }>(
+ async (h3) => {
+ const userId = await aclManager.getUserIdACL(h3, ["collections:remove"]);
+ if (!userId)
+ throw createError({
+ statusCode: 403,
+ });
- const body = await readBody(h3);
- const gameId = body.id;
- if (!gameId)
- throw createError({ statusCode: 400, statusMessage: "Game ID required" });
+ const id = getRouterParam(h3, "id");
+ if (!id)
+ throw createError({
+ statusCode: 400,
+ statusMessage: "ID required in route params",
+ });
- const successful = await userLibraryManager.collectionRemove(
- gameId,
- id,
- userId,
- );
- if (!successful)
- throw createError({
- statusCode: 404,
- statusMessage: "Collection not found",
- });
- return {};
-});
+ const body = await readDropValidatedBody(h3, RemoveEntry);
+ const gameId = body.id;
+
+ const successful = await userLibraryManager.collectionRemove(
+ gameId,
+ id,
+ userId,
+ );
+ if (!successful)
+ throw createError({
+ statusCode: 404,
+ statusMessage: "Collection not found",
+ });
+ return {};
+ },
+);
diff --git a/server/api/v1/collection/[id]/entry.post.ts b/server/api/v1/collection/[id]/entry.post.ts
index d6a2394..1361063 100644
--- a/server/api/v1/collection/[id]/entry.post.ts
+++ b/server/api/v1/collection/[id]/entry.post.ts
@@ -1,7 +1,17 @@
+import { type } from "arktype";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
-export default defineEventHandler(async (h3) => {
+const AddEntry = type({
+ id: "string",
+}).configure(throwingArktype);
+
+/**
+ * Add game to collection
+ * @param id Collection ID
+ */
+export default defineEventHandler<{ body: typeof AddEntry.infer }>(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["collections:add"]);
if (!userId)
throw createError({
@@ -15,10 +25,8 @@ export default defineEventHandler(async (h3) => {
statusMessage: "ID required in route params",
});
- const body = await readBody(h3);
+ const body = await readDropValidatedBody(h3, AddEntry);
const gameId = body.id;
- if (!gameId)
- throw createError({ statusCode: 400, statusMessage: "Game ID required" });
return await userLibraryManager.collectionAdd(gameId, id, userId);
});
diff --git a/server/api/v1/collection/[id]/index.delete.ts b/server/api/v1/collection/[id]/index.delete.ts
index e00b3d6..ec05792 100644
--- a/server/api/v1/collection/[id]/index.delete.ts
+++ b/server/api/v1/collection/[id]/index.delete.ts
@@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
+/**
+ * Delete a collection
+ * @param id Collection ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["collections:delete"]);
if (!userId)
diff --git a/server/api/v1/collection/[id]/index.get.ts b/server/api/v1/collection/[id]/index.get.ts
index 9bb1854..e6fc543 100644
--- a/server/api/v1/collection/[id]/index.get.ts
+++ b/server/api/v1/collection/[id]/index.get.ts
@@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
+/**
+ * Fetch collection by ID
+ * @param id Collection ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId)
diff --git a/server/api/v1/collection/default/entry.delete.ts b/server/api/v1/collection/default/entry.delete.ts
index 77f3c39..5d011eb 100644
--- a/server/api/v1/collection/default/entry.delete.ts
+++ b/server/api/v1/collection/default/entry.delete.ts
@@ -1,20 +1,29 @@
+import { type } from "arktype";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, ["library:remove"]);
- if (!userId)
- throw createError({
- statusCode: 403,
- statusMessage: "Requires authentication",
- });
+const DeleteEntry = type({
+ id: "string",
+}).configure(throwingArktype);
- const body = await readBody(h3);
+/**
+ * Remove game from user library
+ */
+export default defineEventHandler<{ body: typeof DeleteEntry.infer }>(
+ async (h3) => {
+ const userId = await aclManager.getUserIdACL(h3, ["library:remove"]);
+ if (!userId)
+ throw createError({
+ statusCode: 403,
+ statusMessage: "Requires authentication",
+ });
- const gameId = body.id;
- if (!gameId)
- throw createError({ statusCode: 400, statusMessage: "Game ID required" });
+ const body = await readDropValidatedBody(h3, DeleteEntry);
- await userLibraryManager.libraryRemove(gameId, userId);
- return {};
-});
+ const gameId = body.id;
+
+ await userLibraryManager.libraryRemove(gameId, userId);
+ return {};
+ },
+);
diff --git a/server/api/v1/collection/default/entry.post.ts b/server/api/v1/collection/default/entry.post.ts
index 4c8b8fd..9fa5d45 100644
--- a/server/api/v1/collection/default/entry.post.ts
+++ b/server/api/v1/collection/default/entry.post.ts
@@ -1,20 +1,29 @@
+import { type } from "arktype";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, ["library:add"]);
- if (!userId)
- throw createError({
- statusCode: 403,
- statusMessage: "Requires authentication",
- });
+const AddGame = type({
+ id: "string",
+}).configure(throwingArktype);
- const body = await readBody(h3);
- const gameId = body.id;
- if (!gameId)
- throw createError({ statusCode: 400, statusMessage: "Game ID required" });
+/**
+ * Add game to user library
+ */
+export default defineEventHandler<{ body: typeof AddGame.infer }>(
+ async (h3) => {
+ const userId = await aclManager.getUserIdACL(h3, ["library:add"]);
+ if (!userId)
+ throw createError({
+ statusCode: 403,
+ statusMessage: "Requires authentication",
+ });
- // Add the game to the default collection
- await userLibraryManager.libraryAdd(gameId, userId);
- return {};
-});
+ const body = await readDropValidatedBody(h3, AddGame);
+ const gameId = body.id;
+
+ // Add the game to the default collection
+ await userLibraryManager.libraryAdd(gameId, userId);
+ return {};
+ },
+);
diff --git a/server/api/v1/collection/default/index.get.ts b/server/api/v1/collection/default/index.get.ts
index 22a357d..32647f7 100644
--- a/server/api/v1/collection/default/index.get.ts
+++ b/server/api/v1/collection/default/index.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
+/**
+ * Fetch user library
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId)
diff --git a/server/api/v1/collection/index.get.ts b/server/api/v1/collection/index.get.ts
index 2fbe41b..92f28fd 100644
--- a/server/api/v1/collection/index.get.ts
+++ b/server/api/v1/collection/index.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
+/**
+ * Fetch all collections
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
if (!userId)
diff --git a/server/api/v1/collection/index.post.ts b/server/api/v1/collection/index.post.ts
index 1d3daad..dc950db 100644
--- a/server/api/v1/collection/index.post.ts
+++ b/server/api/v1/collection/index.post.ts
@@ -1,20 +1,28 @@
+import { type } from "arktype";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import userLibraryManager from "~/server/internal/userlibrary";
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
- if (!userId)
- throw createError({
- statusCode: 403,
- });
+const CreateCollection = type({
+ name: "string",
+}).configure(throwingArktype);
- const body = await readBody(h3);
+export default defineEventHandler<{ body: typeof CreateCollection.infer }>(
+ async (h3) => {
+ const userId = await aclManager.getUserIdACL(h3, ["collections:read"]);
+ if (!userId)
+ throw createError({
+ statusCode: 403,
+ });
- const name = body.name;
- if (!name)
- throw createError({ statusCode: 400, statusMessage: "Requires name" });
+ const body = await readDropValidatedBody(h3, CreateCollection);
+ const name = body.name;
- // Create the collection using the manager
- const newCollection = await userLibraryManager.collectionCreate(name, userId);
- return newCollection;
-});
+ // Create the collection using the manager
+ const newCollection = await userLibraryManager.collectionCreate(
+ name,
+ userId,
+ );
+ return newCollection;
+ },
+);
diff --git a/server/api/v1/companies/[id]/index.get.ts b/server/api/v1/companies/[id]/index.get.ts
index a326371..618b924 100644
--- a/server/api/v1/companies/[id]/index.get.ts
+++ b/server/api/v1/companies/[id]/index.get.ts
@@ -1,16 +1,15 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch company by ID
+ * @param id Company ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 });
- const companyId = getRouterParam(h3, "id");
- if (!companyId)
- throw createError({
- statusCode: 400,
- statusMessage: "Missing gameId in route params (somehow...?)",
- });
+ const companyId = getRouterParam(h3, "id")!;
const company = await prisma.company.findUnique({
where: { id: companyId },
diff --git a/server/api/v1/games/[id]/index.get.ts b/server/api/v1/games/[id]/index.get.ts
index 029b9b2..ccd9b60 100644
--- a/server/api/v1/games/[id]/index.get.ts
+++ b/server/api/v1/games/[id]/index.get.ts
@@ -1,16 +1,15 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch game by ID
+ * @param id Game ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 });
- const gameId = getRouterParam(h3, "id");
- if (!gameId)
- throw createError({
- statusCode: 400,
- statusMessage: "Missing gameId in route params (somehow...?)",
- });
+ const gameId = getRouterParam(h3, "id")!;
const game = await prisma.game.findUnique({
where: { id: gameId },
diff --git a/server/api/v1/index.get.ts b/server/api/v1/index.get.ts
index 5500dd4..b7cb461 100644
--- a/server/api/v1/index.get.ts
+++ b/server/api/v1/index.get.ts
@@ -1,5 +1,8 @@
import { systemConfig } from "~/server/internal/config/sys-conf";
+/**
+ * Fetch instance information
+ */
export default defineEventHandler(async (_h3) => {
return {
appName: "Drop",
diff --git a/server/api/v1/news/[id]/index.get.ts b/server/api/v1/news/[id]/index.get.ts
index 2499779..c0f5d89 100644
--- a/server/api/v1/news/[id]/index.get.ts
+++ b/server/api/v1/news/[id]/index.get.ts
@@ -2,6 +2,10 @@ import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
+/**
+ * Fetch news article by ID
+ * @param id Article ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
if (!userId)
@@ -10,12 +14,7 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Requires authentication",
});
- 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/news/index.get.ts b/server/api/v1/news/index.get.ts
index e5b6aac..2d64c48 100644
--- a/server/api/v1/news/index.get.ts
+++ b/server/api/v1/news/index.get.ts
@@ -1,37 +1,44 @@
+import { ArkErrors, type } from "arktype";
import { defineEventHandler, getQuery } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
- if (!userId)
- throw createError({
- statusCode: 403,
- statusMessage: "Requires authentication",
- });
-
- 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 defineEventHandler<{ 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/notifications/[id]/index.delete.ts b/server/api/v1/notifications/[id]/index.delete.ts
index ac60839..f8c5930 100644
--- a/server/api/v1/notifications/[id]/index.delete.ts
+++ b/server/api/v1/notifications/[id]/index.delete.ts
@@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Delete notification.
+ * @param id Notification ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["notifications:delete"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/notifications/[id]/index.get.ts b/server/api/v1/notifications/[id]/index.get.ts
index ba2c6fa..e65b380 100644
--- a/server/api/v1/notifications/[id]/index.get.ts
+++ b/server/api/v1/notifications/[id]/index.get.ts
@@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch notification by ID
+ * @param id Notification ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/notifications/[id]/read.post.ts b/server/api/v1/notifications/[id]/read.post.ts
index 4ffe007..b33eaed 100644
--- a/server/api/v1/notifications/[id]/read.post.ts
+++ b/server/api/v1/notifications/[id]/read.post.ts
@@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Mark notification as read
+ * @param id Notification ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/notifications/index.get.ts b/server/api/v1/notifications/index.get.ts
index 982d520..b8e1019 100644
--- a/server/api/v1/notifications/index.get.ts
+++ b/server/api/v1/notifications/index.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch all notifications for this token
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/notifications/readall.post.ts b/server/api/v1/notifications/readall.post.ts
index 0e4c8c7..daaffca 100644
--- a/server/api/v1/notifications/readall.post.ts
+++ b/server/api/v1/notifications/readall.post.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Mark all notifications as read
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/notifications/ws.get.ts b/server/api/v1/notifications/ws.get.ts
index 2f86d7c..4d913ef 100644
--- a/server/api/v1/notifications/ws.get.ts
+++ b/server/api/v1/notifications/ws.get.ts
@@ -6,6 +6,11 @@ import { logger } from "~/server/internal/logging";
// Peer ID to user ID
const socketSessions = new Map
();
+/**
+ * Connect to a WebSocket to listen for notification pushes.
+ *
+ * Sends "unauthenticated" if authentication fails, otherwise JSON notifications.
+ */
export default defineWebSocketHandler({
async open(peer) {
const h3 = { headers: peer.request?.headers ?? new Headers() };
diff --git a/server/api/v1/object/[id]/index.delete.ts b/server/api/v1/object/[id]/index.delete.ts
index fbdd5ee..c20e45c 100644
--- a/server/api/v1/object/[id]/index.delete.ts
+++ b/server/api/v1/object/[id]/index.delete.ts
@@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
import objectHandler from "~/server/internal/objects";
import sanitize from "sanitize-filename";
+/**
+ * Delete object
+ * @param id Object ID
+ */
export default defineEventHandler(async (h3) => {
const unsafeId = getRouterParam(h3, "id");
if (!unsafeId)
diff --git a/server/api/v1/object/[id]/index.get.ts b/server/api/v1/object/[id]/index.get.ts
index 649bfcd..ae1dabe 100644
--- a/server/api/v1/object/[id]/index.get.ts
+++ b/server/api/v1/object/[id]/index.get.ts
@@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
import objectHandler from "~/server/internal/objects";
import sanitize from "sanitize-filename";
+/**
+ * Fetch object. Sets a lot of caching headers, recommended to use them.
+ * @param id Object ID
+ */
export default defineEventHandler(async (h3) => {
const unsafeId = getRouterParam(h3, "id");
if (!unsafeId)
diff --git a/server/api/v1/object/[id]/index.head.ts b/server/api/v1/object/[id]/index.head.ts
index e762de5..3e479a2 100644
--- a/server/api/v1/object/[id]/index.head.ts
+++ b/server/api/v1/object/[id]/index.head.ts
@@ -2,7 +2,10 @@ import aclManager from "~/server/internal/acls";
import objectHandler from "~/server/internal/objects";
import sanitize from "sanitize-filename";
-// this request method is purely used by the browser to check if etag values are still valid
+/**
+ * Check if object has changed (etag/browser caching)
+ * @param id Object ID
+ */
export default defineEventHandler(async (h3) => {
const unsafeId = getRouterParam(h3, "id");
if (!unsafeId)
diff --git a/server/api/v1/object/[id]/index.post.ts b/server/api/v1/object/[id]/index.post.ts
index e6779ff..ee40fc7 100644
--- a/server/api/v1/object/[id]/index.post.ts
+++ b/server/api/v1/object/[id]/index.post.ts
@@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
import objectHandler from "~/server/internal/objects";
import sanitize from "sanitize-filename";
+/**
+ * Upload and overwrite object. Takes raw binary data (`application/octet-stream`)
+ * @param id Object ID
+ */
export default defineEventHandler(async (h3) => {
const unsafeId = getRouterParam(h3, "id");
if (!unsafeId)
diff --git a/server/api/v1/screenshots/[id]/index.delete.ts b/server/api/v1/screenshots/[id]/index.delete.ts
index 41e2bf5..e4fa0a3 100644
--- a/server/api/v1/screenshots/[id]/index.delete.ts
+++ b/server/api/v1/screenshots/[id]/index.delete.ts
@@ -1,8 +1,11 @@
-// get a specific screenshot
import aclManager from "~/server/internal/acls";
import screenshotManager from "~/server/internal/screenshots";
import sanitize from "sanitize-filename";
+/**
+ * Delete screenshot by ID
+ * @param id Screenshot ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["screenshots:delete"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/screenshots/[id]/index.get.ts b/server/api/v1/screenshots/[id]/index.get.ts
index 79d569a..423850f 100644
--- a/server/api/v1/screenshots/[id]/index.get.ts
+++ b/server/api/v1/screenshots/[id]/index.get.ts
@@ -3,6 +3,10 @@ import aclManager from "~/server/internal/acls";
import screenshotManager from "~/server/internal/screenshots";
import sanitize from "sanitize-filename";
+/**
+ * Fetch screenshot by ID. Use `/api/v1/object/:id` to actually fetch screenshot image data.
+ * @param id Screenshot ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/screenshots/game/[id]/index.get.ts b/server/api/v1/screenshots/game/[id]/index.get.ts
index 71addae..18976cc 100644
--- a/server/api/v1/screenshots/game/[id]/index.get.ts
+++ b/server/api/v1/screenshots/game/[id]/index.get.ts
@@ -1,7 +1,10 @@
-// get all user screenshots by game
import aclManager from "~/server/internal/acls";
import screenshotManager from "~/server/internal/screenshots";
+/**
+ * Fetch all screenshots for game
+ * @param id Game ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/screenshots/game/[id]/index.post.ts b/server/api/v1/screenshots/game/[id]/index.post.ts
index c06b815..77aa5c0 100644
--- a/server/api/v1/screenshots/game/[id]/index.post.ts
+++ b/server/api/v1/screenshots/game/[id]/index.post.ts
@@ -3,8 +3,10 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import screenshotManager from "~/server/internal/screenshots";
-// TODO: make defineClientEventHandler instead?
-// only clients will be upload screenshots yea??
+/**
+ * Upload screenshot by game. Subject to change, will likely become a client route. Takes raw upload (`application/octet-stream`)
+ * @param id Game ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["screenshots:new"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/screenshots/index.get.ts b/server/api/v1/screenshots/index.get.ts
index 97b9f93..28c9cbf 100644
--- a/server/api/v1/screenshots/index.get.ts
+++ b/server/api/v1/screenshots/index.get.ts
@@ -2,6 +2,9 @@
import aclManager from "~/server/internal/acls";
import screenshotManager from "~/server/internal/screenshots";
+/**
+ * Fetch all screenshots
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/settings/index.get.ts b/server/api/v1/settings/index.get.ts
index dea2604..61456ac 100644
--- a/server/api/v1/settings/index.get.ts
+++ b/server/api/v1/settings/index.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import { applicationSettings } from "~/server/internal/config/application-configuration";
+/**
+ * Fetch system settings
+ */
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.getUserACL(h3, ["settings:read"]);
if (!allowed) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/setup.post.ts b/server/api/v1/setup.post.ts
index acd83f6..958214c 100644
--- a/server/api/v1/setup.post.ts
+++ b/server/api/v1/setup.post.ts
@@ -2,6 +2,9 @@ import { APITokenMode } from "~/prisma/client/enums";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Complete setup, and delete setup token.
+ */
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["setup"]);
if (!allowed)
diff --git a/server/api/v1/store/featured.get.ts b/server/api/v1/store/featured.get.ts
index fb353e4..c9647f4 100644
--- a/server/api/v1/store/featured.get.ts
+++ b/server/api/v1/store/featured.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch all featured games. Used for store carousel.
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/store/index.get.ts b/server/api/v1/store/index.get.ts
index 9a61e67..2c19e2d 100644
--- a/server/api/v1/store/index.get.ts
+++ b/server/api/v1/store/index.get.ts
@@ -21,102 +21,107 @@ const StoreRead = type({
sort: "'default' | 'newest' | 'recent' = 'default'",
});
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
- if (!userId) throw createError({ statusCode: 403 });
+/**
+ * Store endpoint. Filter games with pagination. Used for all "store views".
+ */
+export default defineEventHandler<{ query: typeof StoreRead.infer }>(
+ async (h3) => {
+ const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
+ if (!userId) throw createError({ statusCode: 403 });
- const query = getQuery(h3);
- const options = StoreRead(query);
- if (options instanceof ArkErrors)
- throw createError({ statusCode: 400, statusMessage: options.summary });
+ const query = getQuery(h3);
+ const options = StoreRead(query);
+ if (options instanceof ArkErrors)
+ throw createError({ statusCode: 400, statusMessage: options.summary });
- /**
- * Generic filters
- */
- const tagFilter = options.tags
- ? {
- tags: {
- some: {
- id: {
- in: options.tags.split(","),
+ /**
+ * Generic filters
+ */
+ const tagFilter = options.tags
+ ? {
+ tags: {
+ some: {
+ id: {
+ in: options.tags.split(","),
+ },
},
},
- },
- }
- : undefined;
- const platformFilter = options.platform
- ? {
- versions: {
- some: {
- platform: {
- in: options.platform
- .split(",")
- .map(parsePlatform)
- .filter((e) => e !== undefined),
+ }
+ : undefined;
+ const platformFilter = options.platform
+ ? {
+ versions: {
+ some: {
+ platform: {
+ in: options.platform
+ .split(",")
+ .map(parsePlatform)
+ .filter((e) => e !== undefined),
+ },
},
},
- },
- }
- : undefined;
+ }
+ : undefined;
- /**
- * Company filtering
- */
- const companyActions = options.companyActions.split(",");
- const developedFilter = companyActions.includes("developed")
- ? {
- developers: {
- some: {
- id: options.company!,
+ /**
+ * Company filtering
+ */
+ const companyActions = options.companyActions.split(",");
+ const developedFilter = companyActions.includes("developed")
+ ? {
+ developers: {
+ some: {
+ id: options.company!,
+ },
},
- },
- }
- : undefined;
- const publishedFilter = companyActions.includes("published")
- ? {
- publishers: {
- some: {
- id: options.company!,
+ }
+ : undefined;
+ const publishedFilter = companyActions.includes("published")
+ ? {
+ publishers: {
+ some: {
+ id: options.company!,
+ },
},
- },
- }
- : undefined;
- const companyFilter = options.company
- ? ({
- OR: [developedFilter, publishedFilter].filter((e) => e !== undefined),
- } satisfies Prisma.GameWhereInput)
- : undefined;
+ }
+ : undefined;
+ const companyFilter = options.company
+ ? ({
+ OR: [developedFilter, publishedFilter].filter((e) => e !== undefined),
+ } satisfies Prisma.GameWhereInput)
+ : undefined;
- /**
- * Query
- */
+ /**
+ * Query
+ */
- const finalFilter: Prisma.GameWhereInput = {
- ...tagFilter,
- ...platformFilter,
- ...companyFilter,
- };
+ const finalFilter: Prisma.GameWhereInput = {
+ ...tagFilter,
+ ...platformFilter,
+ ...companyFilter,
+ };
- const sort: Prisma.GameOrderByWithRelationInput = {};
- switch (options.sort) {
- case "default":
- case "newest":
- sort.mReleased = "desc";
- break;
- case "recent":
- sort.created = "desc";
- break;
- }
+ const sort: Prisma.GameOrderByWithRelationInput = {};
+ switch (options.sort) {
+ case "default":
+ case "newest":
+ sort.mReleased = "desc";
+ break;
+ case "recent":
+ sort.created = "desc";
+ break;
+ }
- const [results, count] = await prisma.$transaction([
- prisma.game.findMany({
- skip: options.skip,
- take: Math.min(options.take, 50),
- where: finalFilter,
- orderBy: sort,
- }),
- prisma.game.count({ where: finalFilter }),
- ]);
+ const [results, count] = await prisma.$transaction([
+ prisma.game.findMany({
+ skip: options.skip,
+ take: Math.min(options.take, 50),
+ where: finalFilter,
+ orderBy: sort,
+ }),
+ prisma.game.count({ where: finalFilter }),
+ ]);
- return { results, count };
-});
+ return { results, count };
+ },
+);
diff --git a/server/api/v1/store/tags.get.ts b/server/api/v1/store/tags.get.ts
index 07f4030..8aeb24d 100644
--- a/server/api/v1/store/tags.get.ts
+++ b/server/api/v1/store/tags.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch all game tags.
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/tags/[id]/index.get.ts b/server/api/v1/tags/[id]/index.get.ts
index 5d5d686..8be5bc6 100644
--- a/server/api/v1/tags/[id]/index.get.ts
+++ b/server/api/v1/tags/[id]/index.get.ts
@@ -1,16 +1,15 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch tag by ID
+ * @param id Tag ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
if (!userId) throw createError({ statusCode: 403 });
- const tagId = getRouterParam(h3, "id");
- if (!tagId)
- throw createError({
- statusCode: 400,
- statusMessage: "Missing gameId in route params (somehow...?)",
- });
+ const tagId = getRouterParam(h3, "id")!;
const tag = await prisma.gameTag.findUnique({
where: { id: tagId },
diff --git a/server/api/v1/task/index.get.ts b/server/api/v1/task/index.get.ts
index 6f25a53..11fcc01 100644
--- a/server/api/v1/task/index.get.ts
+++ b/server/api/v1/task/index.get.ts
@@ -5,6 +5,15 @@ import type { MinimumRequestObject } from "~/server/h3";
// ID to admin
const socketHeaders = new Map();
+/**
+ * WebSocket to listen to task updates.
+ *
+ * Sends "unauthenticated" if authentication failed.
+ *
+ * Use `connect/:taskId` to subscribe to a task.
+ *
+ * Sends JSON tasks for all tasks subscribed.
+ */
export default defineWebSocketHandler({
async open(peer) {
const request = peer.request;
diff --git a/server/api/v1/user/client/[id]/index.delete.ts b/server/api/v1/user/client/[id]/index.delete.ts
index 2f17273..cf20929 100644
--- a/server/api/v1/user/client/[id]/index.delete.ts
+++ b/server/api/v1/user/client/[id]/index.delete.ts
@@ -1,16 +1,15 @@
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler";
+/**
+ * Revoke client
+ * @param id Client ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]);
if (!userId) throw createError({ statusCode: 403 });
- const clientId = getRouterParam(h3, "id");
- if (!clientId)
- throw createError({
- statusCode: 400,
- statusMessage: "Client ID missing in route params",
- });
+ const clientId = getRouterParam(h3, "id")!;
await clientHandler.removeClient(clientId);
});
diff --git a/server/api/v1/user/client/index.get.ts b/server/api/v1/user/client/index.get.ts
index 17b5f1f..4e7e95f 100644
--- a/server/api/v1/user/client/index.get.ts
+++ b/server/api/v1/user/client/index.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch all clients connected to this account
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["clients:read"]);
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/user/index.get.ts b/server/api/v1/user/index.get.ts
index fb8a254..4ee6bfd 100644
--- a/server/api/v1/user/index.get.ts
+++ b/server/api/v1/user/index.get.ts
@@ -1,5 +1,8 @@
import aclManager from "~/server/internal/acls";
+/**
+ * Fetch user.
+ */
export default defineEventHandler(async (h3) => {
const user = await aclManager.getUserACL(h3, ["read"]);
return user ?? null; // Need to specifically return null
diff --git a/server/api/v1/user/token/[id]/index.delete.ts b/server/api/v1/user/token/[id]/index.delete.ts
index 54d1be1..778c996 100644
--- a/server/api/v1/user/token/[id]/index.delete.ts
+++ b/server/api/v1/user/token/[id]/index.delete.ts
@@ -2,16 +2,15 @@ import { APITokenMode } from "~/prisma/client/enums";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Revoke token
+ * @param id Token ID
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
if (!userId) throw createError({ statusCode: 403 });
- const id = h3.context.params?.id;
- if (!id)
- throw createError({
- statusCode: 400,
- statusMessage: "No id in router params",
- });
+ const id = getRouterParam(h3, "id")!;
const deleted = await prisma.aPIToken.delete({
where: { id: id, userId: userId, mode: APITokenMode.User },
diff --git a/server/api/v1/user/token/acls.get.ts b/server/api/v1/user/token/acls.get.ts
index d118809..05a1ca9 100644
--- a/server/api/v1/user/token/acls.get.ts
+++ b/server/api/v1/user/token/acls.get.ts
@@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import { userACLDescriptions } from "~/server/internal/acls/descriptions";
+/**
+ * Fetch ACL descriptions.
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/user/token/index.get.ts b/server/api/v1/user/token/index.get.ts
index cff1b85..cc04eb7 100644
--- a/server/api/v1/user/token/index.get.ts
+++ b/server/api/v1/user/token/index.get.ts
@@ -2,6 +2,9 @@ import { APITokenMode } from "~/prisma/client/enums";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+/**
+ * Fetch all API tokens for this account.
+ */
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
if (!userId) throw createError({ statusCode: 403 });
diff --git a/server/api/v1/user/token/index.post.ts b/server/api/v1/user/token/index.post.ts
index b5fb661..921cb6a 100644
--- a/server/api/v1/user/token/index.post.ts
+++ b/server/api/v1/user/token/index.post.ts
@@ -1,46 +1,44 @@
+import { type } from "arktype";
import { APITokenMode } from "~/prisma/client/enums";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager, { userACLs } from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
-export default defineEventHandler(async (h3) => {
- const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
- if (!userId) throw createError({ statusCode: 403 });
+const CreateToken = type({
+ name: "string",
+ acls: "string[] > 0",
+}).configure(throwingArktype);
- const body = await readBody(h3);
- const name: string = body.name;
- const acls: string[] = body.acls;
+/**
+ *
+ */
+export default defineEventHandler<{ body: typeof CreateToken.infer }>(
+ async (h3) => {
+ const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
+ if (!userId) throw createError({ statusCode: 403 });
- if (!name || typeof name !== "string")
- throw createError({
- statusCode: 400,
- statusMessage: "Token name required",
- });
- if (!acls || !Array.isArray(acls))
- throw createError({ statusCode: 400, statusMessage: "ACLs required" });
+ const body = await readDropValidatedBody(h3, CreateToken);
+ const name: string = body.name;
+ const acls: string[] = body.acls;
- if (acls.length == 0)
- throw createError({
- statusCode: 400,
- statusMessage: "Token requires more than zero ACLs",
+ const invalidACLs = acls.filter(
+ (e) => userACLs.findIndex((v) => e == v) == -1,
+ );
+ if (invalidACLs.length > 0)
+ throw createError({
+ statusCode: 400,
+ statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`,
+ });
+
+ const token = await prisma.aPIToken.create({
+ data: {
+ mode: APITokenMode.User,
+ name: name,
+ userId: userId,
+ acls: acls,
+ },
});
- const invalidACLs = acls.filter(
- (e) => userACLs.findIndex((v) => e == v) == -1,
- );
- if (invalidACLs.length > 0)
- throw createError({
- statusCode: 400,
- statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`,
- });
-
- const token = await prisma.aPIToken.create({
- data: {
- mode: APITokenMode.User,
- name: name,
- userId: userId,
- acls: acls,
- },
- });
-
- return token;
-});
+ return token;
+ },
+);
diff --git a/server/api/v2/client/chunk.post.ts b/server/api/v2/client/chunk.post.ts
index af67c07..7589367 100644
--- a/server/api/v2/client/chunk.post.ts
+++ b/server/api/v2/client/chunk.post.ts
@@ -11,63 +11,70 @@ const GetChunk = type({
}).array(),
}).configure(throwingArktype);
-export default defineEventHandler(async (h3) => {
- const body = await readDropValidatedBody(h3, GetChunk);
+/**
+ * Part of v2 download API. Intended to be client-only.
+ *
+ * Returns raw stream of all files requested, in order.
+ */
+export default defineEventHandler<{ body: typeof GetChunk.infer }>(
+ async (h3) => {
+ const body = await readDropValidatedBody(h3, GetChunk);
- const context = await contextManager.fetchContext(body.context);
- if (!context)
- throw createError({
- statusCode: 400,
- statusMessage: "Invalid download context.",
- });
-
- const streamFiles = [];
-
- for (const file of body.files) {
- const manifestFile = context.manifest[file.filename];
- if (!manifestFile)
+ const context = await contextManager.fetchContext(body.context);
+ if (!context)
throw createError({
statusCode: 400,
- statusMessage: `Unknown file: ${file.filename}`,
+ statusMessage: "Invalid download context.",
});
- const start = manifestFile.lengths
- .slice(0, file.chunkIndex)
- .reduce((a, b) => a + b, 0);
- const end = start + manifestFile.lengths[file.chunkIndex];
+ const streamFiles = [];
- streamFiles.push({ filename: file.filename, start, end });
- }
+ for (const file of body.files) {
+ const manifestFile = context.manifest[file.filename];
+ if (!manifestFile)
+ throw createError({
+ statusCode: 400,
+ statusMessage: `Unknown file: ${file.filename}`,
+ });
- setHeader(
- h3,
- "Content-Lengths",
- streamFiles.map((e) => e.end - e.start).join(","),
- ); // Non-standard header, but we're cool like that 😎
+ const start = manifestFile.lengths
+ .slice(0, file.chunkIndex)
+ .reduce((a, b) => a + b, 0);
+ const end = start + manifestFile.lengths[file.chunkIndex];
- for (const file of streamFiles) {
- const gameReadStream = await libraryManager.readFile(
- context.libraryId,
- context.libraryPath,
- context.versionName,
- file.filename,
- { start: file.start, end: file.end },
- );
- if (!gameReadStream)
- throw createError({
- statusCode: 500,
- statusMessage: "Failed to create read stream",
- });
- await gameReadStream.pipeTo(
- new WritableStream({
- write(chunk) {
- h3.node.res.write(chunk);
- },
- }),
- );
- }
+ streamFiles.push({ filename: file.filename, start, end });
+ }
- await h3.node.res.end();
+ setHeader(
+ h3,
+ "Content-Lengths",
+ streamFiles.map((e) => e.end - e.start).join(","),
+ ); // Non-standard header, but we're cool like that 😎
- return;
-});
+ for (const file of streamFiles) {
+ const gameReadStream = await libraryManager.readFile(
+ context.libraryId,
+ context.libraryPath,
+ context.versionName,
+ file.filename,
+ { start: file.start, end: file.end },
+ );
+ if (!gameReadStream)
+ throw createError({
+ statusCode: 500,
+ statusMessage: "Failed to create read stream",
+ });
+ await gameReadStream.pipeTo(
+ new WritableStream({
+ write(chunk) {
+ h3.node.res.write(chunk);
+ },
+ }),
+ );
+ }
+
+ await h3.node.res.end();
+
+ return;
+ },
+);
diff --git a/server/api/v2/client/context.post.ts b/server/api/v2/client/context.post.ts
index e54356a..c1f5c84 100644
--- a/server/api/v2/client/context.post.ts
+++ b/server/api/v2/client/context.post.ts
@@ -8,15 +8,20 @@ const CreateContext = type({
version: "string",
}).configure(throwingArktype);
-export default defineClientEventHandler(async (h3) => {
- const body = await readDropValidatedBody(h3, CreateContext);
+/**
+ * Part of v2 download API. Create a download context for use with `/api/v2/client/chunk`.
+ */
+export default defineClientEventHandler<{ body: typeof CreateContext.infer }>(
+ async (h3) => {
+ const body = await readDropValidatedBody(h3, CreateContext);
- const context = await contextManager.createContext(body.game, body.version);
- if (!context)
- throw createError({
- statusCode: 400,
- statusMessage: "Invalid game or version",
- });
+ const context = await contextManager.createContext(body.game, body.version);
+ if (!context)
+ throw createError({
+ statusCode: 400,
+ statusMessage: "Invalid game or version",
+ });
- return { context };
-});
+ return { context };
+ },
+);
diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts
index d6997c5..3d21fb5 100644
--- a/server/internal/acls/index.ts
+++ b/server/internal/acls/index.ts
@@ -184,9 +184,6 @@ class ACLManager {
if (!token) return false;
if (token.mode != APITokenMode.System) return false;
- // If empty, we just want to check we are an admin *at all*, not specific ACLs
- if (acls.length == 0) return true;
-
for (const acl of acls) {
const tokenACLIndex = token.acls.findIndex((e) => e == acl);
if (tokenACLIndex != -1) return true;
diff --git a/server/internal/clients/event-handler.ts b/server/internal/clients/event-handler.ts
index dfbdf0d..9587aa6 100644
--- a/server/internal/clients/event-handler.ts
+++ b/server/internal/clients/event-handler.ts
@@ -17,7 +17,9 @@ type ClientUtils = {
const NONCE_LENIENCE = 30_000;
-export function defineClientEventHandler(handler: EventHandlerFunction) {
+export function defineClientEventHandler(
+ handler: (h3: H3Event, utils: ClientUtils) => Promise | K,
+) {
return defineEventHandler(async (h3) => {
const header = getHeader(h3, "Authorization");
if (!header) throw createError({ statusCode: 403 });
diff --git a/server/internal/news/index.ts b/server/internal/news/index.ts
index 00725eb..16566cc 100644
--- a/server/internal/news/index.ts
+++ b/server/internal/news/index.ts
@@ -46,9 +46,9 @@ class NewsManager {
options: {
take?: number;
skip?: number;
- orderBy?: "asc" | "desc";
+ orderBy?: "asc" | "desc" | undefined;
tags?: string[];
- search?: string;
+ search?: string | undefined;
} = {},
) {
return await prisma.article.findMany({