Depot API & v4 (#298)

* feat: nginx + torrential basics & services system

* fix: lint + i18n

* fix: update torrential to remove openssl

* feat: add torrential to Docker build

* feat: move to self hosted runner

* fix: move off self-hosted runner

* fix: update nginx.conf

* feat: torrential cache invalidation

* fix: update torrential for cache invalidation

* feat: integrity check task

* fix: lint

* feat: move to version ids

* fix: client fixes and client-side checks

* feat: new depot apis and version id fixes

* feat: update torrential

* feat: droplet bump and remove unsafe update functions

* fix: lint

* feat: v4 featureset: emulators, multi-launch commands

* fix: lint

* fix: mobile ui for game editor

* feat: launch options

* fix: lint

* fix: remove axios, use $fetch

* feat: metadata and task api improvements

* feat: task actions

* fix: slight styling issue

* feat: fix style and lints

* feat: totp backend routes

* feat: oidc groups

* fix: update drop-base

* feat: creation of passkeys & totp

* feat: totp signin

* feat: webauthn mfa/signin

* feat: launch selecting ui

* fix: manually running tasks

* feat: update add company game modal to use new SelectorGame

* feat: executor selector

* fix(docker): update rust to rust nightly for torrential build (#305)

* feat: new version ui

* feat: move package lookup to build time to allow for deno dev

* fix: lint

* feat: localisation cleanup

* feat: apply localisation cleanup

* feat: potential i18n refactor logic

* feat: remove args from commands

* fix: lint

* fix: lockfile

---------

Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
This commit is contained in:
DecDuck
2026-01-13 15:32:39 +11:00
committed by GitHub
parent 8ef983304c
commit 63ac2b8ffc
190 changed files with 5848 additions and 2309 deletions
+95 -12
View File
@@ -1,9 +1,67 @@
import type { GameVersion } from "~/prisma/client/client";
import type { GameVersion, Prisma } from "~/prisma/client/client";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
async function getGameVersionSize<
T extends Omit<GameVersion, "dropletManifest">,
>(gameId: string, version: T) {
const size = await libraryManager.getGameVersionSize(
gameId,
version.versionId,
);
return { ...version, size };
}
export type AdminFetchGameType = Prisma.GameGetPayload<{
include: {
versions: {
include: {
setups: true;
launches: {
include: {
executor: {
include: {
gameVersion: {
select: {
versionId: true;
displayName: true;
versionPath: true;
game: {
select: {
id: true;
mName: true;
mIconObjectId: true;
};
};
};
};
};
};
executions: {
select: {
launchId: true;
};
};
};
};
};
omit: {
dropletManifest: true;
};
};
tags: true;
};
}>;
// Types in the route ensure we actually return the value as defined above
export default defineEventHandler<
{ body: never },
Promise<{
game: AdminFetchGameType;
unimportedVersions: string[] | undefined;
}>
>(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
@@ -15,12 +73,42 @@ export default defineEventHandler(async (h3) => {
},
include: {
versions: {
orderBy: {
versionIndex: "asc",
include: {
setups: true,
launches: {
include: {
executor: {
include: {
gameVersion: {
select: {
versionId: true,
displayName: true,
versionPath: true,
game: {
select: {
id: true,
mName: true,
mIconObjectId: true,
},
},
},
},
},
},
executions: {
select: {
launchId: true,
},
},
},
},
},
omit: {
dropletManifest: true,
},
orderBy: {
versionIndex: "asc",
},
},
tags: true,
},
@@ -29,16 +117,11 @@ export default defineEventHandler(async (h3) => {
if (!game || !game.libraryId)
throw createError({ statusCode: 404, statusMessage: "Game ID not found" });
const getGameVersionSize = async (version: GameVersion) => {
const size = await libraryManager.getGameVersionSize(
gameId,
version.versionName,
);
return { ...version, size };
};
const gameWithVersionSize = {
...game,
versions: await Promise.all(game.versions.map(getGameVersionSize)),
versions: await Promise.all(
game.versions.map((v) => getGameVersionSize(gameId, v)),
),
};
const unimportedVersions = await libraryManager.fetchUnimportedGameVersions(
+12 -7
View File
@@ -11,13 +11,18 @@ export default defineEventHandler(async (h3) => {
const restOfTheBody = { ...body };
delete restOfTheBody["id"];
const newObj = await prisma.game.update({
where: {
id: id,
},
data: restOfTheBody,
// I would put a select here, but it would be based on the body, and muck up the types
});
const newObj = (
await prisma.game.updateManyAndReturn({
where: {
id: id,
},
data: restOfTheBody,
// I would put a select here, but it would be based on the body, and muck up the types
})
).at(0);
if (!newObj)
throw createError({ statusCode: 404, message: "Game not found" });
return newObj;
});
+11 -6
View File
@@ -52,12 +52,17 @@ export default defineEventHandler(async (h3) => {
}
}
const newObject = await prisma.game.update({
where: {
id: gameId,
},
data: updateModel,
});
const newObject = (
await prisma.game.updateManyAndReturn({
where: {
id: gameId,
},
data: updateModel,
})
).at(0);
if (!newObject)
throw createError({ statusCode: 404, message: "Game not found" });
return newObject;
});
@@ -14,6 +14,14 @@ export default defineEventHandler(async (h3) => {
const body = await readDropValidatedBody(h3, PatchTags);
const id = getRouterParam(h3, "id")!;
const game = await prisma.game.findUnique({
where: { id },
select: { id: true },
});
if (!game) throw createError({ statusCode: 404, message: "Game not found" });
// SAFETY: Okay to disable due to check above
// eslint-disable-next-line drop/no-prisma-delete
await prisma.game.update({
where: {
id,
@@ -4,8 +4,7 @@ import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
const DeleteVersion = type({
id: "string",
versionName: "string",
version: "string",
}).configure(throwingArktype);
export default defineEventHandler<{ body: typeof DeleteVersion }>(
@@ -17,8 +16,8 @@ export default defineEventHandler<{ body: typeof DeleteVersion }>(
const body = await readDropValidatedBody(h3, DeleteVersion);
const gameId = body.id.toString();
const version = body.versionName.toString();
const gameId = getRouterParam(h3, "id")!;
const version = body.version.toString();
await libraryManager.deleteGameVersion(gameId, version);
return {};
@@ -0,0 +1,35 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const id = getRouterParam(h3, "id")!;
const game = await prisma.game.findUnique({
where: {
id,
},
select: {
versions: {
select: {
versionId: true,
displayName: true,
versionPath: true,
launches: {
select: {
launchId: true,
command: true,
name: true,
platform: true,
},
},
},
},
},
});
if (!game) throw createError({ statusCode: 404, message: "Game not found" });
return game.versions;
});
@@ -0,0 +1,67 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
const UpdateVersionOrder = type({
versions: "string[]",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:version:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, UpdateVersionOrder);
const gameId = getRouterParam(h3, "id")!;
// We expect an array of the version names for this game
const unsortedVersions = await prisma.gameVersion.findMany({
where: {
versionId: { in: body.versions },
},
select: {
versionId: true,
versionIndex: true,
delta: true,
launches: { select: { platform: true } },
},
});
const versions = body.versions
.map((e) => unsortedVersions.find((v) => v.versionId === 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) {
for (const versionPlatform of version.launches.map((v) => v.platform)) {
if (version.delta && !has[versionPlatform])
throw createError({
statusCode: 400,
statusMessage: `"${version.versionId}" requires a base version to apply the delta to for platform ${versionPlatform}.`,
});
has[versionPlatform] = true;
}
}
await prisma.$transaction(
versions.map((version, versionIndex) =>
prisma.gameVersion.updateMany({
where: {
gameId: gameId,
versionId: version.versionId,
},
data: {
versionIndex: versionIndex,
},
}),
),
);
return versions.map((v) => v.versionId);
});
+17 -15
View File
@@ -48,21 +48,23 @@ export default defineEventHandler<{
game.mCoverObjectId = game.mImageLibraryObjectIds[0];
}
const result = await prisma.game.update({
where: {
id: gameId,
},
data: {
mBannerObjectId: game.mBannerObjectId,
mImageLibraryObjectIds: game.mImageLibraryObjectIds,
mCoverObjectId: game.mCoverObjectId,
},
select: {
mBannerObjectId: true,
mImageLibraryObjectIds: true,
mCoverObjectId: true,
},
});
const result = (
await prisma.game.updateManyAndReturn({
where: {
id: gameId,
},
data: {
mBannerObjectId: game.mBannerObjectId,
mImageLibraryObjectIds: game.mImageLibraryObjectIds,
mCoverObjectId: game.mCoverObjectId,
},
select: {
mBannerObjectId: true,
mImageLibraryObjectIds: true,
mCoverObjectId: true,
},
})
).at(0);
return result;
});
+11 -9
View File
@@ -42,16 +42,18 @@ export default defineEventHandler(async (h3) => {
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
}
const result = await prisma.game.update({
where: {
id: gameId,
},
data: {
mImageLibraryObjectIds: {
push: ids,
const result = (
await prisma.game.updateManyAndReturn({
where: {
id: gameId,
},
},
});
data: {
mImageLibraryObjectIds: {
push: ids,
},
},
})
).at(0);
await pull();
return result;
@@ -1,72 +0,0 @@
import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
const UpdateVersionOrder = type({
id: "string",
versions: "string[]",
}).configure(throwingArktype);
export default defineEventHandler<{ body: typeof UpdateVersionOrder }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:version:update",
]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, UpdateVersionOrder);
const gameId = body.id;
// We expect an array of the version names for this game
const unsortedVersions = await prisma.gameVersion.findMany({
where: {
versionName: { in: body.versions },
},
select: {
versionName: true,
versionIndex: true,
delta: true,
platform: true,
},
});
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: version.versionName,
},
},
data: {
versionIndex: versionIndex,
},
}),
),
);
return versions;
},
);