11 Commits

133 changed files with 1388 additions and 902 deletions

View File

@ -2,14 +2,10 @@
<div class="min-h-screen flex flex-col">
<div class="grow grid grid-cols-1 lg:grid-cols-2">
<div class="border-b lg:border-b-0 lg:border-r border-zinc-700">
<header
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
>
<header class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8">
<DropWordmark />
</header>
<main
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
>
<main class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8">
<div>
<h1 class="text-4xl font-display font-bold text-zinc-100">
{{ $t("setup.welcome") }}
@ -20,32 +16,19 @@
</p>
</div>
<ul role="list" class="mt-10 divide-y divide-zinc-700/5">
<li
v-for="(action, actionIdx) in actions"
:key="action.name"
class="relative flex gap-x-6 py-6"
>
<li v-for="(action, actionIdx) in actions" :key="action.name" class="relative flex gap-x-6 py-6">
<div
class="flex size-10 flex-none items-center justify-center rounded-lg shadow-xs outline-1 outline-zinc-100/10"
>
<component
:is="action.icon"
v-if="!actionsComplete[actionIdx]"
class="size-6 text-blue-500"
aria-hidden="true"
/>
class="flex size-10 flex-none items-center justify-center rounded-lg shadow-xs outline-1 outline-zinc-100/10">
<component :is="action.icon" v-if="!actionsComplete[actionIdx]" class="size-6 text-blue-500"
aria-hidden="true" />
<CheckIcon v-else class="size-6 text-blue-500" />
</div>
<div class="flex-auto">
<h3 class="text-sm/6 font-semibold text-zinc-100">
<button
:class="
actionsComplete[actionIdx]
? 'line-through text-zinc-300'
: ''
"
@click="() => (currentAction = actionIdx)"
>
<button :class="actionsComplete[actionIdx]
? 'line-through text-zinc-300'
: ''
" @click="() => (currentAction = actionIdx)">
<span class="absolute inset-0" aria-hidden="true" />
{{ action.name }}
</button>
@ -55,18 +38,11 @@
</p>
</div>
<div class="flex-none self-center">
<ChevronRightIcon
class="size-5 text-gray-400"
aria-hidden="true"
/>
<ChevronRightIcon class="size-5 text-gray-400" aria-hidden="true" />
</div>
</li>
</ul>
<LoadingButton
:disabled="!finished"
:loading="finishLoading"
@click="() => finish()"
>
<LoadingButton :disabled="!finished" :loading="finishLoading" @click="() => finish()">
<i18n-t keypath="setup.finish" tag="span" scope="global">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
@ -75,25 +51,13 @@
</LoadingButton>
</main>
</div>
<component
:is="actions[currentAction].page"
v-if="actions[currentAction] && !useModal"
v-model="actionsComplete[currentAction]"
:token="bearerToken"
/>
<div
v-else-if="!useModal"
class="bg-zinc-950/30 flex items-center justify-center"
>
<component :is="actions[currentAction].page" v-if="actions[currentAction] && !useModal"
v-model="actionsComplete[currentAction]" :token="bearerToken" />
<div v-else-if="!useModal" class="bg-zinc-950/30 flex items-center justify-center">
<!-- <p class="uppercase text-sm font-display text-zinc-700 font-bold">
{{ $t("setup.noPage") }}
</p> -->
<img
src="/wallpapers/signin.jpg"
class="inset-0 h-full w-full object-cover"
alt=""
preload
/>
<img src="/wallpapers/signin.jpg" class="inset-0 h-full w-full object-cover" alt="" preload />
</div>
</div>
<div>
@ -102,25 +66,17 @@
<div class="fixed inset-0 bg-zinc-900/75 transition-opacity" />
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div
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"
>
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">
<div>
<component
:is="actions[currentAction].page"
v-model="actionsComplete[currentAction]"
:token="bearerToken"
/>
<component :is="actions[currentAction].page" v-model="actionsComplete[currentAction]"
:token="bearerToken" />
</div>
<div class="mt-5 sm:mt-6 p-4">
<button
type="button"
<button type="button"
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
@click="currentAction = -1"
>
@click="currentAction = -1">
{{ $t("common.close") }}
</button>
</div>
@ -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)

