mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
Compare commits
2 Commits
2db8e753b7
...
4c9a2c681a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c9a2c681a | |||
| 55878bdf5f |
@ -92,7 +92,7 @@ import type { Locale } from "vue-i18n";
|
|||||||
|
|
||||||
const { showText = true } = defineProps<{ showText?: boolean }>();
|
const { showText = true } = defineProps<{ showText?: boolean }>();
|
||||||
|
|
||||||
const { availableLocales, locale: currLocale, setLocale } = useI18n();
|
const { locale: currLocale, setLocale, locales } = useI18n();
|
||||||
|
|
||||||
function changeLocale(locale: Locale) {
|
function changeLocale(locale: Locale) {
|
||||||
setLocale(locale);
|
setLocale(locale);
|
||||||
@ -102,7 +102,7 @@ function changeLocale(locale: Locale) {
|
|||||||
useHead({
|
useHead({
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: locale,
|
lang: locale,
|
||||||
// dir: availableLocales.find((l) => l === locale)?.dir || "ltr",
|
dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -150,6 +150,6 @@ const wiredLocale = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const currentLocaleInformation = computed(() =>
|
const currentLocaleInformation = computed(() =>
|
||||||
availableLocales.find((e) => e == wiredLocale.value),
|
locales.value.find((e) => e.code == wiredLocale.value),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -106,7 +106,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value?: string;
|
value?: string | undefined;
|
||||||
guesses?: Array<{ platform: PlatformRenderable; filename: string }>;
|
guesses?: Array<{ platform: PlatformRenderable; filename: string }>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AdminSourcesPage from "~~/pages/admin/library/sources/index.vue";
|
import AdminSourcesPage from "~/pages/admin/library/sources/index.vue";
|
||||||
|
|
||||||
const complete = defineModel<boolean>({ required: true });
|
const complete = defineModel<boolean>({ required: true });
|
||||||
// Only runs on component load, so it's fine
|
// Only runs on component load, so it's fine
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||||
<UserHeader class="z-50" hydrate-on-idle />
|
<LazyUserHeader class="z-50" hydrate-on-idle />
|
||||||
<div class="grow flex">
|
<div class="grow flex">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
<UserFooter class="z-50" hydrate-on-interaction />
|
<LazyUserFooter class="z-50" hydrate-on-interaction />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|||||||
@ -252,7 +252,8 @@
|
|||||||
>Uninstall command</label
|
>Uninstall command</label
|
||||||
>
|
>
|
||||||
<p class="text-zinc-400 text-xs">
|
<p class="text-zinc-400 text-xs">
|
||||||
Executable to be run on uninstalling a game. Useful for installer-only games.
|
Executable to be run on uninstalling a game. Useful for installer-only
|
||||||
|
games.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div
|
<div
|
||||||
@ -301,7 +302,8 @@
|
|||||||
</SwitchDescription>
|
</SwitchDescription>
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
v-model="versionSettings.delta"
|
:model-value="versionSettings.delta || false"
|
||||||
|
@update:model-value="(v) => (versionSettings.delta = v)"
|
||||||
:class="[
|
:class="[
|
||||||
versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-800',
|
versionSettings.delta ? 'bg-blue-600' : 'bg-zinc-800',
|
||||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
|
||||||
@ -489,7 +491,6 @@ const versionGuesses =
|
|||||||
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
|
Array<SerializeObject<{ platform: PlatformRenderable; filename: string }>>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
|
||||||
function updateLaunchCommand(idx: number, value: string) {
|
function updateLaunchCommand(idx: number, value: string) {
|
||||||
versionSettings.value.launches![idx].launchCommand = value;
|
versionSettings.value.launches![idx].launchCommand = value;
|
||||||
autosetPlatform(value);
|
autosetPlatform(value);
|
||||||
|
|||||||
1
app/pages/admin/library/r/[id]/import.vue
Normal file
1
app/pages/admin/library/r/[id]/import.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template></template>
|
||||||
@ -163,9 +163,9 @@ const scheduledTasks: {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
debug: {
|
"import:version": {
|
||||||
name: "Debug Task",
|
name: "",
|
||||||
description: "Does debugging things.",
|
description: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AuthMec } from "~~/prisma/client/enums";
|
import type { AuthMec } from "~~/prisma/client/enums";
|
||||||
import DropLogo from "~~/components/DropLogo.vue";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const enabledAuths = await $dropFetch("/api/v1/auth");
|
const enabledAuths = await $dropFetch("/api/v1/auth");
|
||||||
|
|||||||
@ -224,14 +224,14 @@ const scopes = [
|
|||||||
href: "/docs/access/status",
|
href: "/docs/access/status",
|
||||||
icon: UserGroupIcon,
|
icon: UserGroupIcon,
|
||||||
},
|
},
|
||||||
clientData.capabilities["peerAPI"] && {
|
clientData.capabilities["PeerAPI"] && {
|
||||||
name: "Access the Drop network",
|
name: "Access the Drop network",
|
||||||
description:
|
description:
|
||||||
"The client will be able to establish P2P connections with other users to enable features like download aggregation, Remote LAN play and P2P multiplayer.",
|
"The client will be able to establish P2P connections with other users to enable features like download aggregation, Remote LAN play and P2P multiplayer.",
|
||||||
href: "/docs/access/network",
|
href: "/docs/access/network",
|
||||||
icon: LockClosedIcon,
|
icon: LockClosedIcon,
|
||||||
},
|
},
|
||||||
clientData.capabilities["cloudSaves"] && {
|
clientData.capabilities["CloudSaves"] && {
|
||||||
name: "Upload and sync cloud saves",
|
name: "Upload and sync cloud saves",
|
||||||
description:
|
description:
|
||||||
"The client will be able to upload new cloud saves, and edit your existing ones.",
|
"The client will be able to upload new cloud saves, and edit your existing ones.",
|
||||||
|
|||||||
@ -105,14 +105,14 @@ function input(index: number) {
|
|||||||
function select(index: number) {
|
function select(index: number) {
|
||||||
if (!codeElements.value) return;
|
if (!codeElements.value) return;
|
||||||
if (index >= codeElements.value.length) return;
|
if (index >= codeElements.value.length) return;
|
||||||
codeElements.value[index].select();
|
codeElements.value[index]!.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
function paste(index: number, event: ClipboardEvent) {
|
function paste(index: number, event: ClipboardEvent) {
|
||||||
const newCode = event.clipboardData!.getData("text/plain");
|
const newCode = event.clipboardData!.getData("text/plain");
|
||||||
for (let i = 0; i < newCode.length && i < codeLength; i++) {
|
for (let i = 0; i < newCode.length && i < codeLength; i++) {
|
||||||
code.value[i] = newCode[i];
|
code.value[i] = newCode[i]!;
|
||||||
codeElements.value![i].focus();
|
codeElements.value![i]!.focus();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,7 +110,7 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<component
|
<component
|
||||||
:is="actions[currentAction].page"
|
:is="actions[currentAction]!.page"
|
||||||
v-model="actionsComplete[currentAction]"
|
v-model="actionsComplete[currentAction]"
|
||||||
:token="bearerToken"
|
:token="bearerToken"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -254,7 +254,13 @@ import { StarIcon } from "@heroicons/vue/24/solid";
|
|||||||
import { micromark } from "micromark";
|
import { micromark } from "micromark";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const gameId = route.params.id.toString();
|
const gameId = route.params.id?.toString();
|
||||||
|
if (!gameId)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: "Game not found",
|
||||||
|
fatal: true,
|
||||||
|
});
|
||||||
|
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export default withNuxt([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@intlify/vue-i18n/no-missing-keys": "error",
|
"@intlify/vue-i18n/no-missing-keys": "error",
|
||||||
|
"vue/multi-word-component-names": "ignore",
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
"vue-i18n": {
|
"vue-i18n": {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const twemojiJson = module.findPackageJSON(
|
|||||||
if (!twemojiJson) {
|
if (!twemojiJson) {
|
||||||
throw new Error("Could not find @discordapp/twemoji package.");
|
throw new Error("Could not find @discordapp/twemoji package.");
|
||||||
}
|
}
|
||||||
|
const svgSrcDir = path.join(path.dirname(twemojiJson), "dist", "svg");
|
||||||
|
|
||||||
// get drop version
|
// get drop version
|
||||||
const dropVersion = getDropVersion();
|
const dropVersion = getDropVersion();
|
||||||
@ -74,14 +75,13 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [
|
plugins: [
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
tailwindcss(),
|
||||||
tailwindcss() as any,
|
|
||||||
// only used in dev server, not build because nitro sucks
|
// only used in dev server, not build because nitro sucks
|
||||||
// see build hook below
|
// see build hook below
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
src: "node_modules/@discordapp/twemoji/dist/svg/*",
|
src: `${svgSrcDir}/*`,
|
||||||
dest: "twemoji",
|
dest: "twemoji",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -96,7 +96,7 @@ export default defineNuxtConfig({
|
|||||||
// https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964
|
// https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964
|
||||||
// copy emojis to .output/public/twemoji
|
// copy emojis to .output/public/twemoji
|
||||||
const targetDir = path.join(nitro.options.output.publicDir, "twemoji");
|
const targetDir = path.join(nitro.options.output.publicDir, "twemoji");
|
||||||
cpSync(path.join(path.dirname(twemojiJson), "dist", "svg"), targetDir, {
|
cpSync(svgSrcDir, targetDir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -163,9 +163,11 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
tsConfig: {
|
tsConfig: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
// Not having these options on is sloppy, but it's a task for later me
|
||||||
verbatimModuleSyntax: false,
|
verbatimModuleSyntax: false,
|
||||||
strictNullChecks: true,
|
strictNullChecks: true,
|
||||||
exactOptionalPropertyTypes: true,
|
exactOptionalPropertyTypes: false,
|
||||||
|
//erasableSyntaxOnly: true,
|
||||||
noUncheckedIndexedAccess: false,
|
noUncheckedIndexedAccess: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -24,7 +24,6 @@
|
|||||||
"@drop-oss/droplet": "3.0.1",
|
"@drop-oss/droplet": "3.0.1",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
"@lobomfz/prismark": "0.0.3",
|
|
||||||
"@nuxt/fonts": "^0.11.0",
|
"@nuxt/fonts": "^0.11.0",
|
||||||
"@nuxt/image": "^1.10.0",
|
"@nuxt/image": "^1.10.0",
|
||||||
"@nuxtjs/i18n": "^9.5.5",
|
"@nuxtjs/i18n": "^9.5.5",
|
||||||
@ -40,7 +39,7 @@
|
|||||||
"fast-fuzzy": "^1.12.0",
|
"fast-fuzzy": "^1.12.0",
|
||||||
"file-type-mime": "^0.4.3",
|
"file-type-mime": "^0.4.3",
|
||||||
"jdenticon": "^3.3.0",
|
"jdenticon": "^3.3.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^27.0.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"micromark": "^4.0.1",
|
"micromark": "^4.0.1",
|
||||||
"normalize-url": "^8.0.2",
|
"normalize-url": "^8.0.2",
|
||||||
@ -48,7 +47,7 @@
|
|||||||
"nuxt-security": "2.2.0",
|
"nuxt-security": "2.2.0",
|
||||||
"pino": "^9.7.0",
|
"pino": "^9.7.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"prisma": "^6.14.0",
|
"prisma": "^6.11.1",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"stream-mime-type": "^2.0.0",
|
"stream-mime-type": "^2.0.0",
|
||||||
@ -88,5 +87,8 @@
|
|||||||
"vue3-carousel": "^0.16.0"
|
"vue3-carousel": "^0.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"prisma": {
|
||||||
|
"schema": "./prisma"
|
||||||
|
},
|
||||||
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
|
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
|
||||||
}
|
}
|
||||||
|
|||||||
2439
pnpm-lock.yaml
generated
2439
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +0,0 @@
|
|||||||
import { defineConfig } from "prisma/config";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
schema: path.join("prisma", "schema.prisma"),
|
|
||||||
});
|
|
||||||
@ -10,15 +10,6 @@ generator client {
|
|||||||
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* generator arktype {
|
|
||||||
* provider = "yarn prismark"
|
|
||||||
* output = "./validate"
|
|
||||||
* fileName = "schema.ts"
|
|
||||||
* nullish = true
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
|
|||||||
@ -42,10 +42,10 @@ export default defineEventHandler<{
|
|||||||
await objectHandler.deleteAsSystem(imageId);
|
await objectHandler.deleteAsSystem(imageId);
|
||||||
|
|
||||||
if (game.mBannerObjectId === imageId) {
|
if (game.mBannerObjectId === imageId) {
|
||||||
game.mBannerObjectId = game.mImageLibraryObjectIds[0];
|
game.mBannerObjectId = game.mImageLibraryObjectIds[0] ?? "";
|
||||||
}
|
}
|
||||||
if (game.mCoverObjectId === imageId) {
|
if (game.mCoverObjectId === imageId) {
|
||||||
game.mCoverObjectId = game.mImageLibraryObjectIds[0];
|
game.mCoverObjectId = game.mImageLibraryObjectIds[0] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await prisma.game.update({
|
const result = await prisma.game.update({
|
||||||
|
|||||||
@ -2,11 +2,10 @@ import { type } from "arktype";
|
|||||||
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
|
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
|
||||||
import aclManager from "~~/server/internal/acls";
|
import aclManager from "~~/server/internal/acls";
|
||||||
import taskHandler from "~~/server/internal/tasks";
|
import taskHandler from "~~/server/internal/tasks";
|
||||||
import type { TaskGroup } from "~~/server/internal/tasks/group";
|
import { TASK_GROUPS, type TaskGroup } from "~~/server/internal/tasks/group";
|
||||||
import { taskGroups } from "~~/server/internal/tasks/group";
|
|
||||||
|
|
||||||
const StartTask = type({
|
const StartTask = type({
|
||||||
taskGroup: type("string"),
|
taskGroup: type.enumerated(...TASK_GROUPS),
|
||||||
}).configure(throwingArktype);
|
}).configure(throwingArktype);
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
@ -14,14 +13,8 @@ export default defineEventHandler(async (h3) => {
|
|||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
const body = await readDropValidatedBody(h3, StartTask);
|
const body = await readDropValidatedBody(h3, StartTask);
|
||||||
const taskGroup = body.taskGroup as TaskGroup;
|
|
||||||
if (!taskGroups[taskGroup])
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
message: "Invalid task group.",
|
|
||||||
});
|
|
||||||
|
|
||||||
const task = await taskHandler.runTaskGroupByName(taskGroup);
|
const task = await taskHandler.runTaskGroupByName(body.taskGroup);
|
||||||
if (!task)
|
if (!task)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import { ClientCapabilities } from "~~/prisma/client/enums";
|
||||||
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
|
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
|
||||||
import type {
|
import type {
|
||||||
CapabilityConfiguration,
|
CapabilityConfiguration,
|
||||||
InternalClientCapability,
|
|
||||||
} from "~~/server/internal/clients/capabilities";
|
} from "~~/server/internal/clients/capabilities";
|
||||||
import capabilityManager, {
|
import capabilityManager, {
|
||||||
validCapabilities,
|
validCapabilities,
|
||||||
} from "~~/server/internal/clients/capabilities";
|
} from "~~/server/internal/clients/capabilities";
|
||||||
import clientHandler, { AuthMode } from "~~/server/internal/clients/handler";
|
import clientHandler, { AuthMode, AuthModes } from "~~/server/internal/clients/handler";
|
||||||
import { parsePlatform } from "~~/server/internal/utils/parseplatform";
|
import { parsePlatform } from "~~/server/internal/utils/parseplatform";
|
||||||
|
|
||||||
const ClientAuthInitiate = type({
|
const ClientAuthInitiate = type({
|
||||||
name: "string",
|
name: "string",
|
||||||
platform: "string",
|
platform: "string",
|
||||||
capabilities: "object",
|
capabilities: "object",
|
||||||
mode: type.valueOf(AuthMode).default(AuthMode.Callback),
|
mode: type.enumerated(...AuthModes).default("callback"),
|
||||||
}).configure(throwingArktype);
|
}).configure(throwingArktype);
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
@ -32,7 +32,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const capabilityIterable = Object.entries(capabilities) as Array<
|
const capabilityIterable = Object.entries(capabilities) as Array<
|
||||||
[InternalClientCapability, object]
|
[ClientCapabilities, object]
|
||||||
>;
|
>;
|
||||||
if (
|
if (
|
||||||
capabilityIterable.length > 0 &&
|
capabilityIterable.length > 0 &&
|
||||||
|
|||||||
@ -1,39 +1,24 @@
|
|||||||
import type { InternalClientCapability } from "~~/server/internal/clients/capabilities";
|
import { type } from "arktype";
|
||||||
|
import { ClientCapabilities } from "~~/prisma/client/enums";
|
||||||
|
import { readDropValidatedBody, throwingArktype } from "~~/server/arktype";
|
||||||
import capabilityManager, {
|
import capabilityManager, {
|
||||||
validCapabilities,
|
validCapabilities,
|
||||||
} from "~~/server/internal/clients/capabilities";
|
} from "~~/server/internal/clients/capabilities";
|
||||||
import { defineClientEventHandler } from "~~/server/internal/clients/event-handler";
|
import { defineClientEventHandler } from "~~/server/internal/clients/event-handler";
|
||||||
import notificationSystem from "~~/server/internal/notifications";
|
import notificationSystem from "~~/server/internal/notifications";
|
||||||
|
|
||||||
|
const SetCapability = type({
|
||||||
|
capability: type.enumerated(...Object.values(ClientCapabilities)),
|
||||||
|
configuration: "object"
|
||||||
|
}).configure(throwingArktype);
|
||||||
|
|
||||||
export default defineClientEventHandler(
|
export default defineClientEventHandler(
|
||||||
async (h3, { clientId, fetchClient, fetchUser }) => {
|
async (h3, { clientId, fetchClient, fetchUser }) => {
|
||||||
const body = await readBody(h3);
|
const body = await readDropValidatedBody(h3, SetCapability);
|
||||||
const rawCapability = body.capability;
|
|
||||||
const configuration = body.configuration;
|
|
||||||
|
|
||||||
if (!rawCapability || typeof rawCapability !== "string")
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
message: "capability must be a string",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!configuration || typeof configuration !== "object")
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
message: "configuration must be an object",
|
|
||||||
});
|
|
||||||
|
|
||||||
const capability = rawCapability as InternalClientCapability;
|
|
||||||
|
|
||||||
if (!validCapabilities.includes(capability))
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
message: "Invalid capability.",
|
|
||||||
});
|
|
||||||
|
|
||||||
const isValid = await capabilityManager.validateCapabilityConfiguration(
|
const isValid = await capabilityManager.validateCapabilityConfiguration(
|
||||||
capability,
|
body.capability,
|
||||||
configuration,
|
body.configuration,
|
||||||
);
|
);
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
throw createError({
|
throw createError({
|
||||||
@ -42,8 +27,8 @@ export default defineClientEventHandler(
|
|||||||
});
|
});
|
||||||
|
|
||||||
await capabilityManager.upsertClientCapability(
|
await capabilityManager.upsertClientCapability(
|
||||||
capability,
|
body.capability,
|
||||||
configuration,
|
body.configuration,
|
||||||
clientId,
|
clientId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -51,9 +36,9 @@ export default defineClientEventHandler(
|
|||||||
const user = await fetchUser();
|
const user = await fetchUser();
|
||||||
|
|
||||||
await notificationSystem.push(user.id, {
|
await notificationSystem.push(user.id, {
|
||||||
nonce: `capability-${clientId}-${capability}`,
|
nonce: `capability-${clientId}-${body.capability}`,
|
||||||
title: `"${client.name}" can now access ${capability}`,
|
title: `"${client.name}" can now access ${body.capability}`,
|
||||||
description: `A device called "${client.name}" now has access to your ${capability}.`,
|
description: `A device called "${client.name}" now has access to your ${body.capability}.`,
|
||||||
actions: ["Review|/account/devices"],
|
actions: ["Review|/account/devices"],
|
||||||
acls: ["user:clients:read"],
|
acls: ["user:clients:read"],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export class CertificateAuthority {
|
|||||||
let ca;
|
let ca;
|
||||||
if (root === undefined) {
|
if (root === undefined) {
|
||||||
const [cert, priv] = droplet.generateRootCa();
|
const [cert, priv] = droplet.generateRootCa();
|
||||||
const bundle: CertificateBundle = { priv, cert };
|
const bundle: CertificateBundle = { priv: priv!, cert: cert! };
|
||||||
await store.store("ca", bundle);
|
await store.store("ca", bundle);
|
||||||
ca = new CertificateAuthority(store, bundle);
|
ca = new CertificateAuthority(store, bundle);
|
||||||
} else {
|
} else {
|
||||||
@ -50,8 +50,8 @@ export class CertificateAuthority {
|
|||||||
caCertificate.priv,
|
caCertificate.priv,
|
||||||
);
|
);
|
||||||
const certBundle: CertificateBundle = {
|
const certBundle: CertificateBundle = {
|
||||||
priv,
|
priv: priv!,
|
||||||
cert,
|
cert: cert!,
|
||||||
};
|
};
|
||||||
return certBundle;
|
return certBundle;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,28 +2,18 @@ import type { EnumDictionary } from "../utils/types";
|
|||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
import { ClientCapabilities } from "~~/prisma/client/enums";
|
import { ClientCapabilities } from "~~/prisma/client/enums";
|
||||||
|
|
||||||
// These values are technically mapped to the database,
|
|
||||||
// but Typescript/Prisma doesn't let me link them
|
|
||||||
// They are also what are required by clients in the API
|
|
||||||
// BREAKING CHANGE
|
|
||||||
export enum InternalClientCapability {
|
|
||||||
PeerAPI = "peerAPI",
|
|
||||||
UserStatus = "userStatus",
|
|
||||||
CloudSaves = "cloudSaves",
|
|
||||||
TrackPlaytime = "trackPlaytime",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validCapabilities = Object.values(InternalClientCapability);
|
export const validCapabilities = Object.values(ClientCapabilities);
|
||||||
|
|
||||||
export type CapabilityConfiguration = {
|
export type CapabilityConfiguration = {
|
||||||
[InternalClientCapability.PeerAPI]: object;
|
[ClientCapabilities.PeerAPI]: object;
|
||||||
[InternalClientCapability.UserStatus]: object;
|
[ClientCapabilities.UserStatus]: object;
|
||||||
[InternalClientCapability.CloudSaves]: object;
|
[ClientCapabilities.CloudSaves]: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CapabilityManager {
|
class CapabilityManager {
|
||||||
private validationFunctions: EnumDictionary<
|
private validationFunctions: EnumDictionary<
|
||||||
InternalClientCapability,
|
ClientCapabilities,
|
||||||
(configuration: object) => Promise<boolean>
|
(configuration: object) => Promise<boolean>
|
||||||
> = {
|
> = {
|
||||||
/*
|
/*
|
||||||
@ -77,14 +67,14 @@ class CapabilityManager {
|
|||||||
return valid;
|
return valid;
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
[InternalClientCapability.PeerAPI]: async () => true,
|
[ClientCapabilities.PeerAPI]: async () => true,
|
||||||
[InternalClientCapability.UserStatus]: async () => true, // No requirements for user status
|
[ClientCapabilities.UserStatus]: async () => true, // No requirements for user status
|
||||||
[InternalClientCapability.CloudSaves]: async () => true, // No requirements for cloud saves
|
[ClientCapabilities.CloudSaves]: async () => true, // No requirements for cloud saves
|
||||||
[InternalClientCapability.TrackPlaytime]: async () => true,
|
[ClientCapabilities.TrackPlaytime]: async () => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
async validateCapabilityConfiguration(
|
async validateCapabilityConfiguration(
|
||||||
capability: InternalClientCapability,
|
capability: ClientCapabilities,
|
||||||
configuration: object,
|
configuration: object,
|
||||||
) {
|
) {
|
||||||
const validationFunction = this.validationFunctions[capability];
|
const validationFunction = this.validationFunctions[capability];
|
||||||
@ -93,15 +83,15 @@ class CapabilityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upsertClientCapability(
|
async upsertClientCapability(
|
||||||
capability: InternalClientCapability,
|
capability: ClientCapabilities,
|
||||||
rawCapabilityConfiguration: object,
|
rawCapabilityConfiguration: object,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
) {
|
) {
|
||||||
const upsertFunctions: EnumDictionary<
|
const upsertFunctions: EnumDictionary<
|
||||||
InternalClientCapability,
|
ClientCapabilities,
|
||||||
() => Promise<void> | void
|
() => Promise<void> | void
|
||||||
> = {
|
> = {
|
||||||
[InternalClientCapability.PeerAPI]: async function () {
|
[ClientCapabilities.PeerAPI]: async function () {
|
||||||
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
|
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
|
||||||
|
|
||||||
const currentClient = await prisma.client.findUnique({
|
const currentClient = await prisma.client.findUnique({
|
||||||
@ -139,10 +129,10 @@ class CapabilityManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[InternalClientCapability.UserStatus]: function (): Promise<void> | void {
|
[ClientCapabilities.UserStatus]: function (): Promise<void> | void {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
},
|
},
|
||||||
[InternalClientCapability.CloudSaves]: async function () {
|
[ClientCapabilities.CloudSaves]: async function () {
|
||||||
const currentClient = await prisma.client.findUnique({
|
const currentClient = await prisma.client.findUnique({
|
||||||
where: { id: clientId },
|
where: { id: clientId },
|
||||||
select: {
|
select: {
|
||||||
@ -162,7 +152,7 @@ class CapabilityManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[InternalClientCapability.TrackPlaytime]: async function () {
|
[ClientCapabilities.TrackPlaytime]: async function () {
|
||||||
const currentClient = await prisma.client.findUnique({
|
const currentClient = await prisma.client.findUnique({
|
||||||
where: { id: clientId },
|
where: { id: clientId },
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
import type { HardwarePlatform } from "~~/prisma/client/enums";
|
import type { ClientCapabilities, HardwarePlatform } from "~~/prisma/client/enums";
|
||||||
import { useCertificateAuthority } from "~~/server/plugins/ca";
|
import { useCertificateAuthority } from "~~/server/plugins/ca";
|
||||||
import type {
|
import type {
|
||||||
CapabilityConfiguration,
|
CapabilityConfiguration,
|
||||||
InternalClientCapability,
|
|
||||||
} from "./capabilities";
|
} from "./capabilities";
|
||||||
import capabilityManager from "./capabilities";
|
import capabilityManager from "./capabilities";
|
||||||
import type { PeerImpl } from "../tasks";
|
import type { PeerImpl } from "../tasks";
|
||||||
|
|
||||||
export enum AuthMode {
|
export const AuthModes = ["callback", "code"] as const;
|
||||||
Callback = "callback",
|
export type AuthMode = (typeof AuthModes)[number];
|
||||||
Code = "code",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientMetadata {
|
export interface ClientMetadata {
|
||||||
name: string;
|
name: string;
|
||||||
@ -62,9 +59,9 @@ export class ClientHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
switch (metadata.mode) {
|
switch (metadata.mode) {
|
||||||
case AuthMode.Callback:
|
case "callback":
|
||||||
return `/client/authorize/${clientId}`;
|
return `/client/authorize/${clientId}`;
|
||||||
case AuthMode.Code: {
|
case "code": {
|
||||||
const code = randomUUID()
|
const code = randomUUID()
|
||||||
.replaceAll(/-/g, "")
|
.replaceAll(/-/g, "")
|
||||||
.slice(0, 7)
|
.slice(0, 7)
|
||||||
@ -171,7 +168,7 @@ export class ClientHandler {
|
|||||||
metadata.data.capabilities,
|
metadata.data.capabilities,
|
||||||
)) {
|
)) {
|
||||||
await capabilityManager.upsertClientCapability(
|
await capabilityManager.upsertClientCapability(
|
||||||
capability as InternalClientCapability,
|
capability as ClientCapabilities,
|
||||||
configuration,
|
configuration,
|
||||||
client.id,
|
client.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -200,7 +200,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
|||||||
return url.pathname.replace("/games/", "").replace(/\/$/, "");
|
return url.pathname.replace("/games/", "").replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
logger.warn("Pcgamingwiki, unknown host", url.hostname);
|
logger.warn("Pcgamingwiki, unknown host: %s", url.hostname);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,7 +234,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
|||||||
});
|
});
|
||||||
if (ratingObj instanceof type.errors) {
|
if (ratingObj instanceof type.errors) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"pcgamingwiki: failed to properly get review rating",
|
"pcgamingwiki: failed to properly get review rating: %s",
|
||||||
ratingObj.summary,
|
ratingObj.summary,
|
||||||
);
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -123,7 +123,7 @@ export class FsObjectBackend extends ObjectBackend {
|
|||||||
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||||
const metadata = objectMetadata(metadataRaw);
|
const metadata = objectMetadata(metadataRaw);
|
||||||
if (metadata instanceof type.errors) {
|
if (metadata instanceof type.errors) {
|
||||||
logger.error("FsObjectBackend#fetchMetadata", metadata.summary);
|
logger.error("FsObjectBackend#fetchMetadata: %s", metadata.summary);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
await this.metadataCache.set(id, metadata);
|
await this.metadataCache.set(id, metadata);
|
||||||
@ -194,11 +194,13 @@ export class FsObjectBackend extends ObjectBackend {
|
|||||||
try {
|
try {
|
||||||
fs.rmSync(filePath);
|
fs.rmSync(filePath);
|
||||||
cleanupLogger.info(
|
cleanupLogger.info(
|
||||||
`[FsObjectBackend#cleanupMetadata]: Removed ${file}`,
|
`[FsObjectBackend#cleanupMetadata]: Removed %s`,
|
||||||
|
file
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cleanupLogger.error(
|
cleanupLogger.error(
|
||||||
`[FsObjectBackend#cleanupMetadata]: Failed to remove ${file}`,
|
`[FsObjectBackend#cleanupMetadata]: Failed to remove %s: %s`,
|
||||||
|
file,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,15 +32,12 @@ export const objectMetadata = type({
|
|||||||
});
|
});
|
||||||
export type ObjectMetadata = typeof objectMetadata.infer;
|
export type ObjectMetadata = typeof objectMetadata.infer;
|
||||||
|
|
||||||
export enum ObjectPermission {
|
export const ObjectPermissions = ["read", "write", "delete"] as const;
|
||||||
Read = "read",
|
export type ObjectPermission = (typeof ObjectPermissions)[number];
|
||||||
Write = "write",
|
|
||||||
Delete = "delete",
|
|
||||||
}
|
|
||||||
export const ObjectPermissionPriority: Array<ObjectPermission> = [
|
export const ObjectPermissionPriority: Array<ObjectPermission> = [
|
||||||
ObjectPermission.Read,
|
"read",
|
||||||
ObjectPermission.Write,
|
"write",
|
||||||
ObjectPermission.Delete,
|
"delete",
|
||||||
];
|
];
|
||||||
|
|
||||||
export type Object = { mime: string; data: Source };
|
export type Object = { mime: string; data: Source };
|
||||||
|
|||||||
@ -1,22 +1,32 @@
|
|||||||
export const taskGroups = {
|
export const TASK_GROUPS = [
|
||||||
"cleanup:invitations": {
|
"cleanup:invitations",
|
||||||
concurrency: false,
|
"cleanup:objects",
|
||||||
},
|
"cleanup:sessions",
|
||||||
"cleanup:objects": {
|
"check:update",
|
||||||
concurrency: false,
|
"import:game",
|
||||||
},
|
"import:version",
|
||||||
"cleanup:sessions": {
|
] as const;
|
||||||
concurrency: false,
|
|
||||||
},
|
|
||||||
"check:update": {
|
|
||||||
concurrency: false,
|
|
||||||
},
|
|
||||||
"import:game": {
|
|
||||||
concurrency: true,
|
|
||||||
},
|
|
||||||
debug: {
|
|
||||||
concurrency: true,
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type TaskGroup = keyof typeof taskGroups;
|
export type TaskGroup = (typeof TASK_GROUPS)[number];
|
||||||
|
|
||||||
|
export const TASK_GROUP_CONFIG: { [key in TaskGroup]: { concurrency: boolean } } =
|
||||||
|
{
|
||||||
|
"cleanup:invitations": {
|
||||||
|
concurrency: false
|
||||||
|
},
|
||||||
|
"cleanup:objects": {
|
||||||
|
concurrency: false
|
||||||
|
},
|
||||||
|
"cleanup:sessions": {
|
||||||
|
concurrency: false
|
||||||
|
},
|
||||||
|
"check:update": {
|
||||||
|
concurrency: false
|
||||||
|
},
|
||||||
|
"import:game": {
|
||||||
|
concurrency: true
|
||||||
|
},
|
||||||
|
"import:version": {
|
||||||
|
concurrency: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import cleanupInvites from "./registry/invitations";
|
|||||||
import cleanupSessions from "./registry/sessions";
|
import cleanupSessions from "./registry/sessions";
|
||||||
import checkUpdate from "./registry/update";
|
import checkUpdate from "./registry/update";
|
||||||
import cleanupObjects from "./registry/objects";
|
import cleanupObjects from "./registry/objects";
|
||||||
import { taskGroups, type TaskGroup } from "./group";
|
import { TASK_GROUP_CONFIG, type TaskGroup } from "./group";
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
@ -54,7 +54,6 @@ class TaskHandler {
|
|||||||
"cleanup:invitations",
|
"cleanup:invitations",
|
||||||
"cleanup:sessions",
|
"cleanup:sessions",
|
||||||
"check:update",
|
"check:update",
|
||||||
"debug",
|
|
||||||
];
|
];
|
||||||
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
|
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
|
||||||
|
|
||||||
@ -83,7 +82,7 @@ class TaskHandler {
|
|||||||
let logOffset: number = 0;
|
let logOffset: number = 0;
|
||||||
|
|
||||||
// if taskgroup disallows concurrency
|
// if taskgroup disallows concurrency
|
||||||
if (!taskGroups[task.taskGroup].concurrency) {
|
if (!TASK_GROUP_CONFIG[task.taskGroup].concurrency) {
|
||||||
for (const existingTask of this.taskPool.values()) {
|
for (const existingTask of this.taskPool.values()) {
|
||||||
// if a task is already running, we don't want to start another
|
// if a task is already running, we don't want to start another
|
||||||
if (existingTask.taskGroup === task.taskGroup) {
|
if (existingTask.taskGroup === task.taskGroup) {
|
||||||
@ -150,7 +149,7 @@ class TaskHandler {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// fallback: ignore or log error
|
// fallback: ignore or log error
|
||||||
logger.error("Failed to parse log chunk", {
|
logger.error("Failed to parse log chunk %s", {
|
||||||
error: e,
|
error: e,
|
||||||
chunk: chunk,
|
chunk: chunk,
|
||||||
});
|
});
|
||||||
@ -178,7 +177,7 @@ class TaskHandler {
|
|||||||
|
|
||||||
const progress = (progress: number) => {
|
const progress = (progress: number) => {
|
||||||
if (progress < 0 || progress > 100) {
|
if (progress < 0 || progress > 100) {
|
||||||
logger.error("Progress must be between 0 and 100", { progress });
|
logger.error("Progress must be between 0 and 100, actually %d", progress);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const taskEntry = this.taskPool.get(task.id);
|
const taskEntry = this.taskPool.get(task.id);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { defineDropTask } from "..";
|
import { defineDropTask } from "..";
|
||||||
|
|
||||||
|
/*
|
||||||
export default defineDropTask({
|
export default defineDropTask({
|
||||||
buildId: () => `debug:${new Date().toISOString()}`,
|
buildId: () => `debug:${new Date().toISOString()}`,
|
||||||
name: "Debug Task",
|
name: "Debug Task",
|
||||||
@ -16,3 +17,4 @@ export default defineDropTask({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export default defineDropTask({
|
|||||||
|
|
||||||
// if response failed somehow
|
// if response failed somehow
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.info("Failed to check for update ", {
|
logger.info("Failed to check for update: %s", {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
body: response.body,
|
body: response.body,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user