mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-20 11:41:14 +10:00
partial: user routes
This commit is contained in:
@ -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)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
|
||||
/**
|
||||
* Check if we are an admin/system
|
||||
* Check if we are a setup token
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, []);
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["setup"]);
|
||||
if (!allowed) return false;
|
||||
return true;
|
||||
});
|
||||
@ -1,5 +1,8 @@
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
/**
|
||||
* Fetch public authentication provider mechanisms
|
||||
*/
|
||||
export default defineEventHandler(() => {
|
||||
return authManager.getEnabledAuthProviders();
|
||||
});
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import taskHandler from "~/server/internal/tasks";
|
||||
import authManager from "~/server/internal/auth";
|
||||
import { ArkErrors, type } from "arktype";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const Query = type({
|
||||
id: "string",
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch invitation details for pre-filling
|
||||
*/
|
||||
export default defineEventHandler<{ query: typeof Query.infer }>(async (h3) => {
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
@ -11,13 +19,13 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
if (!id)
|
||||
const query = Query(getQuery(h3));
|
||||
if (query instanceof ArkErrors)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: t("errors.auth.inviteIdRequired"),
|
||||
statusMessage: "Invalid query: " + query.summary,
|
||||
});
|
||||
const id = query.id;
|
||||
taskHandler.runTaskGroupByName("cleanup:invitations");
|
||||
|
||||
const invitation = await prisma.invitation.findUnique({ where: { id: id } });
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 {};
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
import { type } from "arktype";
|
||||
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const AddEntry = type({
|
||||
id: "string",
|
||||
}).configure(throwingArktype);
|
||||
|
||||
/**
|
||||
* Add game to collection
|
||||
* @param id Collection ID
|
||||
*/
|
||||
export default defineEventHandler<{ body: typeof AddEntry.infer }>(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["collections:add"]);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
@ -15,10 +25,8 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "ID required in route params",
|
||||
});
|
||||
|
||||
const body = await readBody(h3);
|
||||
const body = await readDropValidatedBody(h3, AddEntry);
|
||||
const gameId = body.id;
|
||||
if (!gameId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Game ID required" });
|
||||
|
||||
return await userLibraryManager.collectionAdd(gameId, id, userId);
|
||||
});
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {};
|
||||
},
|
||||
);
|
||||
|
||||
@ -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 {};
|
||||
},
|
||||
);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { systemConfig } from "~/server/internal/config/sys-conf";
|
||||
|
||||
/**
|
||||
* Fetch instance information
|
||||
*/
|
||||
export default defineEventHandler(async (_h3) => {
|
||||
return {
|
||||
appName: "Drop",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,37 +1,44 @@
|
||||
import { ArkErrors, type } from "arktype";
|
||||
import { defineEventHandler, getQuery } from "h3";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import newsManager from "~/server/internal/news";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const query = getQuery(h3);
|
||||
|
||||
const orderBy = query.order as "asc" | "desc";
|
||||
if (orderBy) {
|
||||
if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy))
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid order" });
|
||||
}
|
||||
|
||||
const tags = query.tags as string[] | undefined;
|
||||
if (tags) {
|
||||
if (typeof tags !== "object" || !Array.isArray(tags))
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid tags" });
|
||||
}
|
||||
|
||||
const options = {
|
||||
take: parseInt(query.limit as string),
|
||||
skip: parseInt(query.skip as string),
|
||||
orderBy: orderBy,
|
||||
...(tags && { tags: tags.map((e) => e.toString()) }),
|
||||
search: query.search as string,
|
||||
};
|
||||
|
||||
const news = await newsManager.fetch(options);
|
||||
return news;
|
||||
const NewsFetch = type({
|
||||
"order?": "'asc' | 'desc'",
|
||||
"tags?": "string[]",
|
||||
"limit?": "string.numeric.parse",
|
||||
"skip?": "string.numeric.parse",
|
||||
"search?": "string",
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch instance news articles
|
||||
*/
|
||||
export default defineEventHandler<{ query: typeof NewsFetch.infer }>(
|
||||
async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Requires authentication",
|
||||
});
|
||||
|
||||
const query = NewsFetch(getQuery(h3));
|
||||
if (query instanceof ArkErrors)
|
||||
throw createError({ statusCode: 400, statusMessage: query.summary });
|
||||
|
||||
const orderBy = query.order;
|
||||
const tags = query.tags;
|
||||
|
||||
const options = {
|
||||
take: Math.min(query.limit ?? 10, 10),
|
||||
skip: query.skip ?? 0,
|
||||
orderBy: orderBy,
|
||||
...(tags && { tags: tags.map((e) => e.toString()) }),
|
||||
search: query.search,
|
||||
};
|
||||
|
||||
const news = await newsManager.fetch(options);
|
||||
return news;
|
||||
},
|
||||
);
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Mark notification as read
|
||||
* @param id Notification ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Fetch all notifications for this token
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["notifications:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Mark all notifications as read
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -6,6 +6,11 @@ import { logger } from "~/server/internal/logging";
|
||||
// Peer ID to user ID
|
||||
const socketSessions = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Connect to a WebSocket to listen for notification pushes.
|
||||
*
|
||||
* Sends "unauthenticated" if authentication fails, otherwise JSON notifications.
|
||||
*/
|
||||
export default defineWebSocketHandler({
|
||||
async open(peer) {
|
||||
const h3 = { headers: peer.request?.headers ?? new Headers() };
|
||||
|
||||
@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
/**
|
||||
* Delete object
|
||||
* @param id Object ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const unsafeId = getRouterParam(h3, "id");
|
||||
if (!unsafeId)
|
||||
|
||||
@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
/**
|
||||
* Fetch object. Sets a lot of caching headers, recommended to use them.
|
||||
* @param id Object ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const unsafeId = getRouterParam(h3, "id");
|
||||
if (!unsafeId)
|
||||
|
||||
@ -2,7 +2,10 @@ import aclManager from "~/server/internal/acls";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
// this request method is purely used by the browser to check if etag values are still valid
|
||||
/**
|
||||
* Check if object has changed (etag/browser caching)
|
||||
* @param id Object ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const unsafeId = getRouterParam(h3, "id");
|
||||
if (!unsafeId)
|
||||
|
||||
@ -2,6 +2,10 @@ import aclManager from "~/server/internal/acls";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
/**
|
||||
* Upload and overwrite object. Takes raw binary data (`application/octet-stream`)
|
||||
* @param id Object ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const unsafeId = getRouterParam(h3, "id");
|
||||
if (!unsafeId)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
// get a specific screenshot
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import screenshotManager from "~/server/internal/screenshots";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
/**
|
||||
* Delete screenshot by ID
|
||||
* @param id Screenshot ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["screenshots:delete"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -3,6 +3,10 @@ import aclManager from "~/server/internal/acls";
|
||||
import screenshotManager from "~/server/internal/screenshots";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
/**
|
||||
* Fetch screenshot by ID. Use `/api/v1/object/:id` to actually fetch screenshot image data.
|
||||
* @param id Screenshot ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
// get all user screenshots by game
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import screenshotManager from "~/server/internal/screenshots";
|
||||
|
||||
/**
|
||||
* Fetch all screenshots for game
|
||||
* @param id Game ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -3,8 +3,10 @@ import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import screenshotManager from "~/server/internal/screenshots";
|
||||
|
||||
// TODO: make defineClientEventHandler instead?
|
||||
// only clients will be upload screenshots yea??
|
||||
/**
|
||||
* Upload screenshot by game. Subject to change, will likely become a client route. Takes raw upload (`application/octet-stream`)
|
||||
* @param id Game ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["screenshots:new"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import screenshotManager from "~/server/internal/screenshots";
|
||||
|
||||
/**
|
||||
* Fetch all screenshots
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["screenshots:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { applicationSettings } from "~/server/internal/config/application-configuration";
|
||||
|
||||
/**
|
||||
* Fetch system settings
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.getUserACL(h3, ["settings:read"]);
|
||||
if (!allowed) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -2,6 +2,9 @@ import { APITokenMode } from "~/prisma/client/enums";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Complete setup, and delete setup token.
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["setup"]);
|
||||
if (!allowed)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Fetch all featured games. Used for store carousel.
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -21,102 +21,107 @@ const StoreRead = type({
|
||||
sort: "'default' | 'newest' | 'recent' = 'default'",
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
/**
|
||||
* Store endpoint. Filter games with pagination. Used for all "store views".
|
||||
*/
|
||||
export default defineEventHandler<{ query: typeof StoreRead.infer }>(
|
||||
async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const query = getQuery(h3);
|
||||
const options = StoreRead(query);
|
||||
if (options instanceof ArkErrors)
|
||||
throw createError({ statusCode: 400, statusMessage: options.summary });
|
||||
const query = getQuery(h3);
|
||||
const options = StoreRead(query);
|
||||
if (options instanceof ArkErrors)
|
||||
throw createError({ statusCode: 400, statusMessage: options.summary });
|
||||
|
||||
/**
|
||||
* Generic filters
|
||||
*/
|
||||
const tagFilter = options.tags
|
||||
? {
|
||||
tags: {
|
||||
some: {
|
||||
id: {
|
||||
in: options.tags.split(","),
|
||||
/**
|
||||
* Generic filters
|
||||
*/
|
||||
const tagFilter = options.tags
|
||||
? {
|
||||
tags: {
|
||||
some: {
|
||||
id: {
|
||||
in: options.tags.split(","),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const platformFilter = options.platform
|
||||
? {
|
||||
versions: {
|
||||
some: {
|
||||
platform: {
|
||||
in: options.platform
|
||||
.split(",")
|
||||
.map(parsePlatform)
|
||||
.filter((e) => e !== undefined),
|
||||
}
|
||||
: undefined;
|
||||
const platformFilter = options.platform
|
||||
? {
|
||||
versions: {
|
||||
some: {
|
||||
platform: {
|
||||
in: options.platform
|
||||
.split(",")
|
||||
.map(parsePlatform)
|
||||
.filter((e) => e !== undefined),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* Company filtering
|
||||
*/
|
||||
const companyActions = options.companyActions.split(",");
|
||||
const developedFilter = companyActions.includes("developed")
|
||||
? {
|
||||
developers: {
|
||||
some: {
|
||||
id: options.company!,
|
||||
/**
|
||||
* Company filtering
|
||||
*/
|
||||
const companyActions = options.companyActions.split(",");
|
||||
const developedFilter = companyActions.includes("developed")
|
||||
? {
|
||||
developers: {
|
||||
some: {
|
||||
id: options.company!,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const publishedFilter = companyActions.includes("published")
|
||||
? {
|
||||
publishers: {
|
||||
some: {
|
||||
id: options.company!,
|
||||
}
|
||||
: undefined;
|
||||
const publishedFilter = companyActions.includes("published")
|
||||
? {
|
||||
publishers: {
|
||||
some: {
|
||||
id: options.company!,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const companyFilter = options.company
|
||||
? ({
|
||||
OR: [developedFilter, publishedFilter].filter((e) => e !== undefined),
|
||||
} satisfies Prisma.GameWhereInput)
|
||||
: undefined;
|
||||
}
|
||||
: undefined;
|
||||
const companyFilter = options.company
|
||||
? ({
|
||||
OR: [developedFilter, publishedFilter].filter((e) => e !== undefined),
|
||||
} satisfies Prisma.GameWhereInput)
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* Query
|
||||
*/
|
||||
/**
|
||||
* Query
|
||||
*/
|
||||
|
||||
const finalFilter: Prisma.GameWhereInput = {
|
||||
...tagFilter,
|
||||
...platformFilter,
|
||||
...companyFilter,
|
||||
};
|
||||
const finalFilter: Prisma.GameWhereInput = {
|
||||
...tagFilter,
|
||||
...platformFilter,
|
||||
...companyFilter,
|
||||
};
|
||||
|
||||
const sort: Prisma.GameOrderByWithRelationInput = {};
|
||||
switch (options.sort) {
|
||||
case "default":
|
||||
case "newest":
|
||||
sort.mReleased = "desc";
|
||||
break;
|
||||
case "recent":
|
||||
sort.created = "desc";
|
||||
break;
|
||||
}
|
||||
const sort: Prisma.GameOrderByWithRelationInput = {};
|
||||
switch (options.sort) {
|
||||
case "default":
|
||||
case "newest":
|
||||
sort.mReleased = "desc";
|
||||
break;
|
||||
case "recent":
|
||||
sort.created = "desc";
|
||||
break;
|
||||
}
|
||||
|
||||
const [results, count] = await prisma.$transaction([
|
||||
prisma.game.findMany({
|
||||
skip: options.skip,
|
||||
take: Math.min(options.take, 50),
|
||||
where: finalFilter,
|
||||
orderBy: sort,
|
||||
}),
|
||||
prisma.game.count({ where: finalFilter }),
|
||||
]);
|
||||
const [results, count] = await prisma.$transaction([
|
||||
prisma.game.findMany({
|
||||
skip: options.skip,
|
||||
take: Math.min(options.take, 50),
|
||||
where: finalFilter,
|
||||
orderBy: sort,
|
||||
}),
|
||||
prisma.game.count({ where: finalFilter }),
|
||||
]);
|
||||
|
||||
return { results, count };
|
||||
});
|
||||
return { results, count };
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Fetch all game tags.
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Fetch tag by ID
|
||||
* @param id Tag ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const tagId = getRouterParam(h3, "id");
|
||||
if (!tagId)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Missing gameId in route params (somehow...?)",
|
||||
});
|
||||
const tagId = getRouterParam(h3, "id")!;
|
||||
|
||||
const tag = await prisma.gameTag.findUnique({
|
||||
where: { id: tagId },
|
||||
|
||||
@ -5,6 +5,15 @@ import type { MinimumRequestObject } from "~/server/h3";
|
||||
// ID to admin
|
||||
const socketHeaders = new Map<string, MinimumRequestObject>();
|
||||
|
||||
/**
|
||||
* WebSocket to listen to task updates.
|
||||
*
|
||||
* Sends "unauthenticated" if authentication failed.
|
||||
*
|
||||
* Use `connect/:taskId` to subscribe to a task.
|
||||
*
|
||||
* Sends JSON tasks for all tasks subscribed.
|
||||
*/
|
||||
export default defineWebSocketHandler({
|
||||
async open(peer) {
|
||||
const request = peer.request;
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
|
||||
/**
|
||||
* Revoke client
|
||||
* @param id Client ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const clientId = getRouterParam(h3, "id");
|
||||
if (!clientId)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Client ID missing in route params",
|
||||
});
|
||||
const clientId = getRouterParam(h3, "id")!;
|
||||
|
||||
await clientHandler.removeClient(clientId);
|
||||
});
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Fetch all clients connected to this account
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["clients:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
|
||||
/**
|
||||
* Fetch user.
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await aclManager.getUserACL(h3, ["read"]);
|
||||
return user ?? null; // Need to specifically return null
|
||||
|
||||
@ -2,16 +2,15 @@ import { APITokenMode } from "~/prisma/client/enums";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Revoke token
|
||||
* @param id Token ID
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const id = h3.context.params?.id;
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "No id in router params",
|
||||
});
|
||||
const id = getRouterParam(h3, "id")!;
|
||||
|
||||
const deleted = await prisma.aPIToken.delete({
|
||||
where: { id: id, userId: userId, mode: APITokenMode.User },
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { userACLDescriptions } from "~/server/internal/acls/descriptions";
|
||||
|
||||
/**
|
||||
* Fetch ACL descriptions.
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -2,6 +2,9 @@ import { APITokenMode } from "~/prisma/client/enums";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
/**
|
||||
* Fetch all API tokens for this account.
|
||||
*/
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -1,46 +1,44 @@
|
||||
import { type } from "arktype";
|
||||
import { APITokenMode } from "~/prisma/client/enums";
|
||||
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||
import aclManager, { userACLs } from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
const CreateToken = type({
|
||||
name: "string",
|
||||
acls: "string[] > 0",
|
||||
}).configure(throwingArktype);
|
||||
|
||||
const body = await readBody(h3);
|
||||
const name: string = body.name;
|
||||
const acls: string[] = body.acls;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default defineEventHandler<{ body: typeof CreateToken.infer }>(
|
||||
async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, []); // No ACLs only allows session authentication
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
if (!name || typeof name !== "string")
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Token name required",
|
||||
});
|
||||
if (!acls || !Array.isArray(acls))
|
||||
throw createError({ statusCode: 400, statusMessage: "ACLs required" });
|
||||
const body = await readDropValidatedBody(h3, CreateToken);
|
||||
const name: string = body.name;
|
||||
const acls: string[] = body.acls;
|
||||
|
||||
if (acls.length == 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Token requires more than zero ACLs",
|
||||
const invalidACLs = acls.filter(
|
||||
(e) => userACLs.findIndex((v) => e == v) == -1,
|
||||
);
|
||||
if (invalidACLs.length > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`,
|
||||
});
|
||||
|
||||
const token = await prisma.aPIToken.create({
|
||||
data: {
|
||||
mode: APITokenMode.User,
|
||||
name: name,
|
||||
userId: userId,
|
||||
acls: acls,
|
||||
},
|
||||
});
|
||||
|
||||
const invalidACLs = acls.filter(
|
||||
(e) => userACLs.findIndex((v) => e == v) == -1,
|
||||
);
|
||||
if (invalidACLs.length > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`,
|
||||
});
|
||||
|
||||
const token = await prisma.aPIToken.create({
|
||||
data: {
|
||||
mode: APITokenMode.User,
|
||||
name: name,
|
||||
userId: userId,
|
||||
acls: acls,
|
||||
},
|
||||
});
|
||||
|
||||
return token;
|
||||
});
|
||||
return token;
|
||||
},
|
||||
);
|
||||
|
||||
@ -11,63 +11,70 @@ const GetChunk = type({
|
||||
}).array(),
|
||||
}).configure(throwingArktype);
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readDropValidatedBody(h3, GetChunk);
|
||||
/**
|
||||
* Part of v2 download API. Intended to be client-only.
|
||||
*
|
||||
* Returns raw stream of all files requested, in order.
|
||||
*/
|
||||
export default defineEventHandler<{ body: typeof GetChunk.infer }>(
|
||||
async (h3) => {
|
||||
const body = await readDropValidatedBody(h3, GetChunk);
|
||||
|
||||
const context = await contextManager.fetchContext(body.context);
|
||||
if (!context)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid download context.",
|
||||
});
|
||||
|
||||
const streamFiles = [];
|
||||
|
||||
for (const file of body.files) {
|
||||
const manifestFile = context.manifest[file.filename];
|
||||
if (!manifestFile)
|
||||
const context = await contextManager.fetchContext(body.context);
|
||||
if (!context)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Unknown file: ${file.filename}`,
|
||||
statusMessage: "Invalid download context.",
|
||||
});
|
||||
|
||||
const start = manifestFile.lengths
|
||||
.slice(0, file.chunkIndex)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const end = start + manifestFile.lengths[file.chunkIndex];
|
||||
const streamFiles = [];
|
||||
|
||||
streamFiles.push({ filename: file.filename, start, end });
|
||||
}
|
||||
for (const file of body.files) {
|
||||
const manifestFile = context.manifest[file.filename];
|
||||
if (!manifestFile)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Unknown file: ${file.filename}`,
|
||||
});
|
||||
|
||||
setHeader(
|
||||
h3,
|
||||
"Content-Lengths",
|
||||
streamFiles.map((e) => e.end - e.start).join(","),
|
||||
); // Non-standard header, but we're cool like that 😎
|
||||
const start = manifestFile.lengths
|
||||
.slice(0, file.chunkIndex)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const end = start + manifestFile.lengths[file.chunkIndex];
|
||||
|
||||
for (const file of streamFiles) {
|
||||
const gameReadStream = await libraryManager.readFile(
|
||||
context.libraryId,
|
||||
context.libraryPath,
|
||||
context.versionName,
|
||||
file.filename,
|
||||
{ start: file.start, end: file.end },
|
||||
);
|
||||
if (!gameReadStream)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to create read stream",
|
||||
});
|
||||
await gameReadStream.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
h3.node.res.write(chunk);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
streamFiles.push({ filename: file.filename, start, end });
|
||||
}
|
||||
|
||||
await h3.node.res.end();
|
||||
setHeader(
|
||||
h3,
|
||||
"Content-Lengths",
|
||||
streamFiles.map((e) => e.end - e.start).join(","),
|
||||
); // Non-standard header, but we're cool like that 😎
|
||||
|
||||
return;
|
||||
});
|
||||
for (const file of streamFiles) {
|
||||
const gameReadStream = await libraryManager.readFile(
|
||||
context.libraryId,
|
||||
context.libraryPath,
|
||||
context.versionName,
|
||||
file.filename,
|
||||
{ start: file.start, end: file.end },
|
||||
);
|
||||
if (!gameReadStream)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to create read stream",
|
||||
});
|
||||
await gameReadStream.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
h3.node.res.write(chunk);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await h3.node.res.end();
|
||||
|
||||
return;
|
||||
},
|
||||
);
|
||||
|
||||
@ -8,15 +8,20 @@ const CreateContext = type({
|
||||
version: "string",
|
||||
}).configure(throwingArktype);
|
||||
|
||||
export default defineClientEventHandler(async (h3) => {
|
||||
const body = await readDropValidatedBody(h3, CreateContext);
|
||||
/**
|
||||
* Part of v2 download API. Create a download context for use with `/api/v2/client/chunk`.
|
||||
*/
|
||||
export default defineClientEventHandler<{ body: typeof CreateContext.infer }>(
|
||||
async (h3) => {
|
||||
const body = await readDropValidatedBody(h3, CreateContext);
|
||||
|
||||
const context = await contextManager.createContext(body.game, body.version);
|
||||
if (!context)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid game or version",
|
||||
});
|
||||
const context = await contextManager.createContext(body.game, body.version);
|
||||
if (!context)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid game or version",
|
||||
});
|
||||
|
||||
return { context };
|
||||
});
|
||||
return { context };
|
||||
},
|
||||
);
|
||||
|
||||
@ -184,9 +184,6 @@ class ACLManager {
|
||||
if (!token) return false;
|
||||
if (token.mode != APITokenMode.System) return false;
|
||||
|
||||
// If empty, we just want to check we are an admin *at all*, not specific ACLs
|
||||
if (acls.length == 0) return true;
|
||||
|
||||
for (const acl of acls) {
|
||||
const tokenACLIndex = token.acls.findIndex((e) => e == acl);
|
||||
if (tokenACLIndex != -1) return true;
|
||||
|
||||
@ -17,7 +17,9 @@ type ClientUtils = {
|
||||
|
||||
const NONCE_LENIENCE = 30_000;
|
||||
|
||||
export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
export function defineClientEventHandler<R extends EventHandlerRequest = object, K = unknown>(
|
||||
handler: (h3: H3Event<R>, utils: ClientUtils) => Promise<K> | K,
|
||||
) {
|
||||
return defineEventHandler(async (h3) => {
|
||||
const header = getHeader(h3, "Authorization");
|
||||
if (!header) throw createError({ statusCode: 403 });
|
||||
|
||||
@ -46,9 +46,9 @@ class NewsManager {
|
||||
options: {
|
||||
take?: number;
|
||||
skip?: number;
|
||||
orderBy?: "asc" | "desc";
|
||||
orderBy?: "asc" | "desc" | undefined;
|
||||
tags?: string[];
|
||||
search?: string;
|
||||
search?: string | undefined;
|
||||
} = {},
|
||||
) {
|
||||
return await prisma.article.findMany({
|
||||
|
||||
Reference in New Issue
Block a user