View File

@ -2,6 +2,9 @@ import { AuthMec } from "~/prisma/client/enums";
import aclManager from "~/server/internal/acls";
import authManager from "~/server/internal/auth";
/**
* Fetches all the enabled authentication mechanisms on this instance, and their configuration, if enabled.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["auth:read", "setup"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -7,6 +7,10 @@ const DeleteInvite = type({
id: "string",
}).configure(throwingArktype);
/**
* Deletes a "Simple" invitation
* @returns nothing
*/
export default defineEventHandler<{
body: typeof DeleteInvite.infer;
}>(async (h3) => {

View File

@ -3,6 +3,9 @@ import { systemConfig } from "~/server/internal/config/sys-conf";
import prisma from "~/server/internal/db/database";
import taskHandler from "~/server/internal/tasks";
/**
* Fetches a "Simple" invitation
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"auth:simple:invitation:read",

View File

@ -11,6 +11,9 @@ const CreateInvite = SharedRegisterValidator.partial()
})
.configure(throwingArktype);
/**
* Creates a "Simple" invitation
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"auth:simple:invitation:new",

View File

@ -3,6 +3,11 @@ import prisma from "~/server/internal/db/database";
import objectHandler from "~/server/internal/objects";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
/**
* Multi-part form upload for the banner.
* @request `multipart/form-data` data. Only one file, can be named anything.
* @param id Company ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -7,31 +7,37 @@ const GameDelete = type({
id: "string",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Delete a game's association with a company
* @param id Company ID
*/
export default defineEventHandler<{ body: typeof GameDelete.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const companyId = getRouterParam(h3, "id")!;
const companyId = getRouterParam(h3, "id")!;
const body = await readDropValidatedBody(h3, GameDelete);
const body = await readDropValidatedBody(h3, GameDelete);
await prisma.game.update({
where: {
id: body.id,
},
data: {
publishers: {
disconnect: {
id: companyId,
await prisma.game.update({
where: {
id: body.id,
},
data: {
publishers: {
disconnect: {
id: companyId,
},
},
developers: {
disconnect: {
id: companyId,
},
},
},
developers: {
disconnect: {
id: companyId,
},
},
},
});
});
return;
});
return;
},
);

View File

@ -9,29 +9,35 @@ const GamePatch = type({
id: "string",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Update a company's association with a game.
* @param id Company ID
*/
export default defineEventHandler<{ body: typeof GamePatch.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const companyId = getRouterParam(h3, "id")!;
const companyId = getRouterParam(h3, "id")!;
const body = await readDropValidatedBody(h3, GamePatch);
const body = await readDropValidatedBody(h3, GamePatch);
const action = body.action === "developed" ? "developers" : "publishers";
const actionType = body.enabled ? "connect" : "disconnect";
const action = body.action === "developed" ? "developers" : "publishers";
const actionType = body.enabled ? "connect" : "disconnect";
await prisma.game.update({
where: {
id: body.id,
},
data: {
[action]: {
[actionType]: {
id: companyId,
await prisma.game.update({
where: {
id: body.id,
},
data: {
[action]: {
[actionType]: {
id: companyId,
},
},
},
},
});
});
return;
});
return;
},
);

View File

@ -9,61 +9,67 @@ const GamePost = type({
id: "string",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Add a new game association to this company
* @param id Company ID
*/
export default defineEventHandler<{ body: typeof GamePost.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const companyId = getRouterParam(h3, "id")!;
const companyId = getRouterParam(h3, "id")!;
const body = await readDropValidatedBody(h3, GamePost);
const body = await readDropValidatedBody(h3, GamePost);
if (!body.published && !body.developed)
throw createError({
statusCode: 400,
statusMessage: "Must be related (either developed or published).",
if (!body.published && !body.developed)
throw createError({
statusCode: 400,
statusMessage: "Must be related (either developed or published).",
});
const publisherConnect = body.published
? {
publishers: {
connect: {
id: companyId,
},
},
}
: undefined;
const developerConnect = body.developed
? {
developers: {
connect: {
id: companyId,
},
},
}
: undefined;
const game = await prisma.game.update({
where: {
id: body.id,
},
data: {
...publisherConnect,
...developerConnect,
},
include: {
publishers: {
select: {
id: true,
},
},
developers: {
select: {
id: true,
},
},
},
});
const publisherConnect = body.published
? {
publishers: {
connect: {
id: companyId,
},
},
}
: undefined;
const developerConnect = body.developed
? {
developers: {
connect: {
id: companyId,
},
},
}
: undefined;
const game = await prisma.game.update({
where: {
id: body.id,
},
data: {
...publisherConnect,
...developerConnect,
},
include: {
publishers: {
select: {
id: true,
},
},
developers: {
select: {
id: true,
},
},
},
});
return game;
});
return game;
},
);

View File

@ -3,6 +3,11 @@ import prisma from "~/server/internal/db/database";
import objectHandler from "~/server/internal/objects";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
/**
* Multi-part form upload for the icon of this company
* @request `multipart/form-data` data. Only one file, can be named anything.
* @param id Company ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Delete this company
* @param id Company ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:delete"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Fetch a company and its associations
* @param id Company ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Update a company. Pass any fields into the body to be updated on the model
* @request Partial of the data returned by GET, minus the `developed` and `published` fields.
* @param id Company ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:update"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Fetch all companies on this instance
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -12,36 +12,41 @@ const CompanyCreate = type({
website: "string",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:create"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Create a new company on this instance
*/
export default defineEventHandler<{ body: typeof CompanyCreate.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["company:create"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, CompanyCreate);
const obj = new ObjectTransactionalHandler();
const [register, pull, _] = obj.new({}, ["internal:read"]);
const body = await readDropValidatedBody(h3, CompanyCreate);
const obj = new ObjectTransactionalHandler();
const [register, pull, _] = obj.new({}, ["internal:read"]);
const icon = jdenticon.toPng(body.name, 512);
const logoId = register(icon);
const icon = jdenticon.toPng(body.name, 512);
const logoId = register(icon);
const banner = jdenticon.toPng(body.description, 1024);
const bannerId = register(banner);
const banner = jdenticon.toPng(body.description, 1024);
const bannerId = register(banner);
const company = await prisma.company.create({
data: {
metadataSource: MetadataSource.Manual,
metadataId: crypto.randomUUID(),
metadataOriginalQuery: "",
const company = await prisma.company.create({
data: {
metadataSource: MetadataSource.Manual,
metadataId: crypto.randomUUID(),
metadataOriginalQuery: "",
mName: body.name,
mShortDescription: body.description,
mDescription: "",
mLogoObjectId: logoId,
mBannerObjectId: bannerId,
mWebsite: body.website,
},
});
mName: body.name,
mShortDescription: body.description,
mDescription: "",
mLogoObjectId: logoId,
mBannerObjectId: bannerId,
mWebsite: body.website,
},
});
await pull();
await pull();
return company;
});
return company;
},
);

