mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 04:11:32 +10:00
fix: more eslint stuff
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
+505
-486
File diff suppressed because it is too large
Load Diff
+139
-63
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 →
|
Edit →
|
||||||
</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) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<template></template>
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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 }>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 +1 @@
|
|||||||
export default defineEventHandler((h3) => {
|
export default defineEventHandler((_h3) => {});
|
||||||
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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,4 +1,4 @@
|
|||||||
export default defineEventHandler((h3) => {
|
export default defineEventHandler((_h3) => {
|
||||||
return {
|
return {
|
||||||
appName: "Drop",
|
appName: "Drop",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-1
@@ -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 {
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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,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" } },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user