fix: more eslint stuff

This commit is contained in:
Huskydog9988
2025-04-15 21:10:45 -04:00
parent 8f429e1e56
commit 8e109dd562
58 changed files with 1066 additions and 1016 deletions
+11 -12
View File
@@ -195,7 +195,7 @@
name="startup" name="startup"
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6" class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="--setup" placeholder="--setup"
> />
</div> </div>
</div> </div>
</div> </div>
@@ -351,7 +351,7 @@
name="startup" name="startup"
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6" class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="--launch" placeholder="--launch"
> />
</div> </div>
</div> </div>
<div <div
@@ -461,7 +461,7 @@
:disabled="!umuIdEnabled" :disabled="!umuIdEnabled"
placeholder="umu-starcitizen" placeholder="umu-starcitizen"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-950 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-950 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
> />
</div> </div>
</div> </div>
</div> </div>
@@ -536,7 +536,6 @@ import {
Combobox, Combobox,
ComboboxButton, ComboboxButton,
ComboboxInput, ComboboxInput,
ComboboxLabel,
ComboboxOption, ComboboxOption,
ComboboxOptions, ComboboxOptions,
} from "@headlessui/vue"; } from "@headlessui/vue";
@@ -553,7 +552,7 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const gameId = route.params.id.toString(); const gameId = route.params.id.toString();
const versions = await $dropFetch( const versions = await $dropFetch(
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}` `/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
); );
const currentlySelectedVersion = ref(-1); const currentlySelectedVersion = ref(-1);
const versionSettings = ref<{ const versionSettings = ref<{
@@ -585,13 +584,13 @@ const setupProcessQuery = ref("");
const launchFilteredVersionGuesses = computed(() => const launchFilteredVersionGuesses = computed(() =>
versionGuesses.value?.filter((e) => versionGuesses.value?.filter((e) =>
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()) e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()),
) ),
); );
const setupFilteredVersionGuesses = computed(() => const setupFilteredVersionGuesses = computed(() =>
versionGuesses.value?.filter((e) => versionGuesses.value?.filter((e) =>
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase()) e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase()),
) ),
); );
function updateLaunchCommand(value: string) { function updateLaunchCommand(value: string) {
@@ -608,7 +607,7 @@ function autosetPlatform(value: string) {
if (!versionGuesses.value) return; if (!versionGuesses.value) return;
if (versionSettings.value.platform) return; if (versionSettings.value.platform) return;
const guessIndex = versionGuesses.value.findIndex( const guessIndex = versionGuesses.value.findIndex(
(e) => e.filename === value (e) => e.filename === value,
); );
if (guessIndex == -1) return; if (guessIndex == -1) return;
versionSettings.value.platform = versionGuesses.value[guessIndex].platform; versionSettings.value.platform = versionGuesses.value[guessIndex].platform;
@@ -636,8 +635,8 @@ async function updateCurrentlySelectedVersion(value: number) {
const version = versions[currentlySelectedVersion.value]; const version = versions[currentlySelectedVersion.value];
const results = await $dropFetch( const results = await $dropFetch(
`/api/v1/admin/import/version/preload?id=${encodeURIComponent( `/api/v1/admin/import/version/preload?id=${encodeURIComponent(
gameId gameId,
)}&version=${encodeURIComponent(version)}` )}&version=${encodeURIComponent(version)}`,
); );
versionGuesses.value = results.map((e) => ({ versionGuesses.value = results.map((e) => ({
...e, ...e,
File diff suppressed because it is too large Load Diff
+139 -63
View File
@@ -1,45 +1,69 @@
<template> <template>
<div class="flex flex-col gap-y-6 w-full max-w-md"> <div class="flex flex-col gap-y-6 w-full max-w-md">
<Listbox <Listbox
as="div" :model="currentlySelectedGame" as="div"
@update:model-value="(value) => updateSelectedGame_wrapper(value)"> :model="currentlySelectedGame"
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game to import</ListboxLabel> @update:model-value="(value) => updateSelectedGame_wrapper(value)"
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
>Select game to import</ListboxLabel
>
<div class="relative mt-2"> <div class="relative mt-2">
<ListboxButton <ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"> class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
>
<span v-if="currentlySelectedGame != -1" class="block truncate">{{ <span v-if="currentlySelectedGame != -1" class="block truncate">{{
games.unimportedGames[currentlySelectedGame] games.unimportedGames[currentlySelectedGame]
}}</span> }}</span>
<span v-else class="block truncate text-zinc-400">Please select a directory...</span> <span v-else class="block truncate text-zinc-400"
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> >Please select a directory...</span
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" /> >
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span> </span>
</ListboxButton> </ListboxButton>
<transition <transition
leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-active-class="transition ease-in duration-100"
leave-to-class="opacity-0"> leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions <ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm"> class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm"
>
<ListboxOption <ListboxOption
v-for="(game, gameIdx) in games.unimportedGames" :key="game" v-slot="{ active, selected }" as="template" v-for="(game, gameIdx) in games.unimportedGames"
:value="gameIdx"> :key="game"
v-slot="{ active, selected }"
as="template"
:value="gameIdx"
>
<li <li
:class="[ :class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-100', active ? 'bg-blue-600 text-white' : 'text-zinc-100',
'relative cursor-default select-none py-2 pl-3 pr-9', 'relative cursor-default select-none py-2 pl-3 pr-9',
]"> ]"
>
<span <span
:class="[ :class="[
selected ? 'font-semibold' : 'font-normal', selected ? 'font-semibold' : 'font-normal',
'block truncate', 'block truncate',
]">{{ game }}</span> ]"
>{{ game }}</span
>
<span <span
v-if="selected" :class="[ v-if="selected"
active ? 'text-white' : 'text-blue-600', :class="[
'absolute inset-y-0 right-0 flex items-center pr-4', active ? 'text-white' : 'text-blue-600',
]"> 'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" /> <CheckIcon class="h-5 w-5" aria-hidden="true" />
</span> </span>
</li> </li>
@@ -52,47 +76,76 @@ v-if="selected" :class="[
<div v-if="currentlySelectedGame !== -1" class="flex flex-col gap-y-4"> <div v-if="currentlySelectedGame !== -1" class="flex flex-col gap-y-4">
<!-- without metadata option --> <!-- without metadata option -->
<div> <div>
<LoadingButton class="w-fit" :loading="importLoading" @click="() => importGame_wrapper(false)">Import without <LoadingButton
metadata class="w-fit"
:loading="importLoading"
@click="() => importGame_wrapper(false)"
>Import without metadata
</LoadingButton> </LoadingButton>
</div> </div>
<!-- divider --> <!-- divider -->
<div class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold"> <div
<div class="h-[1px] grow bg-zinc-800" />OR class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold"
>
<div class="h-[1px] grow bg-zinc-800" />
OR
<div class="h-[1px] grow bg-zinc-800" /> <div class="h-[1px] grow bg-zinc-800" />
</div> </div>
<!-- with metadata option --> <!-- with metadata option -->
<div class="flex flex-col gap-y-4"> <div class="flex flex-col gap-y-4">
<Listbox v-if="metadataResults && metadataResults.length > 0" v-model="currentlySelectedMetadata" as="div"> <Listbox
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game</ListboxLabel> v-if="metadataResults && metadataResults.length > 0"
v-model="currentlySelectedMetadata"
as="div"
>
<ListboxLabel
class="block text-sm font-medium leading-6 text-zinc-100"
>Select game</ListboxLabel
>
<div class="relative mt-2"> <div class="relative mt-2">
<ListboxButton <ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"> class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
>
<GameSearchResultWidget <GameSearchResultWidget
v-if="currentlySelectedMetadata != -1" v-if="currentlySelectedMetadata != -1"
:game="metadataResults[currentlySelectedMetadata]" /> :game="metadataResults[currentlySelectedMetadata]"
<span v-else class="block truncate text-zinc-600">Please select a game...</span> />
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span v-else class="block truncate text-zinc-600"
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" /> >Please select a game...</span
>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span> </span>
</ListboxButton> </ListboxButton>
<transition <transition
leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-active-class="transition ease-in duration-100"
leave-to-class="opacity-0"> leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions <ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption <ListboxOption
v-for="(result, resultIdx) in metadataResults" :key="result.id" v-slot="{ active, selected }" v-for="(result, resultIdx) in metadataResults"
as="template" :value="resultIdx"> :key="result.id"
v-slot="{ active }"
as="template"
:value="resultIdx"
>
<li <li
:class="[ :class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-100', active ? 'bg-blue-600 text-white' : 'text-zinc-100',
'relative cursor-default select-none py-2 pl-3 pr-9', 'relative cursor-default select-none py-2 pl-3 pr-9',
]"> ]"
>
<GameSearchResultWidget :game="result" /> <GameSearchResultWidget :game="result" />
</li> </li>
</ListboxOption> </ListboxOption>
@@ -101,23 +154,34 @@ v-for="(result, resultIdx) in metadataResults" :key="result.id" v-slot="{ active
</div> </div>
</Listbox> </Listbox>
<div <div
v-else-if="gameSearchResultsLoading" role="status" v-else-if="gameSearchResultsLoading"
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"> role="status"
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
>
Loading game results... Loading game results...
<svg <svg
aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-white" viewBox="0 0 100 101" aria-hidden="true"
fill="none" xmlns="http://www.w3.org/2000/svg"> class="w-6 h-6 text-transparent animate-spin fill-white"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" /> fill="currentColor"
/>
<path <path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" /> fill="currentFill"
/>
</svg> </svg>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
<div v-if="gameSearchResultsError" class="w-fit rounded-md bg-red-600/10 p-4"> <div
v-if="gameSearchResultsError"
class="w-fit rounded-md bg-red-600/10 p-4"
>
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" /> <XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
@@ -132,11 +196,17 @@ aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-white" view
<div> <div>
<LoadingButton <LoadingButton
class="w-fit" :loading="importLoading" :disabled="currentlySelectedMetadata === -1" class="w-fit"
@click="() => importGame_wrapper()">Import :loading="importLoading"
:disabled="currentlySelectedMetadata === -1"
@click="() => importGame_wrapper()"
>Import
</LoadingButton> </LoadingButton>
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4"> <div
v-if="importError"
class="mt-4 w-fit rounded-md bg-red-600/10 p-4"
>
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" /> <XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
@@ -187,18 +257,21 @@ async function updateSelectedGame(value: number) {
currentlySelectedMetadata.value = -1; currentlySelectedMetadata.value = -1;
const results = await $dropFetch( const results = await $dropFetch(
`/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}` `/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}`,
); );
metadataResults.value = results; metadataResults.value = results;
} }
function updateSelectedGame_wrapper(value: number) { function updateSelectedGame_wrapper(value: number) {
gameSearchResultsLoading.value = true; gameSearchResultsLoading.value = true;
updateSelectedGame(value).catch((error) => { updateSelectedGame(value)
gameSearchResultsError.value = error.statusMessage || "An unknown error occurred"; .catch((error) => {
}).finally(() => { gameSearchResultsError.value =
gameSearchResultsLoading.value = false; error.statusMessage || "An unknown error occurred";
}) })
.finally(() => {
gameSearchResultsLoading.value = false;
});
} }
const metadataResults = ref<Array<GameMetadataSearchResult> | undefined>(); const metadataResults = ref<Array<GameMetadataSearchResult> | undefined>();
@@ -215,7 +288,10 @@ async function importGame(metadata: boolean) {
method: "POST", method: "POST",
body: { body: {
path: games.unimportedGames[currentlySelectedGame.value], path: games.unimportedGames[currentlySelectedGame.value],
metadata: metadata && metadataResults.value ? metadataResults.value[currentlySelectedMetadata.value] : undefined, metadata:
metadata && metadataResults.value
? metadataResults.value[currentlySelectedMetadata.value]
: undefined,
}, },
}); });
+17 -16
View File
@@ -49,7 +49,7 @@
name="search" name="search"
class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:pl-9 sm:text-sm/6" class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:pl-9 sm:text-sm/6"
placeholder="Search library..." placeholder="Search library..."
> />
<MagnifyingGlassIcon <MagnifyingGlassIcon
class="pointer-events-none col-start-1 row-start-1 ml-3 size-5 self-center text-zinc-400 sm:size-4" class="pointer-events-none col-start-1 row-start-1 ml-3 size-5 self-center text-zinc-400 sm:size-4"
aria-hidden="true" aria-hidden="true"
@@ -69,7 +69,7 @@
class="h-16 w-16 flex-shrink-0 rounded-md" class="h-16 w-16 flex-shrink-0 rounded-md"
:src="useObject(game.mIconId)" :src="useObject(game.mIconId)"
alt="" alt=""
> />
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-sm font-medium text-zinc-100 font-display"> <h3 class="text-sm font-medium text-zinc-100 font-display">
{{ game.mName }} {{ game.mName }}
@@ -87,17 +87,17 @@
</dl> </dl>
<div class="inline-flex gap-x-2 items-center"> <div class="inline-flex gap-x-2 items-center">
<NuxtLink <NuxtLink
:href="`/admin/library/${game.id}`" :href="`/admin/library/${game.id}`"
class="mt-2 w-fit rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" class="mt-2 w-fit rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
> >
Edit &rarr; Edit &rarr;
</NuxtLink> </NuxtLink>
<button <button
class="mt-2 w-fit rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" class="mt-2 w-fit rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
@click="() => deleteGame(game.id)" @click="() => deleteGame(game.id)"
> >
Delete Delete
</button> </button>
</div> </div>
</div> </div>
</div> </div>
@@ -193,11 +193,12 @@ const libraryGames = ref(
}, },
hasNotifications: noVersions || toImport, hasNotifications: noVersions || toImport,
}; };
}) }),
); );
const filteredLibraryGames = computed(() => const filteredLibraryGames = computed(() =>
// @ts-ignore // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore excessively deep ts
libraryGames.value.filter((e) => { libraryGames.value.filter((e) => {
if (!searchQuery.value) return true; if (!searchQuery.value) return true;
const searchQueryLower = searchQuery.value.toLowerCase(); const searchQueryLower = searchQuery.value.toLowerCase();
@@ -205,7 +206,7 @@ const filteredLibraryGames = computed(() =>
if (e.mShortDescription.toLowerCase().includes(searchQueryLower)) if (e.mShortDescription.toLowerCase().includes(searchQueryLower))
return true; return true;
return false; return false;
}) }),
); );
async function deleteGame(id: string) { async function deleteGame(id: string) {
+3 -1
View File
@@ -1,4 +1,6 @@
<template></template> <template>
<div></div>
</template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
+1 -1
View File
@@ -80,7 +80,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { CheckCircleIcon } from "@heroicons/vue/16/solid"; import { CheckCircleIcon } from "@heroicons/vue/16/solid";
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/vue/24/solid"; import { ExclamationCircleIcon } from "@heroicons/vue/24/solid";
const route = useRoute(); const route = useRoute();
const taskId = route.params.id.toString(); const taskId = route.params.id.toString();
+11 -20
View File
@@ -164,7 +164,7 @@
autocomplete="username" autocomplete="username"
placeholder="myUsername" placeholder="myUsername"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
> />
</div> </div>
</div> </div>
@@ -191,7 +191,7 @@
autocomplete="email" autocomplete="email"
placeholder="me@example.com" placeholder="me@example.com"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6" class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
> />
</div> </div>
</div> </div>
@@ -262,7 +262,7 @@
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
> >
<ListboxOption <ListboxOption
v-for="[label, _] in Object.entries(expiry)" v-for="[label] in Object.entries(expiry)"
:key="label" :key="label"
v-slot="{ active, selected }" v-slot="{ active, selected }"
as="template" as="template"
@@ -354,7 +354,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
Dialog, Dialog,
DialogPanel,
DialogTitle, DialogTitle,
TransitionChild, TransitionChild,
TransitionRoot, TransitionRoot,
@@ -368,16 +367,8 @@ import {
ListboxOption, ListboxOption,
ListboxOptions, ListboxOptions,
} from "@headlessui/vue"; } from "@headlessui/vue";
import { import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
ChevronRightIcon, import { TrashIcon, XCircleIcon } from "@heroicons/vue/24/solid";
CheckIcon,
ChevronUpDownIcon,
} from "@heroicons/vue/20/solid";
import {
CalendarDateRangeIcon,
TrashIcon,
XCircleIcon,
} from "@heroicons/vue/24/solid";
import type { Invitation } from "@prisma/client"; import type { Invitation } from "@prisma/client";
import type { SerializeObject } from "nitropack"; import type { SerializeObject } from "nitropack";
import type { DurationLike } from "luxon"; import type { DurationLike } from "luxon";
@@ -392,7 +383,7 @@ useHead({
}); });
const data = await $dropFetch<Array<SerializeObject<Invitation>>>( const data = await $dropFetch<Array<SerializeObject<Invitation>>>(
"/api/v1/admin/auth/invitation" "/api/v1/admin/auth/invitation",
); );
const invitations = ref(data ?? []); const invitations = ref(data ?? []);
@@ -401,7 +392,7 @@ const generateInvitationUrl = (id: string) =>
const invitationUrls = ref<undefined | Array<string>>(); const invitationUrls = ref<undefined | Array<string>>();
onMounted(() => { onMounted(() => {
invitationUrls.value = invitations.value.map((invitation) => invitationUrls.value = invitations.value.map((invitation) =>
generateInvitationUrl(invitation.id) generateInvitationUrl(invitation.id),
); );
}); });
@@ -417,7 +408,7 @@ const username = computed({
}, },
}); });
const validUsername = computed(() => const validUsername = computed(() =>
_username.value === undefined ? true : _username.value.length >= 5 _username.value === undefined ? true : _username.value.length >= 5,
); );
// Same as above // Same as above
@@ -433,7 +424,7 @@ const email = computed({
}); });
const mailRegex = /^\S+@\S+\.\S+$/; const mailRegex = /^\S+@\S+\.\S+$/;
const validEmail = computed(() => const validEmail = computed(() =>
_email.value === undefined ? true : mailRegex.test(email.value as string) _email.value === undefined ? true : mailRegex.test(email.value as string),
); );
const isAdmin = ref(false); const isAdmin = ref(false);
@@ -459,7 +450,7 @@ const expiry: Record<string, DurationLike> = {
year: 3000, year: 3000,
}, // Never is relative, right? }, // Never is relative, right?
}; };
const expiryKey = ref<keyof typeof expiry>(Object.keys(expiry)[0] as any); // Cast to any because we just know it's okay const expiryKey = ref<keyof typeof expiry>(Object.keys(expiry)[0]); // Cast to any because we just know it's okay
const loading = ref(false); const loading = ref(false);
const error = ref<undefined | string>(); const error = ref<undefined | string>();
@@ -481,7 +472,7 @@ async function invite() {
email.value = ""; email.value = "";
username.value = ""; username.value = "";
isAdmin.value = false; isAdmin.value = false;
expiryKey.value = Object.keys(expiry)[0] as any; // Same reason as above expiryKey.value = Object.keys(expiry)[0]; // Same reason as above
return newInvitation; return newInvitation;
} }
+3 -3
View File
@@ -1,12 +1,12 @@
<template> <template>
<div></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
useHead({ useHead({
title: "Home", title: "Home",
}); });
const router = useRouter(); const router = useRouter();
router.replace('/store') router.replace("/store");
</script> </script>
+1 -10
View File
@@ -97,16 +97,7 @@ import {
TransitionChild, TransitionChild,
TransitionRoot, TransitionRoot,
} from "@headlessui/vue"; } from "@headlessui/vue";
import { import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
Bars3Icon,
CalendarIcon,
ChartPieIcon,
DocumentDuplicateIcon,
FolderIcon,
HomeIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/vue/24/outline";
const router = useRouter(); const router = useRouter();
const sidebarOpen = ref(false); const sidebarOpen = ref(false);
+2 -2
View File
@@ -33,12 +33,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowLeftIcon, TrashIcon } from "@heroicons/vue/20/solid"; import { ArrowLeftIcon } from "@heroicons/vue/20/solid";
const route = useRoute(); const route = useRoute();
const collections = await useCollections(); const collections = await useCollections();
const collection = computed(() => const collection = computed(() =>
collections.value.find((e) => e.id == route.params.id) collections.value.find((e) => e.id == route.params.id),
); );
if (collection.value === undefined) { if (collection.value === undefined) {
throw createError({ statusCode: 404, statusMessage: "Collection not found" }); throw createError({ statusCode: 404, statusMessage: "Collection not found" });
+16 -19
View File
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div <div
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden" class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
@@ -7,7 +8,7 @@
<img <img
:src="useObject(game.mBannerId)" :src="useObject(game.mBannerId)"
class="w-full h-[24rem] object-cover blur-sm scale-105" class="w-full h-[24rem] object-cover blur-sm scale-105"
> />
<div <div
class="absolute inset-0 bg-gradient-to-t from-zinc-900 to-transparent opacity-90" class="absolute inset-0 bg-gradient-to-t from-zinc-900 to-transparent opacity-90"
/> />
@@ -76,7 +77,7 @@
<img <img
class="w-fit h-48 lg:h-96 rounded" class="w-fit h-48 lg:h-96 rounded"
:src="useObject(image)" :src="useObject(image)"
> />
</VueSlide> </VueSlide>
<VueSlide v-if="game.mImageCarousel.length == 0"> <VueSlide v-if="game.mImageCarousel.length == 0">
<div <div
@@ -112,13 +113,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
ArrowLeftIcon, ArrowLeftIcon,
ChevronLeftIcon,
ChevronRightIcon,
PhotoIcon,
ArrowTopRightOnSquareIcon, ArrowTopRightOnSquareIcon,
ArrowUpRightIcon, ArrowUpRightIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { micromark } from "micromark"; import { micromark } from "micromark";
import type { Game } from "@prisma/client"; import type { Game } from "@prisma/client";
@@ -135,23 +132,23 @@ const game = computed(() => {
// Convert markdown to HTML // Convert markdown to HTML
const descriptionHTML = computed(() => const descriptionHTML = computed(() =>
micromark(game.value.mDescription ?? "") micromark(game.value.mDescription ?? ""),
); );
const currentImageIndex = ref(0); // const currentImageIndex = ref(0);
function nextImage() { // function nextImage() {
if (!game.value?.mImageCarousel) return; // if (!game.value?.mImageCarousel) return;
currentImageIndex.value = // currentImageIndex.value =
(currentImageIndex.value + 1) % game.value.mImageCarousel.length; // (currentImageIndex.value + 1) % game.value.mImageCarousel.length;
} // }
function previousImage() { // function previousImage() {
if (!game.value?.mImageCarousel) return; // if (!game.value?.mImageCarousel) return;
currentImageIndex.value = // currentImageIndex.value =
(currentImageIndex.value - 1 + game.value.mImageCarousel.length) % // (currentImageIndex.value - 1 + game.value.mImageCarousel.length) %
game.value.mImageCarousel.length; // game.value.mImageCarousel.length;
} // }
</script> </script>
<style scoped> <style scoped>
+82 -85
View File
@@ -1,101 +1,98 @@
<template> <template>
<div class="flex flex-col gap-y-8"> <div>
<div class="max-w-2xl"> <div class="flex flex-col gap-y-8">
<h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2> <div class="max-w-2xl">
<p class="mt-2 text-zinc-400"> <h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2>
Organize your games into collections for easy access, and access all <p class="mt-2 text-zinc-400">
your games. Organize your games into collections for easy access, and access all
</p> your games.
</div> </p>
</div>
<!-- Collections grid --> <!-- Collections grid -->
<TransitionGroup <TransitionGroup
name="collection-list" name="collection-list"
tag="div" tag="div"
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
<!-- Collection buttons (wrap each in a div for grid layout) -->
<div
v-for="collection in collections"
:key="collection.id"
class="flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105 focus:scale-105"
> >
<NuxtLink <!-- Collection buttons (wrap each in a div for grid layout) -->
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 focus:bg-zinc-800 focus:outline-none" <div
:href="`/library/collection/${collection.id}`" v-for="collection in collections"
:key="collection.id"
class="flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105 focus:scale-105"
> >
<h3 class="text-lg font-semibold text-zinc-100"> <NuxtLink
{{ collection.name }} class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 focus:bg-zinc-800 focus:outline-none"
</h3> :href="`/library/collection/${collection.id}`"
<p class="mt-1 text-sm text-zinc-400">
{{ collection.entries.length }} game(s)
</p>
</NuxtLink>
<!-- Delete button (only show for non-default collections) -->
<button
class="group px-3 ml-[2px] bg-zinc-800/50 hover:bg-zinc-800 group focus:bg-zinc-800 focus:outline-none"
@click="() => (currentlyDeleting = collection)"
>
<TrashIcon
class="transition-all size-5 text-zinc-400 group-hover:text-red-400 group-hover:rotate-[8deg]"
/>
</button>
</div>
<!-- Create new collection button (also wrap in div) -->
<div>
<button
class="group flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105"
@click="collectionCreateOpen = true"
>
<div
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 border-2 border-dashed border-zinc-700"
> >
<div class="flex items-center gap-3"> <h3 class="text-lg font-semibold text-zinc-100">
<PlusIcon {{ collection.name }}
class="h-5 w-5 text-zinc-400 group-hover:text-zinc-300 transition-all duration-300 group-hover:rotate-90" </h3>
/> <p class="mt-1 text-sm text-zinc-400">
<h3 {{ collection.entries.length }} game(s)
class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"
>
Create Collection
</h3>
</div>
<p class="mt-1 text-sm text-zinc-500 group-hover:text-zinc-400">
Add a new collection to organize your games
</p> </p>
</div> </NuxtLink>
</button>
</div>
</TransitionGroup>
<!-- game library grid --> <!-- Delete button (only show for non-default collections) -->
<div> <button
<h1 class="text-zinc-100 text-xl font-bold font-display">All Games</h1> class="group px-3 ml-[2px] bg-zinc-800/50 hover:bg-zinc-800 group focus:bg-zinc-800 focus:outline-none"
<div class="mt-4 flex flex-row flex-wrap justify-left gap-4"> @click="() => (currentlyDeleting = collection)"
<GamePanel >
v-for="game in games" <TrashIcon
:key="game.id" class="transition-all size-5 text-zinc-400 group-hover:text-red-400 group-hover:rotate-[8deg]"
:game="game" />
:href="`/library/game/${game?.id}`" </button>
/> </div>
<!-- Create new collection button (also wrap in div) -->
<div>
<button
class="group flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105"
@click="collectionCreateOpen = true"
>
<div
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 border-2 border-dashed border-zinc-700"
>
<div class="flex items-center gap-3">
<PlusIcon
class="h-5 w-5 text-zinc-400 group-hover:text-zinc-300 transition-all duration-300 group-hover:rotate-90"
/>
<h3
class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"
>
Create Collection
</h3>
</div>
<p class="mt-1 text-sm text-zinc-500 group-hover:text-zinc-400">
Add a new collection to organize your games
</p>
</div>
</button>
</div>
</TransitionGroup>
<!-- game library grid -->
<div>
<h1 class="text-zinc-100 text-xl font-bold font-display">All Games</h1>
<div class="mt-4 flex flex-row flex-wrap justify-left gap-4">
<GamePanel
v-for="game in games"
:key="game.id"
:game="game"
:href="`/library/game/${game?.id}`"
/>
</div>
</div> </div>
</div> </div>
</div>
<CreateCollectionModal v-model="collectionCreateOpen" /> <CreateCollectionModal v-model="collectionCreateOpen" />
<DeleteCollectionModal v-model="currentlyDeleting" /> <DeleteCollectionModal v-model="currentlyDeleting" />
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { TrashIcon, PlusIcon } from "@heroicons/vue/20/solid";
ArrowTopRightOnSquareIcon, import type { Collection } from "@prisma/client";
ArrowUpRightIcon,
TrashIcon,
ArrowLeftIcon, PlusIcon
} from "@heroicons/vue/20/solid";
import type { Collection, Game, GameVersion } from "@prisma/client";
const collections = await useCollections(); const collections = await useCollections();
const collectionCreateOpen = ref(false); const collectionCreateOpen = ref(false);
+2 -11
View File
@@ -97,22 +97,13 @@ import {
TransitionChild, TransitionChild,
TransitionRoot, TransitionRoot,
} from "@headlessui/vue"; } from "@headlessui/vue";
import { import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
Bars3Icon,
CalendarIcon,
ChartPieIcon,
DocumentDuplicateIcon,
FolderIcon,
HomeIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/vue/24/outline";
const news = useNews(); const news = useNews();
if (!news.value) { if (!news.value) {
await fetchNews(); await fetchNews();
console.log('fetched news') console.log("fetched news");
} }
const router = useRouter(); const router = useRouter();
+77 -69
View File
@@ -1,79 +1,85 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div>
<!-- Banner header with blurred background --> <div v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden"> <!-- Banner header with blurred background -->
<div v-if="article.image" class="absolute inset-0"> <div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden">
<img <div v-if="article.image" class="absolute inset-0">
:src="useObject(article.image)" <img
alt="" :src="useObject(article.image)"
class="w-full h-full object-cover blur-sm scale-110" alt=""
> class="w-full h-full object-cover blur-sm scale-110"
<div />
class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-950"
/>
</div>
<div v-else>
<!-- Fallback gradient background when no image -->
<div
class="absolute inset-0 bg-gradient-to-b from-zinc-800 to-zinc-900"
/>
</div>
<div class="relative h-full flex flex-col justify-end p-8">
<div class="flex items-center gap-x-3 mb-6">
<NuxtLink
to="/news"
class="px-2 py-1 rounded bg-zinc-900/80 backdrop-blur-sm transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
Back to News
</NuxtLink>
<button
v-if="user?.admin"
class="px-2 py-1 rounded bg-red-900/50 backdrop-blur-sm transition text-sm/6 font-semibold text-red-400 hover:text-red-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
@click="() => (currentlyDeleting = article)"
>
<TrashIcon class="h-4 w-4" aria-hidden="true" />
Delete Article
</button>
</div>
<div class="max-w-[calc(100%-2rem)]">
<h1 class="text-4xl font-bold text-white mb-3">
{{ article.title }}
</h1>
<div <div
class="flex flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-4 text-zinc-300" class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-950"
> />
<div class="flex items-center gap-x-4"> </div>
<time :datetime="article.publishedAt">{{ <div v-else>
formatDate(article.publishedAt) <!-- Fallback gradient background when no image -->
}}</time> <div
<span class="text-blue-400">{{ class="absolute inset-0 bg-gradient-to-b from-zinc-800 to-zinc-900"
article.author?.displayName ?? "System" />
}}</span> </div>
</div>
<div class="flex flex-wrap gap-2"> <div class="relative h-full flex flex-col justify-end p-8">
<span <div class="flex items-center gap-x-3 mb-6">
v-for="tag in article.tags" <NuxtLink
:key="tag.id" to="/news"
class="inline-flex items-center rounded-full bg-zinc-800/80 backdrop-blur-sm px-3 py-1 text-sm font-semibold text-zinc-100" class="px-2 py-1 rounded bg-zinc-900/80 backdrop-blur-sm transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
> >
{{ tag.name }} <ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
</span> Back to News
</div> </NuxtLink>
<button
v-if="user?.admin"
class="px-2 py-1 rounded bg-red-900/50 backdrop-blur-sm transition text-sm/6 font-semibold text-red-400 hover:text-red-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
@click="() => (currentlyDeleting = article)"
>
<TrashIcon class="h-4 w-4" aria-hidden="true" />
Delete Article
</button>
</div>
<div class="max-w-[calc(100%-2rem)]">
<h1 class="text-4xl font-bold text-white mb-3">
{{ article.title }}
</h1>
<div
class="flex flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-4 text-zinc-300"
>
<div class="flex items-center gap-x-4">
<time :datetime="article.publishedAt">{{
formatDate(article.publishedAt)
}}</time>
<span class="text-blue-400">{{
article.author?.displayName ?? "System"
}}</span>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="tag in article.tags"
:key="tag.id"
class="inline-flex items-center rounded-full bg-zinc-800/80 backdrop-blur-sm px-3 py-1 text-sm font-semibold text-zinc-100"
>
{{ tag.name }}
</span>
</div>
</div>
<p class="mt-4 text-lg text-zinc-300">{{ article.description }}</p>
</div> </div>
<p class="mt-4 text-lg text-zinc-300">{{ article.description }}</p>
</div> </div>
</div> </div>
<!-- Article content - markdown -->
<div
class="mx-auto prose prose-invert prose-lg"
v-html="renderedContent"
/>
</div> </div>
<!-- Article content - markdown --> <DeleteNewsModal v-model="currentlyDeleting" />
<div class="mx-auto prose prose-invert prose-lg" v-html="renderedContent" />
</div> </div>
<DeleteNewsModal v-model="currentlyDeleting" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -88,7 +94,9 @@ const news = useNews();
if (!news.value) { if (!news.value) {
news.value = await fetchNews(); news.value = await fetchNews();
} }
const article = computed(() => news.value?.find((e) => e.id == route.params.id)); const article = computed(() =>
news.value?.find((e) => e.id == route.params.id),
);
if (!article.value) if (!article.value)
throw createError({ throw createError({
statusCode: 404, statusCode: 404,
+2 -2
View File
@@ -30,7 +30,7 @@
:src="useObject(article.image)" :src="useObject(article.image)"
alt="" alt=""
class="h-full w-full object-cover object-center transition-all duration-500 group-hover:scale-110 scale-105" class="h-full w-full object-cover object-center transition-all duration-500 group-hover:scale-110 scale-105"
> />
<div class="absolute top-4 left-4 flex gap-2"> <div class="absolute top-4 left-4 flex gap-2">
<span <span
v-for="tag in article.tags" v-for="tag in article.tags"
@@ -84,7 +84,7 @@ import { DocumentIcon } from "@heroicons/vue/24/outline";
import type { Article } from "@prisma/client"; import type { Article } from "@prisma/client";
import type { SerializeObject } from "nitropack/types"; import type { SerializeObject } from "nitropack/types";
const props = defineProps<{ const { articles } = defineProps<{
articles: SerializeObject< articles: SerializeObject<
Article & { Article & {
tags: Array<{ name: string; id: string }>; tags: Array<{ name: string; id: string }>;
+2 -1
View File
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div <div
class="mx-auto bg-zinc-950 w-full relative flex flex-col justify-center pt-32 xl:pt-24 z-10 overflow-hidden" class="mx-auto bg-zinc-950 w-full relative flex flex-col justify-center pt-32 xl:pt-24 z-10 overflow-hidden"
@@ -197,7 +198,7 @@ const descriptionSplitIndex = gameDescriptionCharacters.findIndex(
if (i < 500) return false; if (i < 500) return false;
if (v != "\n") return false; if (v != "\n") return false;
return true; return true;
} },
); );
const previewDescription = gameDescriptionCharacters const previewDescription = gameDescriptionCharacters
+3 -3
View File
@@ -17,7 +17,7 @@
:src="useObject(game.mBannerId)" :src="useObject(game.mBannerId)"
alt="" alt=""
class="size-full object-cover object-center" class="size-full object-cover object-center"
> />
</div> </div>
<div <div
class="relative flex items-center justify-center w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16" class="relative flex items-center justify-center w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
@@ -99,8 +99,8 @@ const recent = await $dropFetch("/api/v1/store/recent");
const updated = await $dropFetch("/api/v1/store/updated"); const updated = await $dropFetch("/api/v1/store/updated");
const released = await $dropFetch("/api/v1/store/released"); const released = await $dropFetch("/api/v1/store/released");
const developers = await $dropFetch("/api/v1/store/developers"); // const developers = await $dropFetch("/api/v1/store/developers");
const publishers = await $dropFetch("/api/v1/store/publishers"); // const publishers = await $dropFetch("/api/v1/store/publishers");
useHead({ useHead({
title: "Store", title: "Store",
+1 -4
View File
@@ -1,11 +1,8 @@
import aclManager from "~/server/internal/acls"; import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [ const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]);
"game:delete",
]);
if (!allowed) throw createError({ statusCode: 403 }); if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3); const query = getQuery(h3);
+2 -2
View File
@@ -1,4 +1,4 @@
import { defineEventHandler, createError, readBody } from "h3"; import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls"; import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news"; import newsManager from "~/server/internal/news";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload"; import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
@@ -21,7 +21,7 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Failed to upload file", statusMessage: "Failed to upload file",
}); });
const [imageId, options, pull, dump] = uploadResult; const [imageId, options, pull, _dump] = uploadResult;
const title = options.title; const title = options.title;
const description = options.description; const description = options.description;
+2 -2
View File
@@ -1,4 +1,4 @@
import { AuthMec, Invitation } from "@prisma/client"; import { AuthMec } from "@prisma/client";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { createHashArgon2 } from "~/server/internal/security/simple"; import { createHashArgon2 } from "~/server/internal/security/simple";
import * as jdenticon from "jdenticon"; import * as jdenticon from "jdenticon";
@@ -63,7 +63,7 @@ export default defineEventHandler(async (h3) => {
profilePictureId, profilePictureId,
async () => jdenticon.toPng(user.username, 256), async () => jdenticon.toPng(user.username, 256),
{}, {},
[`internal:read`, `${userId}:read`] [`internal:read`, `${userId}:read`],
); );
const [linkMec] = await prisma.$transaction([ const [linkMec] = await prisma.$transaction([
prisma.linkedAuthMec.create({ prisma.linkedAuthMec.create({
+1 -3
View File
@@ -1,3 +1 @@
export default defineEventHandler((h3) => { export default defineEventHandler((_h3) => {});
})
+1 -2
View File
@@ -1,4 +1,3 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@@ -33,7 +32,7 @@ export default defineEventHandler(async (h3) => {
const versionDir = path.join( const versionDir = path.join(
libraryManager.fetchLibraryPath(), libraryManager.fetchLibraryPath(),
game.libraryBasePath, game.libraryBasePath,
versionName versionName,
); );
if (!fs.existsSync(versionDir)) if (!fs.existsSync(versionDir))
throw createError({ throw createError({
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -20,7 +19,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
const successful = await userLibraryManager.collectionRemove( const successful = await userLibraryManager.collectionRemove(
gameId, gameId,
id, id,
user.id user.id,
); );
if (!successful) if (!successful)
throw createError({ throw createError({
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -1,4 +1,3 @@
import aclManager from "~/server/internal/acls";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
@@ -12,6 +11,9 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
throw createError({ statusCode: 400, statusMessage: "Requires name" }); throw createError({ statusCode: 400, statusMessage: "Requires name" });
// Create the collection using the manager // Create the collection using the manager
const newCollection = await userLibraryManager.collectionCreate(name, user.id); const newCollection = await userLibraryManager.collectionCreate(
name,
user.id,
);
return newCollection; return newCollection;
}); });
+3 -4
View File
@@ -1,8 +1,7 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
import { DropManifest } from "~/server/internal/downloads/manifest";
export default defineClientEventHandler(async (h3, {}) => { export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3); const query = getQuery(h3);
const id = query.id?.toString(); const id = query.id?.toString();
if (!id) if (!id)
@@ -23,9 +22,9 @@ export default defineClientEventHandler(async (h3, {}) => {
const mappedVersions = versions const mappedVersions = versions
.map((version) => { .map((version) => {
if (!version.dropletManifest) return undefined; if (!version.dropletManifest) return undefined;
const newVersion = { ...version, dropletManifest: undefined }; const newVersion = { ...version, dropletManifest: undefined };
// @ts-expect-error // @ts-expect-error idk why we delete an undefined object
delete newVersion.dropletManifest; delete newVersion.dropletManifest;
return { return {
...newVersion, ...newVersion,
+12 -15
View File
@@ -1,20 +1,17 @@
import { ClientCapabilities } from "@prisma/client"; import { ClientCapabilities } from "@prisma/client";
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import { applicationSettings } from "~/server/internal/config/application-configuration"; import { applicationSettings } from "~/server/internal/config/application-configuration";
import prisma from "~/server/internal/db/database";
export default defineClientEventHandler( export default defineClientEventHandler(async (_h3, { fetchClient }) => {
async (h3, { fetchClient, fetchUser }) => { const client = await fetchClient();
const client = await fetchClient(); if (!client.capabilities.includes(ClientCapabilities.CloudSaves))
if (!client.capabilities.includes(ClientCapabilities.CloudSaves)) throw createError({
throw createError({ statusCode: 403,
statusCode: 403, statusMessage: "Capability not allowed.",
statusMessage: "Capability not allowed.", });
});
const slotLimit = await applicationSettings.get("saveSlotCountLimit"); const slotLimit = await applicationSettings.get("saveSlotCountLimit");
const sizeLimit = await applicationSettings.get("saveSlotSizeLimit"); const sizeLimit = await applicationSettings.get("saveSlotSizeLimit");
const history = await applicationSettings.get("saveSlotHistoryLimit"); const history = await applicationSettings.get("saveSlotHistoryLimit");
return { slotLimit, sizeLimit, history }; return { slotLimit, sizeLimit, history };
} });
);
+1 -2
View File
@@ -1,8 +1,7 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
import prisma from "~/server/internal/db/database";
import userLibraryManager from "~/server/internal/userlibrary"; import userLibraryManager from "~/server/internal/userlibrary";
export default defineClientEventHandler(async (h3, { fetchUser }) => { export default defineClientEventHandler(async (_h3, { fetchUser }) => {
const user = await fetchUser(); const user = await fetchUser();
const library = await userLibraryManager.fetchLibrary(user.id); const library = await userLibraryManager.fetchLibrary(user.id);
return library.entries.map((e) => e.game); return library.entries.map((e) => e.game);
+1 -1
View File
@@ -1,4 +1,4 @@
export default defineEventHandler((h3) => { export default defineEventHandler((_h3) => {
return { return {
appName: "Drop", appName: "Drop",
}; };
+5 -7
View File
@@ -1,11 +1,9 @@
import notificationSystem from "~/server/internal/notifications"; import notificationSystem from "~/server/internal/notifications";
import session from "~/server/internal/session";
import { parse as parseCookies } from "cookie-es";
import aclManager from "~/server/internal/acls"; import aclManager from "~/server/internal/acls";
// TODO add web socket sessions for horizontal scaling // TODO add web socket sessions for horizontal scaling
// Peer ID to user ID // Peer ID to user ID
const socketSessions: { [key: string]: string } = {}; const socketSessions = new Map<string, string>();
export default defineWebSocketHandler({ export default defineWebSocketHandler({
async open(peer) { async open(peer) {
@@ -25,7 +23,7 @@ export default defineWebSocketHandler({
userIds.push("system"); userIds.push("system");
} }
socketSessions[peer.id] = userId; socketSessions.set(peer.id, userId);
for (const listenUserId of userIds) { for (const listenUserId of userIds) {
notificationSystem.listen(listenUserId, peer.id, (notification) => { notificationSystem.listen(listenUserId, peer.id, (notification) => {
@@ -33,8 +31,8 @@ export default defineWebSocketHandler({
}); });
} }
}, },
async close(peer, details) { async close(peer, _details) {
const userId = socketSessions[peer.id]; const userId = socketSessions.get(peer.id);
if (!userId) { if (!userId) {
console.log(`skipping websocket close for ${peer.id}`); console.log(`skipping websocket close for ${peer.id}`);
return; return;
@@ -42,6 +40,6 @@ export default defineWebSocketHandler({
notificationSystem.unlisten(userId, peer.id); notificationSystem.unlisten(userId, peer.id);
notificationSystem.unlisten("system", peer.id); // In case we were listening as 'system' notificationSystem.unlisten("system", peer.id); // In case we were listening as 'system'
delete socketSessions[peer.id]; socketSessions.delete(peer.id);
}, },
}); });
+10 -11
View File
@@ -1,11 +1,9 @@
import session from "~/server/internal/session"; import taskHandler from "~/server/internal/tasks";
import taskHandler, { TaskMessage } from "~/server/internal/tasks";
import { parse as parseCookies } from "cookie-es";
import type { MinimumRequestObject } from "~/server/h3"; import type { MinimumRequestObject } from "~/server/h3";
// TODO add web socket sessions for horizontal scaling // TODO add web socket sessions for horizontal scaling
// ID to admin // ID to admin
const socketHeaders: { [key: string]: MinimumRequestObject } = {}; const socketHeaders = new Map<string, MinimumRequestObject>();
export default defineWebSocketHandler({ export default defineWebSocketHandler({
async open(peer) { async open(peer) {
@@ -15,25 +13,26 @@ export default defineWebSocketHandler({
return; return;
} }
socketHeaders[peer.id] = { socketHeaders.set(peer.id, {
headers: request.headers ?? new Headers(), headers: request.headers ?? new Headers(),
}; });
peer.send(`connect`); peer.send(`connect`);
}, },
message(peer, message) { message(peer, message) {
if (!peer.id) return; if (!peer.id) return;
if (socketHeaders[peer.id] === undefined) return; const headers = socketHeaders.get(peer.id);
if (headers === undefined) return;
const text = message.text(); const text = message.text();
if (text.startsWith("connect/")) { if (text.startsWith("connect/")) {
const id = text.substring("connect/".length); const id = text.substring("connect/".length);
taskHandler.connect(peer.id, id, peer, socketHeaders[peer.id]); taskHandler.connect(peer.id, id, peer, headers);
return; return;
} }
}, },
close(peer, details) { close(peer, _details) {
if (!peer.id) return; if (!peer.id) return;
if (socketHeaders[peer.id] === undefined) return; if (!socketHeaders.has(peer.id)) return;
delete socketHeaders[peer.id]; socketHeaders.delete(peer.id);
taskHandler.disconnectAll(peer.id); taskHandler.disconnectAll(peer.id);
}, },
@@ -1,6 +1,5 @@
import aclManager from "~/server/internal/acls"; import aclManager from "~/server/internal/acls";
import clientHandler from "~/server/internal/clients/handler"; import clientHandler from "~/server/internal/clients/handler";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]); const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]);
+18 -13
View File
@@ -1,6 +1,5 @@
import { APITokenMode } from "@prisma/client"; import { APITokenMode } from "@prisma/client";
import aclManager, { userACLs } from "~/server/internal/acls"; import aclManager, { userACLs } from "~/server/internal/acls";
import { userACLDescriptions } from "~/server/internal/acls/descriptions";
import prisma from "~/server/internal/db/database"; import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
@@ -25,17 +24,23 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Token requires more than zero ACLs", statusMessage: "Token requires more than zero ACLs",
}); });
const invalidACLs = acls.filter((e) => userACLs.findIndex((v) => e == v) == -1); const invalidACLs = acls.filter(
if(invalidACLs.length > 0) throw createError({statusCode: 400, statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`}); (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({ const token = await prisma.aPIToken.create({
data: { data: {
mode: APITokenMode.User, mode: APITokenMode.User,
name: name, name: name,
userId: userId, userId: userId,
acls: acls, acls: acls,
} },
}) });
return token; return token;
}); });
+9 -6
View File
@@ -1,5 +1,4 @@
import { APITokenMode, User } from "@prisma/client"; import { APITokenMode } from "@prisma/client";
import { H3Context, H3Event } from "h3";
import prisma from "../db/database"; import prisma from "../db/database";
import sessionHandler from "../session"; import sessionHandler from "../session";
import type { MinimumRequestObject } from "~/server/h3"; import type { MinimumRequestObject } from "~/server/h3";
@@ -98,7 +97,7 @@ class ACLManager {
if (!token) return undefined; if (!token) return undefined;
if (!token.userId) if (!token.userId)
throw new Error( throw new Error(
"No userId on user or client token - is something broken?" "No userId on user or client token - is something broken?",
); );
for (const acl of acls) { for (const acl of acls) {
@@ -124,7 +123,7 @@ class ACLManager {
async allowSystemACL( async allowSystemACL(
request: MinimumRequestObject | undefined, request: MinimumRequestObject | undefined,
acls: SystemACL acls: SystemACL,
) { ) {
if (!request) if (!request)
throw new Error("Native web requests not available - weird deployment?"); throw new Error("Native web requests not available - weird deployment?");
@@ -157,13 +156,17 @@ class ACLManager {
for (const acl of acls) { for (const acl of acls) {
if (acl.startsWith(userACLPrefix)) { if (acl.startsWith(userACLPrefix)) {
const rawACL = acl.substring(userACLPrefix.length); const rawACL = acl.substring(userACLPrefix.length);
const userId = await this.getUserIdACL(request, [rawACL as any]); const userId = await this.getUserIdACL(request, [
rawACL as UserACL[number],
]);
if (!userId) return false; if (!userId) return false;
} }
if (acl.startsWith(systemACLPrefix)) { if (acl.startsWith(systemACLPrefix)) {
const rawACL = acl.substring(systemACLPrefix.length); const rawACL = acl.substring(systemACLPrefix.length);
const allowed = await this.allowSystemACL(request, [rawACL as any]); const allowed = await this.allowSystemACL(request, [
rawACL as SystemACL[number],
]);
if (!allowed) return false; if (!allowed) return false;
} }
} }
+3 -6
View File
@@ -1,8 +1,5 @@
import path from "path";
import fs from "fs";
import droplet from "@drop-oss/droplet"; import droplet from "@drop-oss/droplet";
import type { CertificateStore} from "./ca-store"; import type { CertificateStore } from "./ca-store";
import { fsCertificateStore } from "./ca-store";
export type CertificateBundle = { export type CertificateBundle = {
priv: string; priv: string;
@@ -50,7 +47,7 @@ export class CertificateAuthority {
clientId, clientId,
clientName, clientName,
caCertificate.cert, caCertificate.cert,
caCertificate.priv caCertificate.priv,
); );
const certBundle: CertificateBundle = { const certBundle: CertificateBundle = {
priv, priv,
@@ -65,7 +62,7 @@ export class CertificateAuthority {
async fetchClientCertificate(clientId: string) { async fetchClientCertificate(clientId: string) {
const isBlacklist = await this.certificateStore.checkBlacklistCertificate( const isBlacklist = await this.certificateStore.checkBlacklistCertificate(
`client:${clientId}` `client:${clientId}`,
); );
if (isBlacklist) return undefined; if (isBlacklist) return undefined;
return await this.certificateStore.fetch(`client:${clientId}`); return await this.certificateStore.fetch(`client:${clientId}`);
+8 -6
View File
@@ -18,8 +18,8 @@ export const validCapabilities = Object.values(InternalClientCapability);
export type CapabilityConfiguration = { export type CapabilityConfiguration = {
[InternalClientCapability.PeerAPI]: { endpoints: string[] }; [InternalClientCapability.PeerAPI]: { endpoints: string[] };
[InternalClientCapability.UserStatus]: {}; [InternalClientCapability.UserStatus]: object;
[InternalClientCapability.CloudSaves]: {}; [InternalClientCapability.CloudSaves]: object;
}; };
class CapabilityManager { class CapabilityManager {
@@ -53,7 +53,7 @@ class CapabilityManager {
const serverCertificate = await ca.fetchClientCertificate("server"); const serverCertificate = await ca.fetchClientCertificate("server");
if (!serverCertificate) if (!serverCertificate)
throw new Error( throw new Error(
"CA not initialised properly - server mTLS certificate not present" "CA not initialised properly - server mTLS certificate not present",
); );
const httpsAgent = new https.Agent({ const httpsAgent = new https.Agent({
key: serverCertificate.priv, key: serverCertificate.priv,
@@ -70,7 +70,9 @@ class CapabilityManager {
}); });
valid = true; valid = true;
break; break;
} catch {} } catch {
/* empty */
}
} }
return valid; return valid;
@@ -81,7 +83,7 @@ class CapabilityManager {
async validateCapabilityConfiguration( async validateCapabilityConfiguration(
capability: InternalClientCapability, capability: InternalClientCapability,
configuration: object configuration: object,
) { ) {
const validationFunction = this.validationFunctions[capability]; const validationFunction = this.validationFunctions[capability];
if (!validationFunction) return false; if (!validationFunction) return false;
@@ -91,7 +93,7 @@ class CapabilityManager {
async upsertClientCapability( async upsertClientCapability(
capability: InternalClientCapability, capability: InternalClientCapability,
rawCapability: object, rawCapability: object,
clientId: string clientId: string,
) { ) {
const upsertFunctions: EnumDictionary< const upsertFunctions: EnumDictionary<
InternalClientCapability, InternalClientCapability,
+11 -9
View File
@@ -6,7 +6,7 @@ import { useCertificateAuthority } from "~/server/plugins/ca";
export type EventHandlerFunction<T> = ( export type EventHandlerFunction<T> = (
h3: H3Event<EventHandlerRequest>, h3: H3Event<EventHandlerRequest>,
utils: ClientUtils utils: ClientUtils,
) => Promise<T> | T; ) => Promise<T> | T;
type ClientUtils = { type ClientUtils = {
@@ -25,7 +25,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
let clientId: string; let clientId: string;
switch (method) { switch (method) {
case "Debug": case "Debug": {
if (!import.meta.dev) throw createError({ statusCode: 403 }); if (!import.meta.dev) throw createError({ statusCode: 403 });
const client = await prisma.client.findFirst({ select: { id: true } }); const client = await prisma.client.findFirst({ select: { id: true } });
if (!client) if (!client)
@@ -35,7 +35,8 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
}); });
clientId = client.id; clientId = client.id;
break; break;
case "Nonce": }
case "Nonce": {
clientId = parts[0]; clientId = parts[0];
const nonce = parts[1]; const nonce = parts[1];
const signature = parts[2]; const signature = parts[2];
@@ -59,9 +60,8 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
} }
const certificateAuthority = useCertificateAuthority(); const certificateAuthority = useCertificateAuthority();
const certBundle = await certificateAuthority.fetchClientCertificate( const certBundle =
clientId await certificateAuthority.fetchClientCertificate(clientId);
);
// This does the blacklist check already // This does the blacklist check already
if (!certBundle) if (!certBundle)
throw createError({ throw createError({
@@ -76,11 +76,13 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
statusMessage: "Invalid nonce signature.", statusMessage: "Invalid nonce signature.",
}); });
break; break;
default: }
default: {
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
statusMessage: "No authentication", statusMessage: "No authentication",
}); });
}
} }
if (clientId === undefined) if (clientId === undefined)
@@ -95,7 +97,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
}); });
if (!client) if (!client)
throw new Error( throw new Error(
"client util fetch client broke - this should NOT happen" "client util fetch client broke - this should NOT happen",
); );
return client; return client;
} }
@@ -110,7 +112,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
if (!client) if (!client)
throw new Error( throw new Error(
"client util fetch client broke - this should NOT happen" "client util fetch client broke - this should NOT happen",
); );
return client.user; return client.user;
+21 -18
View File
@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { CertificateBundle } from "./ca";
import prisma from "../db/database"; import prisma from "../db/database";
import type { Platform } from "@prisma/client"; import type { Platform } from "@prisma/client";
import { useCertificateAuthority } from "~/server/plugins/ca"; import { useCertificateAuthority } from "~/server/plugins/ca";
@@ -10,25 +9,29 @@ export interface ClientMetadata {
} }
export class ClientHandler { export class ClientHandler {
private temporaryClientTable: { private temporaryClientTable = new Map<
[key: string]: { string,
{
timeout: NodeJS.Timeout; timeout: NodeJS.Timeout;
data: ClientMetadata; data: ClientMetadata;
userId?: string; userId?: string;
authToken?: string; authToken?: string;
}; }
} = {}; >();
async initiate(metadata: ClientMetadata) { async initiate(metadata: ClientMetadata) {
const clientId = randomUUID(); const clientId = randomUUID();
this.temporaryClientTable[clientId] = { this.temporaryClientTable.set(clientId, {
data: metadata, data: metadata,
timeout: setTimeout(() => { timeout: setTimeout(
if (this.temporaryClientTable[clientId]) () => {
delete this.temporaryClientTable[clientId]; if (this.temporaryClientTable.has(clientId))
}, 1000 * 60 * 10), // 10 minutes this.temporaryClientTable.delete(clientId);
}; },
1000 * 60 * 10,
), // 10 minutes
});
return clientId; return clientId;
} }
@@ -38,23 +41,23 @@ export class ClientHandler {
} }
async fetchClient(clientId: string) { async fetchClient(clientId: string) {
const entry = this.temporaryClientTable[clientId]; const entry = this.temporaryClientTable.get(clientId);
if (!entry) return undefined; if (!entry) return undefined;
return entry; return entry;
} }
async attachUserId(clientId: string, userId: string) { async attachUserId(clientId: string, userId: string) {
if (!this.temporaryClientTable[clientId]) const clientTable = this.temporaryClientTable.get(clientId);
throw new Error("Invalid clientId for attaching userId"); if (!clientTable) throw new Error("Invalid clientId for attaching userId");
this.temporaryClientTable[clientId].userId = userId; clientTable.userId = userId;
} }
async generateAuthToken(clientId: string) { async generateAuthToken(clientId: string) {
const entry = this.temporaryClientTable[clientId]; const entry = this.temporaryClientTable.get(clientId);
if (!entry) throw new Error("Invalid clientId to generate token"); if (!entry) throw new Error("Invalid clientId to generate token");
const token = randomUUID(); const token = randomUUID();
this.temporaryClientTable[clientId].authToken = token; entry.authToken = token;
return token; return token;
} }
@@ -66,7 +69,7 @@ export class ClientHandler {
} }
async finialiseClient(id: string) { async finialiseClient(id: string) {
const metadata = this.temporaryClientTable[id]; const metadata = this.temporaryClientTable.get(id);
if (!metadata) throw new Error("Invalid client ID"); if (!metadata) throw new Error("Invalid client ID");
if (!metadata.userId) throw new Error("Un-authorized client ID"); if (!metadata.userId) throw new Error("Un-authorized client ID");
+2 -3
View File
@@ -5,6 +5,5 @@ When a client signs on and registers itself as a peer
*/ */
class DownloadCoordinator { // eslint-disable-next-line @typescript-eslint/no-extraneous-class, @typescript-eslint/no-unused-vars
class DownloadCoordinator {}
}
+4 -3
View File
@@ -33,7 +33,7 @@ class ManifestGenerator {
key, key,
Object.assign({}, value, { versionName: rootManifest.versionName }), Object.assign({}, value, { versionName: rootManifest.versionName }),
]; ];
}) }),
); );
} }
@@ -71,6 +71,7 @@ class ManifestGenerator {
if (baseVersion.delta) { if (baseVersion.delta) {
// Start at the same index minus one, and keep grabbing them // Start at the same index minus one, and keep grabbing them
// until we run out or we hit something that isn't a delta // until we run out or we hit something that isn't a delta
// eslint-disable-next-line no-constant-condition
for (let i = baseVersion.versionIndex - 1; true; i--) { for (let i = baseVersion.versionIndex - 1; true; i--) {
const currentVersion = await prisma.gameVersion.findFirst({ const currentVersion = await prisma.gameVersion.findFirst({
where: { where: {
@@ -88,7 +89,7 @@ class ManifestGenerator {
const metadata: DropManifestMetadata[] = leastToMost.map((e) => { const metadata: DropManifestMetadata[] = leastToMost.map((e) => {
return { return {
manifest: JSON.parse( manifest: JSON.parse(
e.dropletManifest?.toString() ?? "{}" e.dropletManifest?.toString() ?? "{}",
) as DropManifest, ) as DropManifest,
versionName: e.versionName, versionName: e.versionName,
}; };
@@ -96,7 +97,7 @@ class ManifestGenerator {
const manifest = ManifestGenerator.generateManifestFromMetadata( const manifest = ManifestGenerator.generateManifestFromMetadata(
metadata[0], metadata[0],
...metadata.slice(1) ...metadata.slice(1),
); );
return manifest; return manifest;
+10 -11
View File
@@ -8,8 +8,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import prisma from "../db/database"; import prisma from "../db/database";
import type { GameVersion} from "@prisma/client"; import type { GameVersion } from "@prisma/client";
import { Platform } from "@prisma/client";
import { fuzzy } from "fast-fuzzy"; import { fuzzy } from "fast-fuzzy";
import { recursivelyReaddir } from "../utils/recursivedirs"; import { recursivelyReaddir } from "../utils/recursivedirs";
import taskHandler from "../tasks"; import taskHandler from "../tasks";
@@ -52,13 +51,13 @@ class LibraryManager {
async fetchUnimportedGameVersions( async fetchUnimportedGameVersions(
libraryBasePath: string, libraryBasePath: string,
versions: Array<GameVersion> versions: Array<GameVersion>,
) { ) {
const gameDir = path.join(this.basePath, libraryBasePath); const gameDir = path.join(this.basePath, libraryBasePath);
const versionsDirs = fs.readdirSync(gameDir); const versionsDirs = fs.readdirSync(gameDir);
const importedVersionDirs = versions.map((e) => e.versionName); const importedVersionDirs = versions.map((e) => e.versionName);
const unimportedVersions = versionsDirs.filter( const unimportedVersions = versionsDirs.filter(
(e) => !importedVersionDirs.includes(e) (e) => !importedVersionDirs.includes(e),
); );
return unimportedVersions; return unimportedVersions;
@@ -89,10 +88,10 @@ class LibraryManager {
noVersions: e.versions.length == 0, noVersions: e.versions.length == 0,
unimportedVersions: await this.fetchUnimportedGameVersions( unimportedVersions: await this.fetchUnimportedGameVersions(
e.libraryBasePath, e.libraryBasePath,
e.versions e.versions,
), ),
}, },
})) })),
); );
} }
@@ -113,7 +112,7 @@ class LibraryManager {
const targetDir = path.join(this.basePath, game.libraryBasePath); const targetDir = path.join(this.basePath, game.libraryBasePath);
if (!fs.existsSync(targetDir)) if (!fs.existsSync(targetDir))
throw new Error( throw new Error(
"Game in database, but no physical directory? Something is very very wrong..." "Game in database, but no physical directory? Something is very very wrong...",
); );
const versions = fs.readdirSync(targetDir); const versions = fs.readdirSync(targetDir);
const validVersions = versions.filter((versionDir) => { const validVersions = versions.filter((versionDir) => {
@@ -124,7 +123,7 @@ class LibraryManager {
const currentVersions = game.versions.map((e) => e.versionName); const currentVersions = game.versions.map((e) => e.versionName);
const unimportedVersions = validVersions.filter( const unimportedVersions = validVersions.filter(
(e) => !currentVersions.includes(e) (e) => !currentVersions.includes(e),
); );
return unimportedVersions; return unimportedVersions;
} }
@@ -138,7 +137,7 @@ class LibraryManager {
const targetDir = path.join( const targetDir = path.join(
this.basePath, this.basePath,
game.libraryBasePath, game.libraryBasePath,
versionName versionName,
); );
if (!fs.existsSync(targetDir)) return undefined; if (!fs.existsSync(targetDir)) return undefined;
@@ -217,7 +216,7 @@ class LibraryManager {
delta: boolean; delta: boolean;
umuId: string; umuId: string;
} },
) { ) {
const taskId = `import:${gameId}:${versionName}`; const taskId = `import:${gameId}:${versionName}`;
@@ -254,7 +253,7 @@ class LibraryManager {
(err, manifest) => { (err, manifest) => {
if (err) return reject(err); if (err) return reject(err);
resolve(manifest); resolve(manifest);
} },
); );
}); });
+18 -23
View File
@@ -1,10 +1,5 @@
import type { import type { Developer, Publisher } from "@prisma/client";
Developer, import { MetadataSource } from "@prisma/client";
Publisher} from "@prisma/client";
import {
MetadataSource,
PrismaClient
} from "@prisma/client";
import prisma from "../db/database"; import prisma from "../db/database";
import type { import type {
_FetchDeveloperMetadataParams, _FetchDeveloperMetadataParams,
@@ -17,11 +12,7 @@ import type {
PublisherMetadata, PublisherMetadata,
} from "./types"; } from "./types";
import { ObjectTransactionalHandler } from "../objects/transactional"; import { ObjectTransactionalHandler } from "../objects/transactional";
import { PriorityList, PriorityListIndexed } from "../utils/prioritylist"; import { PriorityListIndexed } from "../utils/prioritylist";
import { GiantBombProvider } from "./giantbomb";
import { ManualMetadataProvider } from "./manual";
import { PCGamingWikiProvider } from "./pcgamingwiki";
import { IGDBProvider } from "./igdb";
export class MissingMetadataProviderConfig extends Error { export class MissingMetadataProviderConfig extends Error {
private providerName: string; private providerName: string;
@@ -47,10 +38,10 @@ export abstract class MetadataProvider {
abstract search(query: string): Promise<GameMetadataSearchResult[]>; abstract search(query: string): Promise<GameMetadataSearchResult[]>;
abstract fetchGame(params: _FetchGameMetadataParams): Promise<GameMetadata>; abstract fetchGame(params: _FetchGameMetadataParams): Promise<GameMetadata>;
abstract fetchPublisher( abstract fetchPublisher(
params: _FetchPublisherMetadataParams params: _FetchPublisherMetadataParams,
): Promise<PublisherMetadata>; ): Promise<PublisherMetadata>;
abstract fetchDeveloper( abstract fetchDeveloper(
params: _FetchDeveloperMetadataParams params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata>; ): Promise<DeveloperMetadata>;
} }
@@ -81,6 +72,8 @@ export class MetadataHandler {
for (const provider of this.providers.values()) { for (const provider of this.providers.values()) {
const queryTransformationPromise = new Promise< const queryTransformationPromise = new Promise<
InternalGameMetadataResult[] InternalGameMetadataResult[]
// TODO: fix eslint error
// eslint-disable-next-line no-async-promise-executor
>(async (resolve, reject) => { >(async (resolve, reject) => {
try { try {
const results = await provider.search(query); const results = await provider.search(query);
@@ -89,7 +82,7 @@ export class MetadataHandler {
Object.assign({}, result, { Object.assign({}, result, {
sourceId: provider.id(), sourceId: provider.id(),
sourceName: provider.name(), sourceName: provider.name(),
}) }),
); );
resolve(mappedResults); resolve(mappedResults);
} catch (e) { } catch (e) {
@@ -119,13 +112,13 @@ export class MetadataHandler {
sourceId: "manual", sourceId: "manual",
sourceName: "Manual", sourceName: "Manual",
}, },
libraryBasePath libraryBasePath,
); );
} }
async createGame( async createGame(
result: InternalGameMetadataResult, result: InternalGameMetadataResult,
libraryBasePath: string libraryBasePath: string,
) { ) {
const provider = this.providers.get(result.sourceId); const provider = this.providers.get(result.sourceId);
if (!provider) if (!provider)
@@ -143,7 +136,7 @@ export class MetadataHandler {
const [createObject, pullObjects, dumpObjects] = this.objectHandler.new( const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(
{}, {},
["internal:read"] ["internal:read"],
); );
let metadata; let metadata;
@@ -197,7 +190,7 @@ export class MetadataHandler {
return (await this.fetchDeveloperPublisher( return (await this.fetchDeveloperPublisher(
query, query,
"fetchDeveloper", "fetchDeveloper",
"developer" "developer",
)) as Developer; )) as Developer;
} }
@@ -205,7 +198,7 @@ export class MetadataHandler {
return (await this.fetchDeveloperPublisher( return (await this.fetchDeveloperPublisher(
query, query,
"fetchPublisher", "fetchPublisher",
"publisher" "publisher",
)) as Publisher; )) as Publisher;
} }
@@ -214,8 +207,9 @@ export class MetadataHandler {
private async fetchDeveloperPublisher( private async fetchDeveloperPublisher(
query: string, query: string,
functionName: "fetchDeveloper" | "fetchPublisher", functionName: "fetchDeveloper" | "fetchPublisher",
databaseName: "developer" | "publisher" databaseName: "developer" | "publisher",
) { ) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const existing = await (prisma as any)[databaseName].findFirst({ const existing = await (prisma as any)[databaseName].findFirst({
where: { where: {
metadataOriginalQuery: query, metadataOriginalQuery: query,
@@ -229,7 +223,7 @@ export class MetadataHandler {
const [createObject, pullObjects, dumpObjects] = this.objectHandler.new( const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(
{}, {},
["internal:read"] ["internal:read"],
); );
let result: PublisherMetadata; let result: PublisherMetadata;
try { try {
@@ -243,6 +237,7 @@ export class MetadataHandler {
// If we're successful // If we're successful
await pullObjects(); await pullObjects();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = await (prisma as any)[databaseName].create({ const object = await (prisma as any)[databaseName].create({
data: { data: {
metadataSource: provider.source(), metadataSource: provider.source(),
@@ -262,7 +257,7 @@ export class MetadataHandler {
} }
throw new Error( throw new Error(
`No metadata provider found a ${databaseName} for "${query}"` `No metadata provider found a ${databaseName} for "${query}"`,
); );
} }
} }
+4 -8
View File
@@ -6,9 +6,7 @@ import type {
_FetchPublisherMetadataParams, _FetchPublisherMetadataParams,
PublisherMetadata, PublisherMetadata,
_FetchDeveloperMetadataParams, _FetchDeveloperMetadataParams,
DeveloperMetadata} from "./types"; DeveloperMetadata,
import {
GameMetadataSearchResult
} from "./types"; } from "./types";
import * as jdenticon from "jdenticon"; import * as jdenticon from "jdenticon";
@@ -22,13 +20,11 @@ export class ManualMetadataProvider implements MetadataProvider {
source() { source() {
return MetadataSource.Manual; return MetadataSource.Manual;
} }
async search(query: string) { async search(_query: string) {
return []; return [];
} }
async fetchGame({ async fetchGame({
name, name,
publisher,
developer,
createObject, createObject,
}: _FetchGameMetadataParams): Promise<GameMetadata> { }: _FetchGameMetadataParams): Promise<GameMetadata> {
const icon = jdenticon.toPng(name, 512); const icon = jdenticon.toPng(name, 512);
@@ -52,12 +48,12 @@ export class ManualMetadataProvider implements MetadataProvider {
}; };
} }
async fetchPublisher( async fetchPublisher(
params: _FetchPublisherMetadataParams _params: _FetchPublisherMetadataParams,
): Promise<PublisherMetadata> { ): Promise<PublisherMetadata> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
async fetchDeveloper( async fetchDeveloper(
params: _FetchDeveloperMetadataParams _params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata> { ): Promise<DeveloperMetadata> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
+6 -9
View File
@@ -1,7 +1,6 @@
import type { Developer, Publisher } from "@prisma/client"; import type { Developer, Publisher } from "@prisma/client";
import { MetadataSource } from "@prisma/client"; import { MetadataSource } from "@prisma/client";
import type { MetadataProvider} from "."; import type { MetadataProvider } from ".";
import { MissingMetadataProviderConfig } from ".";
import type { import type {
GameMetadataSearchResult, GameMetadataSearchResult,
_FetchGameMetadataParams, _FetchGameMetadataParams,
@@ -48,7 +47,7 @@ interface PCGamingWikiCargoResult<T> {
cargoquery: [ cargoquery: [
{ {
title: T; title: T;
} },
]; ];
error?: { error?: {
code?: string; code?: string;
@@ -61,8 +60,6 @@ interface PCGamingWikiCargoResult<T> {
// Api Docs: https://www.pcgamingwiki.com/wiki/PCGamingWiki:API // Api Docs: https://www.pcgamingwiki.com/wiki/PCGamingWiki:API
// Good tool for helping build cargo queries: https://www.pcgamingwiki.com/wiki/Special:CargoQuery // Good tool for helping build cargo queries: https://www.pcgamingwiki.com/wiki/Special:CargoQuery
export class PCGamingWikiProvider implements MetadataProvider { export class PCGamingWikiProvider implements MetadataProvider {
constructor() {}
id() { id() {
return "pcgamingwiki"; return "pcgamingwiki";
} }
@@ -75,7 +72,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
private async request<T>( private async request<T>(
query: URLSearchParams, query: URLSearchParams,
options?: AxiosRequestConfig options?: AxiosRequestConfig,
) { ) {
const finalURL = `https://www.pcgamingwiki.com/w/api.php?${query.toString()}`; const finalURL = `https://www.pcgamingwiki.com/w/api.php?${query.toString()}`;
@@ -84,12 +81,12 @@ export class PCGamingWikiProvider implements MetadataProvider {
baseURL: "", baseURL: "",
}; };
const response = await axios.request<PCGamingWikiCargoResult<T>>( const response = await axios.request<PCGamingWikiCargoResult<T>>(
Object.assign({}, options, overlay) Object.assign({}, options, overlay),
); );
if (response.status !== 200) if (response.status !== 200)
throw new Error( throw new Error(
`Error in pcgamingwiki \nStatus Code: ${response.status}` `Error in pcgamingwiki \nStatus Code: ${response.status}`,
); );
else if (response.data.error !== undefined) else if (response.data.error !== undefined)
throw new Error(`Error in pcgamingwiki, malformed query`); throw new Error(`Error in pcgamingwiki, malformed query`);
@@ -256,7 +253,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
} }
async fetchDeveloper( async fetchDeveloper(
params: _FetchDeveloperMetadataParams params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata> { ): Promise<DeveloperMetadata> {
return await this.fetchPublisher(params); return await this.fetchPublisher(params);
} }
-1
View File
@@ -1,6 +1,5 @@
import type { Developer, Publisher } from "@prisma/client"; import type { Developer, Publisher } from "@prisma/client";
import type { TransactionDataType } from "../objects/transactional"; import type { TransactionDataType } from "../objects/transactional";
import { ObjectTransactionalHandler } from "../objects/transactional";
import type { ObjectReference } from "../objects/objectHandler"; import type { ObjectReference } from "../objects/objectHandler";
export interface GameMetadataSearchResult { export interface GameMetadataSearchResult {
+2 -3
View File
@@ -1,4 +1,3 @@
import { triggerAsyncId } from "async_hooks";
import prisma from "../db/database"; import prisma from "../db/database";
import objectHandler from "../objects"; import objectHandler from "../objects";
@@ -50,7 +49,7 @@ class NewsManager {
orderBy?: "asc" | "desc"; orderBy?: "asc" | "desc";
tags?: string[]; tags?: string[];
search?: string; search?: string;
} = {} } = {},
) { ) {
return await prisma.article.findMany({ return await prisma.article.findMany({
where: { where: {
@@ -116,7 +115,7 @@ class NewsManager {
content?: string; content?: string;
excerpt?: string; excerpt?: string;
image?: string; image?: string;
} },
) { ) {
return await prisma.article.update({ return await prisma.article.update({
where: { id }, where: { id },
+10 -9
View File
@@ -15,27 +15,28 @@ export type NotificationCreateArgs = Pick<
>; >;
class NotificationSystem { class NotificationSystem {
private listeners: { private listeners = new Map<
[key: string]: Map<string, (notification: Notification) => any>; string,
} = {}; Map<string, (notification: Notification) => void>
>();
listen( listen(
userId: string, userId: string,
id: string, id: string,
callback: (notification: Notification) => any callback: (notification: Notification) => void,
) { ) {
this.listeners[userId] ??= new Map(); this.listeners.set(userId, new Map());
this.listeners[userId].set(id, callback); this.listeners.get(userId)?.set(id, callback);
this.catchupListener(userId, id); this.catchupListener(userId, id);
} }
unlisten(userId: string, id: string) { unlisten(userId: string, id: string) {
this.listeners[userId].delete(id); this.listeners.get(userId)?.delete(id);
} }
private async catchupListener(userId: string, id: string) { private async catchupListener(userId: string, id: string) {
const callback = this.listeners[userId].get(id); const callback = this.listeners.get(userId)?.get(id);
if (!callback) if (!callback)
throw new Error("Failed to catch-up listener: callback does not exist"); throw new Error("Failed to catch-up listener: callback does not exist");
const notifications = await prisma.notification.findMany({ const notifications = await prisma.notification.findMany({
@@ -50,7 +51,7 @@ class NotificationSystem {
} }
private async pushNotification(userId: string, notification: Notification) { private async pushNotification(userId: string, notification: Notification) {
for (const listener of this.listeners[userId] ?? []) { for (const listener of this.listeners.get(userId) ?? []) {
await listener[1](notification); await listener[1](notification);
} }
} }
+8 -8
View File
@@ -4,7 +4,7 @@ import { ObjectBackend } from "./objectHandler";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { Readable, Stream } from "stream"; import { Readable } from "stream";
import { createHash } from "crypto"; import { createHash } from "crypto";
import prisma from "../db/database"; import prisma from "../db/database";
@@ -40,7 +40,7 @@ export class FsObjectBackend extends ObjectBackend {
if (source instanceof Readable) { if (source instanceof Readable) {
const outputStream = fs.createWriteStream(objectPath); const outputStream = fs.createWriteStream(objectPath);
source.pipe(outputStream, { end: true }); source.pipe(outputStream, { end: true });
await new Promise((r, j) => source.on("end", r)); await new Promise((r, _j) => source.on("end", r));
return true; return true;
} }
@@ -61,7 +61,7 @@ export class FsObjectBackend extends ObjectBackend {
async create( async create(
id: string, id: string,
source: Source, source: Source,
metadata: ObjectMetadata metadata: ObjectMetadata,
): Promise<ObjectReference | undefined> { ): Promise<ObjectReference | undefined> {
const objectPath = path.join(this.baseObjectPath, id); const objectPath = path.join(this.baseObjectPath, id);
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`); const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
@@ -104,7 +104,7 @@ export class FsObjectBackend extends ObjectBackend {
return true; return true;
} }
async fetchMetadata( async fetchMetadata(
id: ObjectReference id: ObjectReference,
): Promise<ObjectMetadata | undefined> { ): Promise<ObjectMetadata | undefined> {
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`); const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
if (!fs.existsSync(metadataPath)) return undefined; if (!fs.existsSync(metadataPath)) return undefined;
@@ -113,7 +113,7 @@ export class FsObjectBackend extends ObjectBackend {
} }
async writeMetadata( async writeMetadata(
id: ObjectReference, id: ObjectReference,
metadata: ObjectMetadata metadata: ObjectMetadata,
): Promise<boolean> { ): Promise<boolean> {
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`); const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
if (!fs.existsSync(metadataPath)) return false; if (!fs.existsSync(metadataPath)) return false;
@@ -153,8 +153,6 @@ class FsHashStore {
max: 1000, // number of items max: 1000, // number of items
}); });
constructor() {}
/** /**
* Gets hash of object * Gets hash of object
* @param id * @param id
@@ -211,6 +209,8 @@ class FsHashStore {
id, id,
}, },
}); });
} catch {} } catch {
/* empty */
}
} }
} }
+8 -8
View File
@@ -16,7 +16,7 @@
import { parse as getMimeTypeBuffer } from "file-type-mime"; import { parse as getMimeTypeBuffer } from "file-type-mime";
import type { Writable } from "stream"; import type { Writable } from "stream";
import Stream, { Readable } from "stream"; import { Readable } from "stream";
import { getMimeType as getMimeTypeStream } from "stream-mime-type"; import { getMimeType as getMimeTypeStream } from "stream-mime-type";
export type ObjectReference = string; export type ObjectReference = string;
@@ -50,19 +50,19 @@ export abstract class ObjectBackend {
abstract create( abstract create(
id: string, id: string,
source: Source, source: Source,
metadata: ObjectMetadata metadata: ObjectMetadata,
): Promise<ObjectReference | undefined>; ): Promise<ObjectReference | undefined>;
abstract createWithWriteStream( abstract createWithWriteStream(
id: string, id: string,
metadata: ObjectMetadata metadata: ObjectMetadata,
): Promise<Writable | undefined>; ): Promise<Writable | undefined>;
abstract delete(id: ObjectReference): Promise<boolean>; abstract delete(id: ObjectReference): Promise<boolean>;
abstract fetchMetadata( abstract fetchMetadata(
id: ObjectReference id: ObjectReference,
): Promise<ObjectMetadata | undefined>; ): Promise<ObjectMetadata | undefined>;
abstract writeMetadata( abstract writeMetadata(
id: ObjectReference, id: ObjectReference,
metadata: ObjectMetadata metadata: ObjectMetadata,
): Promise<boolean>; ): Promise<boolean>;
abstract fetchHash(id: ObjectReference): Promise<string | undefined>; abstract fetchHash(id: ObjectReference): Promise<string | undefined>;
} }
@@ -96,7 +96,7 @@ export class ObjectHandler {
id: string, id: string,
sourceFetcher: () => Promise<Source>, sourceFetcher: () => Promise<Source>,
metadata: { [key: string]: string }, metadata: { [key: string]: string },
permissions: Array<string> permissions: Array<string>,
) { ) {
const { source, mime } = await this.fetchMimeType(await sourceFetcher()); const { source, mime } = await this.fetchMimeType(await sourceFetcher());
if (!mime) if (!mime)
@@ -112,7 +112,7 @@ export class ObjectHandler {
async createWithStream( async createWithStream(
id: string, id: string,
metadata: { [key: string]: string }, metadata: { [key: string]: string },
permissions: Array<string> permissions: Array<string>,
) { ) {
return this.backend.createWithWriteStream(id, { return this.backend.createWithWriteStream(id, {
permissions, permissions,
@@ -194,7 +194,7 @@ export class ObjectHandler {
async writeWithPermissions( async writeWithPermissions(
id: ObjectReference, id: ObjectReference,
sourceFetcher: () => Promise<Source>, sourceFetcher: () => Promise<Source>,
userId?: string userId?: string,
) { ) {
const metadata = await this.backend.fetchMetadata(id); const metadata = await this.backend.fetchMetadata(id);
if (!metadata) return false; if (!metadata) return false;
+1 -1
View File
@@ -1,6 +1,6 @@
import prisma from "../internal/db/database"; import prisma from "../internal/db/database";
export default defineNitroPlugin(async () => { export default defineNitroPlugin(async (_nitro) => {
// Ensure system user exists // Ensure system user exists
// The system user owns any user-based code // The system user owns any user-based code
// that we want to re-use for the app // that we want to re-use for the app
+1 -1
View File
@@ -1,6 +1,6 @@
import prisma from "../internal/db/database"; import prisma from "../internal/db/database";
export default defineNitroPlugin(async () => { export default defineNitroPlugin(async (_nitro) => {
const userCount = await prisma.user.count({ const userCount = await prisma.user.count({
where: { id: { not: "system" } }, where: { id: { not: "system" } },
}); });
+1 -1
View File
@@ -6,7 +6,7 @@ import { IGDBProvider } from "../internal/metadata/igdb";
import { ManualMetadataProvider } from "../internal/metadata/manual"; import { ManualMetadataProvider } from "../internal/metadata/manual";
import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki"; import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki";
export default defineNitroPlugin(async () => { export default defineNitroPlugin(async (_nitro) => {
const metadataProviders = [ const metadataProviders = [
GiantBombProvider, GiantBombProvider,
PCGamingWikiProvider, PCGamingWikiProvider,