-
-
-
- {{ $t("settings.admin.title") }}
-
-
- {{ $t("settings.admin.description") }}
-
-
+
-
+
+ {{ allowSave ? $t("common.save") : $t("common.saved") }}
+
+
diff --git a/prisma/migrations/20250823005001_add_api_token_expiry/migration.sql b/prisma/migrations/20250823005001_add_api_token_expiry/migration.sql
new file mode 100644
index 0000000..c028e99
--- /dev/null
+++ b/prisma/migrations/20250823005001_add_api_token_expiry/migration.sql
@@ -0,0 +1,8 @@
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "APIToken" ADD COLUMN "expiresAt" TIMESTAMP(3);
+
+-- CreateIndex
+CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
diff --git a/prisma/models/auth.prisma b/prisma/models/auth.prisma
index 4b03c7c..8749cf6 100644
--- a/prisma/models/auth.prisma
+++ b/prisma/models/auth.prisma
@@ -45,6 +45,8 @@ model APIToken {
acls String[]
+ expiresAt DateTime?
+
@@index([token])
}
diff --git a/server/api/v1/admin/game/[id]/index.get.ts b/server/api/v1/admin/game/[id]/index.get.ts
index 44693dc..faf0f1c 100644
--- a/server/api/v1/admin/game/[id]/index.get.ts
+++ b/server/api/v1/admin/game/[id]/index.get.ts
@@ -17,11 +17,8 @@ export default defineEventHandler(async (h3) => {
orderBy: {
versionIndex: "asc",
},
- select: {
- versionIndex: true,
- versionName: true,
- platform: true,
- delta: true,
+ omit: {
+ dropletManifest: true,
},
},
tags: true,
diff --git a/server/api/v1/admin/game/version/index.patch.ts b/server/api/v1/admin/game/version/index.patch.ts
index 3833840..54e0547 100644
--- a/server/api/v1/admin/game/version/index.patch.ts
+++ b/server/api/v1/admin/game/version/index.patch.ts
@@ -18,30 +18,55 @@ export default defineEventHandler<{ body: typeof UpdateVersionOrder }>(
const body = await readDropValidatedBody(h3, UpdateVersionOrder);
const gameId = body.id;
// We expect an array of the version names for this game
- const versions = body.versions;
+ const unsortedVersions = await prisma.gameVersion.findMany({
+ where: {
+ versionName: { in: body.versions },
+ },
+ select: {
+ versionName: true,
+ versionIndex: true,
+ delta: true,
+ platform: true,
+ },
+ });
- const newVersions = await prisma.$transaction(
- versions.map((versionName, versionIndex) =>
+ const versions = body.versions
+ .map((e) => unsortedVersions.find((v) => v.versionName === e))
+ .filter((e) => e !== undefined);
+
+ if (versions.length !== unsortedVersions.length)
+ throw createError({
+ statusCode: 500,
+ statusMessage: "Sorting versions yielded less results, somehow.",
+ });
+
+ // Validate the new order
+ const has: { [key: string]: boolean } = {};
+ for (const version of versions) {
+ if (version.delta && !has[version.platform])
+ throw createError({
+ statusCode: 400,
+ statusMessage: `"${version.versionName}" requires a base version to apply the delta to.`,
+ });
+ has[version.platform] = true;
+ }
+
+ await prisma.$transaction(
+ versions.map((version, versionIndex) =>
prisma.gameVersion.update({
where: {
gameId_versionName: {
gameId: gameId,
- versionName: versionName,
+ versionName: version.versionName,
},
},
data: {
versionIndex: versionIndex,
},
- select: {
- versionIndex: true,
- versionName: true,
- platform: true,
- delta: true,
- },
}),
),
);
- return newVersions;
+ return versions;
},
);
diff --git a/server/api/v1/admin/token/[id]/index.delete.ts b/server/api/v1/admin/token/[id]/index.delete.ts
new file mode 100644
index 0000000..1e33730
--- /dev/null
+++ b/server/api/v1/admin/token/[id]/index.delete.ts
@@ -0,0 +1,23 @@
+import { APITokenMode } from "~/prisma/client/enums";
+import aclManager from "~/server/internal/acls";
+import prisma from "~/server/internal/db/database";
+
+export default defineEventHandler(async (h3) => {
+ const allowed = await aclManager.allowSystemACL(h3, []); // No ACLs only allows session authentication
+ if (!allowed) throw createError({ statusCode: 403 });
+
+ const id = h3.context.params?.id;
+ if (!id)
+ throw createError({
+ statusCode: 400,
+ statusMessage: "No id in router params",
+ });
+
+ const deleted = await prisma.aPIToken.delete({
+ where: { id: id, mode: APITokenMode.System },
+ })!;
+ if (!deleted)
+ throw createError({ statusCode: 404, statusMessage: "Token not found" });
+
+ return;
+});
diff --git a/server/api/v1/admin/token/acls.get.ts b/server/api/v1/admin/token/acls.get.ts
new file mode 100644
index 0000000..09b4e0f
--- /dev/null
+++ b/server/api/v1/admin/token/acls.get.ts
@@ -0,0 +1,9 @@
+import aclManager from "~/server/internal/acls";
+import { systemACLDescriptions } from "~/server/internal/acls/descriptions";
+
+export default defineEventHandler(async (h3) => {
+ const allowed = await aclManager.allowSystemACL(h3, []); // No ACLs only allows session authentication
+ if (!allowed) throw createError({ statusCode: 403 });
+
+ return systemACLDescriptions;
+});
diff --git a/server/api/v1/admin/token/index.get.ts b/server/api/v1/admin/token/index.get.ts
new file mode 100644
index 0000000..2438a4b
--- /dev/null
+++ b/server/api/v1/admin/token/index.get.ts
@@ -0,0 +1,15 @@
+import { APITokenMode } from "~/prisma/client/enums";
+import aclManager from "~/server/internal/acls";
+import prisma from "~/server/internal/db/database";
+
+export default defineEventHandler(async (h3) => {
+ const allowed = await aclManager.allowSystemACL(h3, []); // No ACLs only allows session authentication
+ if (!allowed) throw createError({ statusCode: 403 });
+
+ const tokens = await prisma.aPIToken.findMany({
+ where: { mode: APITokenMode.System },
+ omit: { token: true },
+ });
+
+ return tokens;
+});
diff --git a/server/api/v1/admin/token/index.post.ts b/server/api/v1/admin/token/index.post.ts
new file mode 100644
index 0000000..421fe8d
--- /dev/null
+++ b/server/api/v1/admin/token/index.post.ts
@@ -0,0 +1,38 @@
+import { type } from "arktype";
+import { APITokenMode } from "~/prisma/client/enums";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
+import aclManager, { systemACLs } from "~/server/internal/acls";
+import prisma from "~/server/internal/db/database";
+
+const CreateToken = type({
+ name: "string",
+ acls: "string[] > 0",
+ expiry: "string.date.iso.parse?",
+}).configure(throwingArktype);
+
+export default defineEventHandler(async (h3) => {
+ const allowed = await aclManager.allowSystemACL(h3, []); // No ACLs only allows session authentication
+ if (!allowed) throw createError({ statusCode: 403 });
+
+ const body = await readDropValidatedBody(h3, CreateToken);
+
+ const invalidACLs = body.acls.filter(
+ (e) => systemACLs.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.System,
+ name: body.name,
+ acls: body.acls,
+ expiresAt: body.expiry ?? null,
+ },
+ });
+
+ return token;
+});
diff --git a/server/api/v1/token.get.ts b/server/api/v1/token.get.ts
new file mode 100644
index 0000000..1ce5320
--- /dev/null
+++ b/server/api/v1/token.get.ts
@@ -0,0 +1,6 @@
+import aclManager from "~/server/internal/acls";
+
+export default defineEventHandler(async (h3) => {
+ const acls = await aclManager.fetchAllACLs(h3);
+ return acls;
+});
diff --git a/server/api/v1/user/token/index.post.ts b/server/api/v1/user/token/index.post.ts
index b5fb661..aafacf3 100644
--- a/server/api/v1/user/token/index.post.ts
+++ b/server/api/v1/user/token/index.post.ts
@@ -1,30 +1,22 @@
+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";
+const CreateToken = type({
+ name: "string",
+ acls: "string[] > 0",
+ expiry: "string.date.iso.parse?",
+}).configure(throwingArktype);
+
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
if (!userId) throw createError({ statusCode: 403 });
- const body = await readBody(h3);
- const name: string = body.name;
- const acls: string[] = body.acls;
+ const body = await readDropValidatedBody(h3, CreateToken);
- 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" });
-
- if (acls.length == 0)
- throw createError({
- statusCode: 400,
- statusMessage: "Token requires more than zero ACLs",
- });
-
- const invalidACLs = acls.filter(
+ const invalidACLs = body.acls.filter(
(e) => userACLs.findIndex((v) => e == v) == -1,
);
if (invalidACLs.length > 0)
@@ -36,9 +28,10 @@ export default defineEventHandler(async (h3) => {
const token = await prisma.aPIToken.create({
data: {
mode: APITokenMode.User,
- name: name,
+ name: body.name,
userId: userId,
- acls: acls,
+ acls: body.acls,
+ expiresAt: body.expiry ?? null,
},
});
diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts
index 1f090a1..af1a007 100644
--- a/server/internal/acls/descriptions.ts
+++ b/server/internal/acls/descriptions.ts
@@ -36,7 +36,7 @@ export const userACLDescriptions: ObjectFromList
= {
"library:remove": "Remove a game from your library.",
"clients:read": "Read the clients connected to this account",
- "clients:revoke": "",
+ "clients:revoke": "Remove clients connected to this account",
"news:read": "Read the server's news articles.",