View File

@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Delete a game from this instance
* @param id Game ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
/**
* Fetch a game by ID
* @param id Game ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,11 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Update a game's metadata
* @request Partial of data returned by GET
* @param id Game ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -3,6 +3,11 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
/**
* Update icon, name, and/or description
* @request `multipart/form-data`, any file will become the icon, and `name` and `description` will become their respective fields.
* @param id Game ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -7,23 +7,29 @@ const PatchTags = type({
tags: "string[]",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Update the tags associated with this game.
* @param id Game ID
*/
export default defineEventHandler<{ body: typeof PatchTags.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, PatchTags);
const id = getRouterParam(h3, "id")!;
const body = await readDropValidatedBody(h3, PatchTags);
const id = getRouterParam(h3, "id")!;
await prisma.game.update({
where: {
id,
},
data: {
tags: {
connect: body.tags.map((e) => ({ id: e })),
await prisma.game.update({
where: {
id,
},
},
});
data: {
tags: {
connect: body.tags.map((e) => ({ id: e })),
},
},
});
return;
});
return;
},
);

View File

@ -9,6 +9,9 @@ const DeleteGameImage = type({
imageId: "string",
}).configure(throwingArktype);
/**
* Delete a game's image
*/
export default defineEventHandler<{
body: typeof DeleteGameImage.infer;
}>(async (h3) => {

View File

@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
/**
* Upload a game to a game's image library
* @request `multipart/form-data`. All files will be uploaded as images. Set the game ID in the `id` field.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:image:new"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Fetch brief metadata on all games connected to this instance
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -8,7 +8,10 @@ const DeleteVersion = type({
versionName: "string",
}).configure(throwingArktype);
export default defineEventHandler<{ body: typeof DeleteVersion }>(
/**
* Delete a game's version
*/
export default defineEventHandler<{ body: typeof DeleteVersion.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:version:delete",

View File

@ -8,7 +8,10 @@ const UpdateVersionOrder = type({
versions: "string[]",
}).configure(throwingArktype);
export default defineEventHandler<{ body: typeof UpdateVersionOrder }>(
/**
* Update the version order of a game.
*/
export default defineEventHandler<{ body: typeof UpdateVersionOrder.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:version:update",

View File

@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
/**
* Fetch all games that are available for import.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -14,6 +14,10 @@ const ImportGameBody = type({
},
}).configure(throwingArktype);
/**
* Import a game as a background task.
* @response Task IDs can be used with the websocket endpoint /api/v1/task
*/
export default defineEventHandler<{ body: typeof ImportGameBody.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:game:new"]);

View File

@ -1,16 +1,30 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import metadataHandler from "~/server/internal/metadata";
const SearchGame = type({
q: "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof SearchGame.infer;
/**
* Search metadata providers for a query. Results can be used to import a game with metadata.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3);
const search = query.q?.toString();
if (!search)
throw createError({ statusCode: 400, statusMessage: "Invalid search" });
const search = SearchGame(query);
if (search instanceof ArkErrors)
throw createError({
statusCode: 400,
statusMessage: "Invalid search: " + search.summary,
});
const results = await metadataHandler.search(search);
const results = await metadataHandler.search(search.q);
if (results.length == 0)
throw createError({

View File

@ -1,19 +1,31 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
const Query = type({
id: "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof Query.inferIn;
/**
* Fetch all versions available for import for a game (`id` in query params).
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3);
const gameId = query.id?.toString();
if (!gameId)
const query = Query(await getQuery(h3));
if (query instanceof ArkErrors)
throw createError({
statusCode: 400,
statusMessage: "Missing id in request params",
statusMessage: "Invalid query params: " + query.summary,
});
const gameId = query.id;
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { libraryId: true, libraryPath: true },

View File

@ -19,71 +19,80 @@ const ImportVersion = type({
umuId: "string = ''",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:new"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Import a version for a game.
*/
export default defineEventHandler<{ body: typeof ImportVersion.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:new"]);
if (!allowed) throw createError({ statusCode: 403 });
const {
id,
version,
platform,
launch,
launchArgs,
setup,
setupArgs,
onlySetup,
delta,
umuId,
} = await readDropValidatedBody(h3, ImportVersion);
const {
id,
version,
platform,
launch,
launchArgs,
setup,
setupArgs,
onlySetup,
delta,
umuId,
} = await readDropValidatedBody(h3, ImportVersion);
const platformParsed = parsePlatform(platform);
if (!platformParsed)
throw createError({ statusCode: 400, statusMessage: "Invalid platform." });
const platformParsed = parsePlatform(platform);
if (!platformParsed)
throw createError({
statusCode: 400,
statusMessage: "Invalid platform.",
});
if (delta) {
const validOverlayVersions = await prisma.gameVersion.count({
where: { gameId: id, platform: platformParsed, delta: false },
if (delta) {
const validOverlayVersions = await prisma.gameVersion.count({
where: { gameId: id, platform: platformParsed, delta: false },
});
if (validOverlayVersions == 0)
throw createError({
statusCode: 400,
statusMessage:
"Update mode requires a pre-existing version for this platform.",
});
}
if (onlySetup) {
if (!setup)
throw createError({
statusCode: 400,
statusMessage: 'Setup required in "setup mode".',
});
} else {
if (!delta && !launch)
throw createError({
statusCode: 400,
statusMessage:
"Launch executable is required for non-update versions",
});
}
// startup & delta require more complex checking logic
const taskId = await libraryManager.importVersion(id, version, {
platform,
onlySetup,
launch,
launchArgs,
setup,
setupArgs,
umuId,
delta,
});
if (validOverlayVersions == 0)
if (!taskId)
throw createError({
statusCode: 400,
statusMessage:
"Update mode requires a pre-existing version for this platform.",
statusMessage: "Invalid options for import",
});
}
if (onlySetup) {
if (!setup)
throw createError({
statusCode: 400,
statusMessage: 'Setup required in "setup mode".',
});
} else {
if (!delta && !launch)
throw createError({
statusCode: 400,
statusMessage: "Launch executable is required for non-update versions",
});
}
// startup & delta require more complex checking logic
const taskId = await libraryManager.importVersion(id, version, {
platform,
onlySetup,
launch,
launchArgs,
setup,
setupArgs,
umuId,
delta,
});
if (!taskId)
throw createError({
statusCode: 400,
statusMessage: "Invalid options for import",
});
return { taskId: taskId };
});
return { taskId: taskId };
},
);

View File

@ -1,18 +1,30 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
const Query = type({
id: "string",
version: "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof Query.inferIn;
/**
* Fetch recommendations for version import.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3);
const gameId = query.id?.toString();
const versionName = query.version?.toString();
if (!gameId || !versionName)
const query = Query(await getQuery(h3));
if (query instanceof ArkErrors)
throw createError({
statusCode: 400,
statusMessage: "Missing id or version in request params",
statusMessage: "Invalid query: " + query.summary,
});
const gameId = query.id;
const versionName = query.version;
const preload = await libraryManager.fetchUnimportedVersionInformation(
gameId,

View File

@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
/**
* Fetch library data for admin UI
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["library:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -8,6 +8,9 @@ const DeleteLibrarySource = type({
id: "string",
}).configure(throwingArktype);
/**
* Delete a given library source
*/
export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [

View File

@ -4,6 +4,9 @@ import libraryManager from "~/server/internal/library";
export type WorkingLibrarySource = LibraryModel & { working: boolean };
/**
* Fetch all library sources on this instance
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"library:sources:read",

View File

@ -12,6 +12,9 @@ const UpdateLibrarySource = type({
options: "object",
}).configure(throwingArktype);
/**
* Update a library source's options. Validates options and live-updates the source.
*/
export default defineEventHandler<{ body: typeof UpdateLibrarySource.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [

View File

@ -14,6 +14,9 @@ const CreateLibrarySource = type({
options: "object",
}).configure(throwingArktype);
/**
* Create a new library source with options
*/
export default defineEventHandler<{ body: typeof CreateLibrarySource.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [

View File

@ -2,6 +2,10 @@ import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
/**
* Delete a news article
* @param id Article ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["news:delete"]);
if (!allowed)

View File

@ -2,6 +2,10 @@ import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
/**
* Fetch a single news article
* @param id Article ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["news:read"]);
if (!allowed)

View File

@ -2,6 +2,9 @@ import { defineEventHandler, getQuery } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
/**
* Fetch all news articles.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["news:read"]);
if (!allowed)

View File

@ -11,51 +11,56 @@ const CreateNews = type({
tags: "string = '[]'",
});
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["news:create"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Create a new news article
*/
export default defineEventHandler<{ body: typeof CreateNews.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["news:create"]);
if (!allowed) throw createError({ statusCode: 403 });
const form = await readMultipartFormData(h3);
if (!form)
throw createError({
statusCode: 400,
statusMessage: "This endpoint requires multipart form data.",
const form = await readMultipartFormData(h3);
if (!form)
throw createError({
statusCode: 400,
statusMessage: "This endpoint requires multipart form data.",
});
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
});
const [imageIds, options, pull, _dump] = uploadResult;
const body = await CreateNews(options);
if (body instanceof ArkErrors)
throw createError({ statusCode: 400, statusMessage: body.summary });
const parsedTags = JSON.parse(body.tags);
if (typeof parsedTags !== "object" || !Array.isArray(parsedTags))
throw createError({
statusCode: 400,
statusMessage: "Tags must be an array",
});
const imageId = imageIds.at(0);
const article = await newsManager.create({
title: body.title,
description: body.description,
content: body.content,
tags: parsedTags,
...(imageId && { imageObjectId: imageId }),
authorId: "system",
});
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
});
await pull();
const [imageIds, options, pull, _dump] = uploadResult;
const body = await CreateNews(options);
if (body instanceof ArkErrors)
throw createError({ statusCode: 400, statusMessage: body.summary });
const parsedTags = JSON.parse(body.tags);
if (typeof parsedTags !== "object" || !Array.isArray(parsedTags))
throw createError({
statusCode: 400,
statusMessage: "Tags must be an array",
});
const imageId = imageIds.at(0);
const article = await newsManager.create({
title: body.title,
description: body.description,
content: body.content,
tags: parsedTags,
...(imageId && { imageObjectId: imageId }),
authorId: "system",
});
await pull();
return article;
});
return article;
},
);

View File

@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Fetch dummy data for rendering the settings page.
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.getUserACL(h3, ["settings:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -8,6 +8,9 @@ const UpdateSettings = type({
showGamePanelTextDecoration: "boolean",
});
/**
* Update global Drop settings.
*/
export default defineEventHandler<{ body: typeof UpdateSettings.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["settings:update"]);

View File

@ -1,7 +1,10 @@
import aclManager from "~/server/internal/acls";
/**
* 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;
});

View File

@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Delete game tags.
* @param id Tag ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["tags:delete"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -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 allowed = await aclManager.allowSystemACL(h3, ["tags:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -7,16 +7,21 @@ const CreateTag = type({
name: "string",
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["tags:read"]);
if (!allowed) throw createError({ statusCode: 403 });
/**
* Create a game tag
*/
export default defineEventHandler<{ body: typeof CreateTag.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["tags:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readDropValidatedBody(h3, CreateTag);
const body = await readDropValidatedBody(h3, CreateTag);
const tag = await prisma.gameTag.create({
data: {
...body,
},
});
return tag;
});
const tag = await prisma.gameTag.create({
data: {
...body,
},
});
return tag;
},
);

View File

@ -2,6 +2,9 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import taskHandler from "~/server/internal/tasks";
/**
* Fetches all tasks that the current token has access to (ACL-based)
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["task:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -2,6 +2,10 @@ import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Delete a user
* @param id User ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["user:delete"]);
if (!allowed)

View File

@ -1,6 +1,10 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Fetch a user by ID
* @param id User ID
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["user:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,6 +1,9 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
/**
* Fetch all users and their enabled authentication mechanisms
*/
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["user:read"]);
if (!allowed) throw createError({ statusCode: 403 });

View File

@ -1,5 +1,8 @@
import authManager from "~/server/internal/auth";
/**
* Fetch public authentication provider mechanisms
*/
export default defineEventHandler(() => {
return authManager.getEnabledAuthProviders();
});

View File

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

View File

@ -1,7 +1,18 @@
import prisma from "~/server/internal/db/database";
import taskHandler from "~/server/internal/tasks";
import authManager from "~/server/internal/auth";
import { ArkErrors, type } from "arktype";
const Query = type({
id: "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof Query.inferIn;
/**
* Fetch invitation details for pre-filling
*/
export default defineEventHandler(async (h3) => {
const t = await useTranslation(h3);
@ -11,13 +22,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 } });

View File

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

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,25 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
const Query = type({
code: "string.upper",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof Query.inferIn;
/**
* Fetch client ID by authorize code
*/
export default defineEventHandler(async (h3) => {
const user = await sessionHandler.getSession(h3);
if (!user) throw createError({ statusCode: 403 });
const userId = await aclManager.getUserIdACL(h3, []);
if (!userId) throw createError({ statusCode: 403 });
const query = getQuery(h3);
const code = query.code?.toString()?.toUpperCase();
if (!code)
throw createError({
statusCode: 400,
statusMessage: "Code required in query params.",
});
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,25 @@
import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
const Query = type({
id: "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof Query.inferIn;
/**
* Fetch details about an authorization request, and claim it for the current user
*/
export default defineEventHandler(async (h3) => {
const user = await sessionHandler.getSession(h3);
if (!user) throw createError({ statusCode: 403 });
const userId = await aclManager.getUserIdACL(h3, []);
if (!userId) throw createError({ statusCode: 403 });
const query = getQuery(h3);
const providedClientId = query.id?.toString();
if (!providedClientId)
throw createError({
statusCode: 400,
statusMessage: "Provide client ID in request params as 'id'",
});
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 +28,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,12 +11,29 @@ const gameLookupCache = cacheHandler.createCache<{
libraryPath: string;
}>("downloadGameLookupCache");
const Query = type({
id: "string",
version: "string",
name: "string",
chunk: "string.numeric.parse",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof Query.inferIn;
/**
* v1 download API
* @deprecated
* @response Raw binary data (`application/octet-stream`)
*/
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() ?? "?");
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({
@ -35,7 +53,10 @@ export default defineClientEventHandler(async (h3) => {
},
});
if (!game || !game.libraryId)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
throw createError({
statusCode: 400,
statusMessage: "Invalid game ID",
});
await gameLookupCache.setItem(gameId, game);
}
@ -79,5 +100,7 @@ export default defineClientEventHandler(async (h3) => {
statusMessage: "Failed to create stream",
});
return sendStream(h3, gameReadStream);
await sendStream(h3, gameReadStream);
return;
});

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,27 +1,44 @@
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";
const NewsFetch = type({
"order?": "'asc' | 'desc'",
"tags?": "string[]",
"limit?": "string.numeric.parse",
"skip?": "string.numeric.parse",
"search?": "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof NewsFetch.infer;
/**
* Fetch instance news articles
*/
export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3);
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
if (!userId)
throw createError({
statusCode: 403,
statusMessage: "Requires authentication",
});
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 query = NewsFetch(getQuery(h3));
if (query instanceof ArkErrors)
throw createError({ statusCode: 400, statusMessage: query.summary });
const tags = query.tags as string[] | undefined;
if (tags) {
if (typeof tags !== "object" || !Array.isArray(tags))
throw createError({ statusCode: 400, statusMessage: "Invalid tags" });
}
const orderBy = query.order;
const tags = query.tags;
const options = {
take: parseInt(query.limit as string),
skip: parseInt(query.skip as string),
take: Math.min(query.limit ?? 10, 10),
skip: query.skip ?? 0,
orderBy: orderBy,
...(tags && { tags: tags.map((e) => e.toString()) }),
search: query.search as string,
search: query.search,
};
const news = await newsManager.fetch(options);

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

View File

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

View File

@ -1,24 +1,34 @@
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:add"]);
if (!userId)
throw createError({
statusCode: 403,
});
const AddEntry = type({
id: "string",
}).configure(throwingArktype);
const id = getRouterParam(h3, "id");
if (!id)
throw createError({
statusCode: 400,
statusMessage: "ID required in route params",
});
/**
* 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({
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",
});
return await userLibraryManager.collectionAdd(gameId, id, userId);
});
const body = await readDropValidatedBody(h3, AddEntry);
const gameId = body.id;
return await userLibraryManager.collectionAdd(gameId, id, userId);
},
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
import { systemConfig } from "~/server/internal/config/sys-conf";
export default defineEventHandler((_h3) => {
/**
* Fetch instance information
*/
export default defineEventHandler(async (_h3) => {
return {
appName: "Drop",
version: systemConfig.getDropVersion(),

View File

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

View File

@ -1,7 +1,22 @@
import { ArkErrors, type } from "arktype";
import { defineEventHandler, getQuery } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
const NewsFetch = type({
"order?": "'asc' | 'desc'",
"tags?": "string[]",
"limit?": "string.numeric.parse",
"skip?": "string.numeric.parse",
"search?": "string",
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type APIQuery = typeof NewsFetch.infer;
/**
* Fetch instance news articles
*/
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
if (!userId)
@ -10,26 +25,19 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Requires authentication",
});
const query = getQuery(h3);
const query = NewsFetch(getQuery(h3));
if (query instanceof ArkErrors)
throw createError({ statusCode: 400, statusMessage: query.summary });
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 orderBy = query.order;
const tags = query.tags;
const options = {
take: parseInt(query.limit as string),
skip: parseInt(query.skip as string),
take: Math.min(query.limit ?? 10, 10),
skip: query.skip ?? 0,
orderBy: orderBy,
...(tags && { tags: tags.map((e) => e.toString()) }),
search: query.search as string,
search: query.search,
};
const news = await newsManager.fetch(options);

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More