mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
fix: more eslint stuff
This commit is contained in:
@ -195,7 +195,7 @@
|
||||
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"
|
||||
placeholder="--setup"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -351,7 +351,7 @@
|
||||
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"
|
||||
placeholder="--launch"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -461,7 +461,7 @@
|
||||
:disabled="!umuIdEnabled"
|
||||
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"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -536,7 +536,6 @@ import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxLabel,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
@ -553,7 +552,7 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
const gameId = route.params.id.toString();
|
||||
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 versionSettings = ref<{
|
||||
@ -585,13 +584,13 @@ const setupProcessQuery = ref("");
|
||||
|
||||
const launchFilteredVersionGuesses = computed(() =>
|
||||
versionGuesses.value?.filter((e) =>
|
||||
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase())
|
||||
)
|
||||
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()),
|
||||
),
|
||||
);
|
||||
const setupFilteredVersionGuesses = computed(() =>
|
||||
versionGuesses.value?.filter((e) =>
|
||||
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase())
|
||||
)
|
||||
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
function updateLaunchCommand(value: string) {
|
||||
@ -608,7 +607,7 @@ function autosetPlatform(value: string) {
|
||||
if (!versionGuesses.value) return;
|
||||
if (versionSettings.value.platform) return;
|
||||
const guessIndex = versionGuesses.value.findIndex(
|
||||
(e) => e.filename === value
|
||||
(e) => e.filename === value,
|
||||
);
|
||||
if (guessIndex == -1) return;
|
||||
versionSettings.value.platform = versionGuesses.value[guessIndex].platform;
|
||||
@ -636,8 +635,8 @@ async function updateCurrentlySelectedVersion(value: number) {
|
||||
const version = versions[currentlySelectedVersion.value];
|
||||
const results = await $dropFetch(
|
||||
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
||||
gameId
|
||||
)}&version=${encodeURIComponent(version)}`
|
||||
gameId,
|
||||
)}&version=${encodeURIComponent(version)}`,
|
||||
);
|
||||
versionGuesses.value = results.map((e) => ({
|
||||
...e,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,45 +1,69 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-6 w-full max-w-md">
|
||||
<Listbox
|
||||
as="div" :model="currentlySelectedGame"
|
||||
@update:model-value="(value) => updateSelectedGame_wrapper(value)">
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game to import</ListboxLabel>
|
||||
as="div"
|
||||
:model="currentlySelectedGame"
|
||||
@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">
|
||||
<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">{{
|
||||
games.unimportedGames[currentlySelectedGame]
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-zinc-400">Please select a directory...</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 v-else class="block truncate text-zinc-400"
|
||||
>Please select a directory...</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>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<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
|
||||
v-for="(game, gameIdx) in games.unimportedGames" :key="game" v-slot="{ active, selected }" as="template"
|
||||
:value="gameIdx">
|
||||
v-for="(game, gameIdx) in games.unimportedGames"
|
||||
:key="game"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
:value="gameIdx"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]">
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]">{{ game }}</span>
|
||||
:class="[
|
||||
selected ? 'font-semibold' : 'font-normal',
|
||||
'block truncate',
|
||||
]"
|
||||
>{{ game }}</span
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="selected" :class="[
|
||||
active ? 'text-white' : 'text-blue-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
]">
|
||||
v-if="selected"
|
||||
:class="[
|
||||
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" />
|
||||
</span>
|
||||
</li>
|
||||
@ -52,47 +76,76 @@ v-if="selected" :class="[
|
||||
<div v-if="currentlySelectedGame !== -1" class="flex flex-col gap-y-4">
|
||||
<!-- without metadata option -->
|
||||
<div>
|
||||
<LoadingButton class="w-fit" :loading="importLoading" @click="() => importGame_wrapper(false)">Import without
|
||||
metadata
|
||||
<LoadingButton
|
||||
class="w-fit"
|
||||
:loading="importLoading"
|
||||
@click="() => importGame_wrapper(false)"
|
||||
>Import without metadata
|
||||
</LoadingButton>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- divider -->
|
||||
<div 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="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>
|
||||
|
||||
<!-- with metadata option -->
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<Listbox 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>
|
||||
<Listbox
|
||||
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">
|
||||
<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
|
||||
v-if="currentlySelectedMetadata != -1"
|
||||
: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">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
v-if="currentlySelectedMetadata != -1"
|
||||
: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"
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
class="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<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
|
||||
v-for="(result, resultIdx) in metadataResults" :key="result.id" v-slot="{ active, selected }"
|
||||
as="template" :value="resultIdx">
|
||||
v-for="(result, resultIdx) in metadataResults"
|
||||
:key="result.id"
|
||||
v-slot="{ active }"
|
||||
as="template"
|
||||
:value="resultIdx"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]">
|
||||
:class="[
|
||||
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||
]"
|
||||
>
|
||||
<GameSearchResultWidget :game="result" />
|
||||
</li>
|
||||
</ListboxOption>
|
||||
@ -101,23 +154,34 @@ v-for="(result, resultIdx) in metadataResults" :key="result.id" v-slot="{ active
|
||||
</div>
|
||||
</Listbox>
|
||||
<div
|
||||
v-else-if="gameSearchResultsLoading" role="status"
|
||||
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4">
|
||||
v-else-if="gameSearchResultsLoading"
|
||||
role="status"
|
||||
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
|
||||
>
|
||||
Loading game results...
|
||||
<svg
|
||||
aria-hidden="true" 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">
|
||||
aria-hidden="true"
|
||||
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
|
||||
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
|
||||
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>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</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-shrink-0">
|
||||
<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>
|
||||
<LoadingButton
|
||||
class="w-fit" :loading="importLoading" :disabled="currentlySelectedMetadata === -1"
|
||||
@click="() => importGame_wrapper()">Import
|
||||
class="w-fit"
|
||||
:loading="importLoading"
|
||||
:disabled="currentlySelectedMetadata === -1"
|
||||
@click="() => importGame_wrapper()"
|
||||
>Import
|
||||
</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-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
@ -187,18 +257,21 @@ async function updateSelectedGame(value: number) {
|
||||
currentlySelectedMetadata.value = -1;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function updateSelectedGame_wrapper(value: number) {
|
||||
gameSearchResultsLoading.value = true;
|
||||
updateSelectedGame(value).catch((error) => {
|
||||
gameSearchResultsError.value = error.statusMessage || "An unknown error occurred";
|
||||
}).finally(() => {
|
||||
gameSearchResultsLoading.value = false;
|
||||
})
|
||||
updateSelectedGame(value)
|
||||
.catch((error) => {
|
||||
gameSearchResultsError.value =
|
||||
error.statusMessage || "An unknown error occurred";
|
||||
})
|
||||
.finally(() => {
|
||||
gameSearchResultsLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
const metadataResults = ref<Array<GameMetadataSearchResult> | undefined>();
|
||||
@ -215,7 +288,10 @@ async function importGame(metadata: boolean) {
|
||||
method: "POST",
|
||||
body: {
|
||||
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"
|
||||
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..."
|
||||
>
|
||||
/>
|
||||
<MagnifyingGlassIcon
|
||||
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"
|
||||
@ -69,7 +69,7 @@
|
||||
class="h-16 w-16 flex-shrink-0 rounded-md"
|
||||
:src="useObject(game.mIconId)"
|
||||
alt=""
|
||||
>
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<h3 class="text-sm font-medium text-zinc-100 font-display">
|
||||
{{ game.mName }}
|
||||
@ -87,17 +87,17 @@
|
||||
</dl>
|
||||
<div class="inline-flex gap-x-2 items-center">
|
||||
<NuxtLink
|
||||
: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"
|
||||
>
|
||||
Edit →
|
||||
</NuxtLink>
|
||||
<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"
|
||||
@click="() => deleteGame(game.id)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
: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"
|
||||
>
|
||||
Edit →
|
||||
</NuxtLink>
|
||||
<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"
|
||||
@click="() => deleteGame(game.id)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -193,11 +193,12 @@ const libraryGames = ref(
|
||||
},
|
||||
hasNotifications: noVersions || toImport,
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const filteredLibraryGames = computed(() =>
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore excessively deep ts
|
||||
libraryGames.value.filter((e) => {
|
||||
if (!searchQuery.value) return true;
|
||||
const searchQueryLower = searchQuery.value.toLowerCase();
|
||||
@ -205,7 +206,7 @@ const filteredLibraryGames = computed(() =>
|
||||
if (e.mShortDescription.toLowerCase().includes(searchQueryLower))
|
||||
return true;
|
||||
return false;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
async function deleteGame(id: string) {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
<template></template>
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 taskId = route.params.id.toString();
|
||||
|
||||
@ -164,7 +164,7 @@
|
||||
autocomplete="username"
|
||||
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"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -191,7 +191,7 @@
|
||||
autocomplete="email"
|
||||
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"
|
||||
>
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<ListboxOption
|
||||
v-for="[label, _] in Object.entries(expiry)"
|
||||
v-for="[label] in Object.entries(expiry)"
|
||||
:key="label"
|
||||
v-slot="{ active, selected }"
|
||||
as="template"
|
||||
@ -354,7 +354,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
@ -368,16 +367,8 @@ import {
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import {
|
||||
CalendarDateRangeIcon,
|
||||
TrashIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/vue/24/solid";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { TrashIcon, XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Invitation } from "@prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { DurationLike } from "luxon";
|
||||
@ -392,7 +383,7 @@ useHead({
|
||||
});
|
||||
|
||||
const data = await $dropFetch<Array<SerializeObject<Invitation>>>(
|
||||
"/api/v1/admin/auth/invitation"
|
||||
"/api/v1/admin/auth/invitation",
|
||||
);
|
||||
const invitations = ref(data ?? []);
|
||||
|
||||
@ -401,7 +392,7 @@ const generateInvitationUrl = (id: string) =>
|
||||
const invitationUrls = ref<undefined | Array<string>>();
|
||||
onMounted(() => {
|
||||
invitationUrls.value = invitations.value.map((invitation) =>
|
||||
generateInvitationUrl(invitation.id)
|
||||
generateInvitationUrl(invitation.id),
|
||||
);
|
||||
});
|
||||
|
||||
@ -417,7 +408,7 @@ const username = computed({
|
||||
},
|
||||
});
|
||||
const validUsername = computed(() =>
|
||||
_username.value === undefined ? true : _username.value.length >= 5
|
||||
_username.value === undefined ? true : _username.value.length >= 5,
|
||||
);
|
||||
|
||||
// Same as above
|
||||
@ -433,7 +424,7 @@ const email = computed({
|
||||
});
|
||||
const mailRegex = /^\S+@\S+\.\S+$/;
|
||||
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);
|
||||
@ -459,7 +450,7 @@ const expiry: Record<string, DurationLike> = {
|
||||
year: 3000,
|
||||
}, // 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 error = ref<undefined | string>();
|
||||
@ -481,7 +472,7 @@ async function invite() {
|
||||
email.value = "";
|
||||
username.value = "";
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: "Home",
|
||||
title: "Home",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
router.replace('/store')
|
||||
router.replace("/store");
|
||||
</script>
|
||||
|
||||
@ -97,16 +97,7 @@ import {
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from "@headlessui/vue";
|
||||
import {
|
||||
Bars3Icon,
|
||||
CalendarIcon,
|
||||
ChartPieIcon,
|
||||
DocumentDuplicateIcon,
|
||||
FolderIcon,
|
||||
HomeIcon,
|
||||
UsersIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const router = useRouter();
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
@ -33,12 +33,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftIcon, TrashIcon } from "@heroicons/vue/20/solid";
|
||||
import { ArrowLeftIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
const route = useRoute();
|
||||
const collections = await useCollections();
|
||||
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) {
|
||||
throw createError({ statusCode: 404, statusMessage: "Collection not found" });
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
|
||||
@ -7,7 +8,7 @@
|
||||
<img
|
||||
:src="useObject(game.mBannerId)"
|
||||
class="w-full h-[24rem] object-cover blur-sm scale-105"
|
||||
>
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-zinc-900 to-transparent opacity-90"
|
||||
/>
|
||||
@ -76,7 +77,7 @@
|
||||
<img
|
||||
class="w-fit h-48 lg:h-96 rounded"
|
||||
:src="useObject(image)"
|
||||
>
|
||||
/>
|
||||
</VueSlide>
|
||||
<VueSlide v-if="game.mImageCarousel.length == 0">
|
||||
<div
|
||||
@ -112,13 +113,9 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
PhotoIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowUpRightIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
|
||||
import { micromark } from "micromark";
|
||||
import type { Game } from "@prisma/client";
|
||||
|
||||
@ -135,23 +132,23 @@ const game = computed(() => {
|
||||
|
||||
// Convert markdown to HTML
|
||||
const descriptionHTML = computed(() =>
|
||||
micromark(game.value.mDescription ?? "")
|
||||
micromark(game.value.mDescription ?? ""),
|
||||
);
|
||||
|
||||
const currentImageIndex = ref(0);
|
||||
// const currentImageIndex = ref(0);
|
||||
|
||||
function nextImage() {
|
||||
if (!game.value?.mImageCarousel) return;
|
||||
currentImageIndex.value =
|
||||
(currentImageIndex.value + 1) % game.value.mImageCarousel.length;
|
||||
}
|
||||
// function nextImage() {
|
||||
// if (!game.value?.mImageCarousel) return;
|
||||
// currentImageIndex.value =
|
||||
// (currentImageIndex.value + 1) % game.value.mImageCarousel.length;
|
||||
// }
|
||||
|
||||
function previousImage() {
|
||||
if (!game.value?.mImageCarousel) return;
|
||||
currentImageIndex.value =
|
||||
(currentImageIndex.value - 1 + game.value.mImageCarousel.length) %
|
||||
game.value.mImageCarousel.length;
|
||||
}
|
||||
// function previousImage() {
|
||||
// if (!game.value?.mImageCarousel) return;
|
||||
// currentImageIndex.value =
|
||||
// (currentImageIndex.value - 1 + game.value.mImageCarousel.length) %
|
||||
// game.value.mImageCarousel.length;
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,101 +1,98 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-8">
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2>
|
||||
<p class="mt-2 text-zinc-400">
|
||||
Organize your games into collections for easy access, and access all
|
||||
your games.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-col gap-y-8">
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2>
|
||||
<p class="mt-2 text-zinc-400">
|
||||
Organize your games into collections for easy access, and access all
|
||||
your games.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Collections grid -->
|
||||
<TransitionGroup
|
||||
name="collection-list"
|
||||
tag="div"
|
||||
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"
|
||||
<!-- Collections grid -->
|
||||
<TransitionGroup
|
||||
name="collection-list"
|
||||
tag="div"
|
||||
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
<NuxtLink
|
||||
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 focus:bg-zinc-800 focus:outline-none"
|
||||
:href="`/library/collection/${collection.id}`"
|
||||
<!-- 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"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-zinc-100">
|
||||
{{ collection.name }}
|
||||
</h3>
|
||||
<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"
|
||||
<NuxtLink
|
||||
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 focus:bg-zinc-800 focus:outline-none"
|
||||
:href="`/library/collection/${collection.id}`"
|
||||
>
|
||||
<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
|
||||
<h3 class="text-lg font-semibold text-zinc-100">
|
||||
{{ collection.name }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
{{ collection.entries.length }} game(s)
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- 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}`"
|
||||
/>
|
||||
<!-- 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">
|
||||
<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>
|
||||
|
||||
<CreateCollectionModal v-model="collectionCreateOpen" />
|
||||
<DeleteCollectionModal v-model="currentlyDeleting" />
|
||||
<CreateCollectionModal v-model="collectionCreateOpen" />
|
||||
<DeleteCollectionModal v-model="currentlyDeleting" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowUpRightIcon,
|
||||
TrashIcon,
|
||||
ArrowLeftIcon, PlusIcon
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import type { Collection, Game, GameVersion } from "@prisma/client";
|
||||
import { TrashIcon, PlusIcon } from "@heroicons/vue/20/solid";
|
||||
import type { Collection } from "@prisma/client";
|
||||
|
||||
const collections = await useCollections();
|
||||
const collectionCreateOpen = ref(false);
|
||||
|
||||
@ -97,22 +97,13 @@ import {
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from "@headlessui/vue";
|
||||
import {
|
||||
Bars3Icon,
|
||||
CalendarIcon,
|
||||
ChartPieIcon,
|
||||
DocumentDuplicateIcon,
|
||||
FolderIcon,
|
||||
HomeIcon,
|
||||
UsersIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const news = useNews();
|
||||
|
||||
if (!news.value) {
|
||||
await fetchNews();
|
||||
console.log('fetched news')
|
||||
console.log("fetched news");
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -1,79 +1,85 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Banner header with blurred background -->
|
||||
<div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden">
|
||||
<div v-if="article.image" class="absolute inset-0">
|
||||
<img
|
||||
:src="useObject(article.image)"
|
||||
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 v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Banner header with blurred background -->
|
||||
<div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden">
|
||||
<div v-if="article.image" class="absolute inset-0">
|
||||
<img
|
||||
:src="useObject(article.image)"
|
||||
alt=""
|
||||
class="w-full h-full object-cover blur-sm scale-110"
|
||||
/>
|
||||
<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>
|
||||
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
|
||||
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>
|
||||
<p class="mt-4 text-lg text-zinc-300">{{ article.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article content - markdown -->
|
||||
<div
|
||||
class="mx-auto prose prose-invert prose-lg"
|
||||
v-html="renderedContent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Article content - markdown -->
|
||||
<div class="mx-auto prose prose-invert prose-lg" v-html="renderedContent" />
|
||||
<DeleteNewsModal v-model="currentlyDeleting" />
|
||||
</div>
|
||||
|
||||
<DeleteNewsModal v-model="currentlyDeleting" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -88,7 +94,9 @@ const news = useNews();
|
||||
if (!news.value) {
|
||||
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)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
:src="useObject(article.image)"
|
||||
alt=""
|
||||
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">
|
||||
<span
|
||||
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 { SerializeObject } from "nitropack/types";
|
||||
|
||||
const props = defineProps<{
|
||||
const { articles } = defineProps<{
|
||||
articles: SerializeObject<
|
||||
Article & {
|
||||
tags: Array<{ name: string; id: string }>;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div
|
||||
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 (v != "\n") return false;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const previewDescription = gameDescriptionCharacters
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
:src="useObject(game.mBannerId)"
|
||||
alt=""
|
||||
class="size-full object-cover object-center"
|
||||
>
|
||||
/>
|
||||
</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"
|
||||
@ -99,8 +99,8 @@ const recent = await $dropFetch("/api/v1/store/recent");
|
||||
const updated = await $dropFetch("/api/v1/store/updated");
|
||||
const released = await $dropFetch("/api/v1/store/released");
|
||||
|
||||
const developers = await $dropFetch("/api/v1/store/developers");
|
||||
const publishers = await $dropFetch("/api/v1/store/publishers");
|
||||
// const developers = await $dropFetch("/api/v1/store/developers");
|
||||
// const publishers = await $dropFetch("/api/v1/store/publishers");
|
||||
|
||||
useHead({
|
||||
title: "Store",
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import libraryManager from "~/server/internal/library";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, [
|
||||
"game:delete",
|
||||
]);
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]);
|
||||
if (!allowed) throw createError({ statusCode: 403 });
|
||||
|
||||
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 newsManager from "~/server/internal/news";
|
||||
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
|
||||
@ -21,7 +21,7 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Failed to upload file",
|
||||
});
|
||||
|
||||
const [imageId, options, pull, dump] = uploadResult;
|
||||
const [imageId, options, pull, _dump] = uploadResult;
|
||||
|
||||
const title = options.title;
|
||||
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 { createHashArgon2 } from "~/server/internal/security/simple";
|
||||
import * as jdenticon from "jdenticon";
|
||||
@ -63,7 +63,7 @@ export default defineEventHandler(async (h3) => {
|
||||
profilePictureId,
|
||||
async () => jdenticon.toPng(user.username, 256),
|
||||
{},
|
||||
[`internal:read`, `${userId}:read`]
|
||||
[`internal:read`, `${userId}:read`],
|
||||
);
|
||||
const [linkMec] = await prisma.$transaction([
|
||||
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 fs from "fs";
|
||||
import path from "path";
|
||||
@ -33,7 +32,7 @@ export default defineEventHandler(async (h3) => {
|
||||
const versionDir = path.join(
|
||||
libraryManager.fetchLibraryPath(),
|
||||
game.libraryBasePath,
|
||||
versionName
|
||||
versionName,
|
||||
);
|
||||
if (!fs.existsSync(versionDir))
|
||||
throw createError({
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
@ -20,7 +19,7 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
|
||||
const successful = await userLibraryManager.collectionRemove(
|
||||
gameId,
|
||||
id,
|
||||
user.id
|
||||
user.id,
|
||||
);
|
||||
if (!successful)
|
||||
throw createError({
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
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 userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
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 userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
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 userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
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 userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
@ -12,6 +11,9 @@ export default defineClientEventHandler(async (h3, { fetchUser }) => {
|
||||
throw createError({ statusCode: 400, statusMessage: "Requires name" });
|
||||
|
||||
// Create the collection using the manager
|
||||
const newCollection = await userLibraryManager.collectionCreate(name, user.id);
|
||||
const newCollection = await userLibraryManager.collectionCreate(
|
||||
name,
|
||||
user.id,
|
||||
);
|
||||
return newCollection;
|
||||
});
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
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 id = query.id?.toString();
|
||||
if (!id)
|
||||
@ -25,7 +24,7 @@ export default defineClientEventHandler(async (h3, {}) => {
|
||||
if (!version.dropletManifest) return undefined;
|
||||
|
||||
const newVersion = { ...version, dropletManifest: undefined };
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error idk why we delete an undefined object
|
||||
delete newVersion.dropletManifest;
|
||||
return {
|
||||
...newVersion,
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
import { ClientCapabilities } from "@prisma/client";
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import { applicationSettings } from "~/server/internal/config/application-configuration";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineClientEventHandler(
|
||||
async (h3, { fetchClient, fetchUser }) => {
|
||||
const client = await fetchClient();
|
||||
if (!client.capabilities.includes(ClientCapabilities.CloudSaves))
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Capability not allowed.",
|
||||
});
|
||||
export default defineClientEventHandler(async (_h3, { fetchClient }) => {
|
||||
const client = await fetchClient();
|
||||
if (!client.capabilities.includes(ClientCapabilities.CloudSaves))
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Capability not allowed.",
|
||||
});
|
||||
|
||||
const slotLimit = await applicationSettings.get("saveSlotCountLimit");
|
||||
const sizeLimit = await applicationSettings.get("saveSlotSizeLimit");
|
||||
const history = await applicationSettings.get("saveSlotHistoryLimit");
|
||||
return { slotLimit, sizeLimit, history };
|
||||
}
|
||||
);
|
||||
const slotLimit = await applicationSettings.get("saveSlotCountLimit");
|
||||
const sizeLimit = await applicationSettings.get("saveSlotSizeLimit");
|
||||
const history = await applicationSettings.get("saveSlotHistoryLimit");
|
||||
return { slotLimit, sizeLimit, history };
|
||||
});
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import userLibraryManager from "~/server/internal/userlibrary";
|
||||
|
||||
export default defineClientEventHandler(async (h3, { fetchUser }) => {
|
||||
export default defineClientEventHandler(async (_h3, { fetchUser }) => {
|
||||
const user = await fetchUser();
|
||||
const library = await userLibraryManager.fetchLibrary(user.id);
|
||||
return library.entries.map((e) => e.game);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export default defineEventHandler((h3) => {
|
||||
export default defineEventHandler((_h3) => {
|
||||
return {
|
||||
appName: "Drop",
|
||||
};
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
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";
|
||||
|
||||
// TODO add web socket sessions for horizontal scaling
|
||||
// Peer ID to user ID
|
||||
const socketSessions: { [key: string]: string } = {};
|
||||
const socketSessions = new Map<string, string>();
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
async open(peer) {
|
||||
@ -25,7 +23,7 @@ export default defineWebSocketHandler({
|
||||
userIds.push("system");
|
||||
}
|
||||
|
||||
socketSessions[peer.id] = userId;
|
||||
socketSessions.set(peer.id, userId);
|
||||
|
||||
for (const listenUserId of userIds) {
|
||||
notificationSystem.listen(listenUserId, peer.id, (notification) => {
|
||||
@ -33,8 +31,8 @@ export default defineWebSocketHandler({
|
||||
});
|
||||
}
|
||||
},
|
||||
async close(peer, details) {
|
||||
const userId = socketSessions[peer.id];
|
||||
async close(peer, _details) {
|
||||
const userId = socketSessions.get(peer.id);
|
||||
if (!userId) {
|
||||
console.log(`skipping websocket close for ${peer.id}`);
|
||||
return;
|
||||
@ -42,6 +40,6 @@ export default defineWebSocketHandler({
|
||||
|
||||
notificationSystem.unlisten(userId, peer.id);
|
||||
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, { TaskMessage } from "~/server/internal/tasks";
|
||||
import { parse as parseCookies } from "cookie-es";
|
||||
import taskHandler from "~/server/internal/tasks";
|
||||
import type { MinimumRequestObject } from "~/server/h3";
|
||||
|
||||
// TODO add web socket sessions for horizontal scaling
|
||||
// ID to admin
|
||||
const socketHeaders: { [key: string]: MinimumRequestObject } = {};
|
||||
const socketHeaders = new Map<string, MinimumRequestObject>();
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
async open(peer) {
|
||||
@ -15,25 +13,26 @@ export default defineWebSocketHandler({
|
||||
return;
|
||||
}
|
||||
|
||||
socketHeaders[peer.id] = {
|
||||
socketHeaders.set(peer.id, {
|
||||
headers: request.headers ?? new Headers(),
|
||||
};
|
||||
});
|
||||
peer.send(`connect`);
|
||||
},
|
||||
message(peer, message) {
|
||||
if (!peer.id) return;
|
||||
if (socketHeaders[peer.id] === undefined) return;
|
||||
const headers = socketHeaders.get(peer.id);
|
||||
if (headers === undefined) return;
|
||||
const text = message.text();
|
||||
if (text.startsWith("connect/")) {
|
||||
const id = text.substring("connect/".length);
|
||||
taskHandler.connect(peer.id, id, peer, socketHeaders[peer.id]);
|
||||
taskHandler.connect(peer.id, id, peer, headers);
|
||||
return;
|
||||
}
|
||||
},
|
||||
close(peer, details) {
|
||||
close(peer, _details) {
|
||||
if (!peer.id) return;
|
||||
if (socketHeaders[peer.id] === undefined) return;
|
||||
delete socketHeaders[peer.id];
|
||||
if (!socketHeaders.has(peer.id)) return;
|
||||
socketHeaders.delete(peer.id);
|
||||
|
||||
taskHandler.disconnectAll(peer.id);
|
||||
},
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["clients:revoke"]);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { APITokenMode } from "@prisma/client";
|
||||
import aclManager, { userACLs } from "~/server/internal/acls";
|
||||
import { userACLDescriptions } from "~/server/internal/acls/descriptions";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
@ -25,17 +24,23 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Token requires more than zero ACLs",
|
||||
});
|
||||
|
||||
const invalidACLs = acls.filter((e) => userACLs.findIndex((v) => e == v) == -1);
|
||||
if(invalidACLs.length > 0) throw createError({statusCode: 400, statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`});
|
||||
const invalidACLs = acls.filter(
|
||||
(e) => userACLs.findIndex((v) => e == v) == -1,
|
||||
);
|
||||
if (invalidACLs.length > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Invalid ACLs: ${invalidACLs.join(", ")}`,
|
||||
});
|
||||
|
||||
const token = await prisma.aPIToken.create({
|
||||
data: {
|
||||
mode: APITokenMode.User,
|
||||
name: name,
|
||||
userId: userId,
|
||||
acls: acls,
|
||||
}
|
||||
})
|
||||
const token = await prisma.aPIToken.create({
|
||||
data: {
|
||||
mode: APITokenMode.User,
|
||||
name: name,
|
||||
userId: userId,
|
||||
acls: acls,
|
||||
},
|
||||
});
|
||||
|
||||
return token;
|
||||
return token;
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { APITokenMode, User } from "@prisma/client";
|
||||
import { H3Context, H3Event } from "h3";
|
||||
import { APITokenMode } from "@prisma/client";
|
||||
import prisma from "../db/database";
|
||||
import sessionHandler from "../session";
|
||||
import type { MinimumRequestObject } from "~/server/h3";
|
||||
@ -98,7 +97,7 @@ class ACLManager {
|
||||
if (!token) return undefined;
|
||||
if (!token.userId)
|
||||
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) {
|
||||
@ -124,7 +123,7 @@ class ACLManager {
|
||||
|
||||
async allowSystemACL(
|
||||
request: MinimumRequestObject | undefined,
|
||||
acls: SystemACL
|
||||
acls: SystemACL,
|
||||
) {
|
||||
if (!request)
|
||||
throw new Error("Native web requests not available - weird deployment?");
|
||||
@ -157,13 +156,17 @@ class ACLManager {
|
||||
for (const acl of acls) {
|
||||
if (acl.startsWith(userACLPrefix)) {
|
||||
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 (acl.startsWith(systemACLPrefix)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import droplet from "@drop-oss/droplet";
|
||||
import type { CertificateStore} from "./ca-store";
|
||||
import { fsCertificateStore } from "./ca-store";
|
||||
import type { CertificateStore } from "./ca-store";
|
||||
|
||||
export type CertificateBundle = {
|
||||
priv: string;
|
||||
@ -50,7 +47,7 @@ export class CertificateAuthority {
|
||||
clientId,
|
||||
clientName,
|
||||
caCertificate.cert,
|
||||
caCertificate.priv
|
||||
caCertificate.priv,
|
||||
);
|
||||
const certBundle: CertificateBundle = {
|
||||
priv,
|
||||
@ -65,7 +62,7 @@ export class CertificateAuthority {
|
||||
|
||||
async fetchClientCertificate(clientId: string) {
|
||||
const isBlacklist = await this.certificateStore.checkBlacklistCertificate(
|
||||
`client:${clientId}`
|
||||
`client:${clientId}`,
|
||||
);
|
||||
if (isBlacklist) return undefined;
|
||||
return await this.certificateStore.fetch(`client:${clientId}`);
|
||||
|
||||
@ -18,8 +18,8 @@ export const validCapabilities = Object.values(InternalClientCapability);
|
||||
|
||||
export type CapabilityConfiguration = {
|
||||
[InternalClientCapability.PeerAPI]: { endpoints: string[] };
|
||||
[InternalClientCapability.UserStatus]: {};
|
||||
[InternalClientCapability.CloudSaves]: {};
|
||||
[InternalClientCapability.UserStatus]: object;
|
||||
[InternalClientCapability.CloudSaves]: object;
|
||||
};
|
||||
|
||||
class CapabilityManager {
|
||||
@ -53,7 +53,7 @@ class CapabilityManager {
|
||||
const serverCertificate = await ca.fetchClientCertificate("server");
|
||||
if (!serverCertificate)
|
||||
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({
|
||||
key: serverCertificate.priv,
|
||||
@ -70,7 +70,9 @@ class CapabilityManager {
|
||||
});
|
||||
valid = true;
|
||||
break;
|
||||
} catch {}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
@ -81,7 +83,7 @@ class CapabilityManager {
|
||||
|
||||
async validateCapabilityConfiguration(
|
||||
capability: InternalClientCapability,
|
||||
configuration: object
|
||||
configuration: object,
|
||||
) {
|
||||
const validationFunction = this.validationFunctions[capability];
|
||||
if (!validationFunction) return false;
|
||||
@ -91,7 +93,7 @@ class CapabilityManager {
|
||||
async upsertClientCapability(
|
||||
capability: InternalClientCapability,
|
||||
rawCapability: object,
|
||||
clientId: string
|
||||
clientId: string,
|
||||
) {
|
||||
const upsertFunctions: EnumDictionary<
|
||||
InternalClientCapability,
|
||||
|
||||
@ -6,7 +6,7 @@ import { useCertificateAuthority } from "~/server/plugins/ca";
|
||||
|
||||
export type EventHandlerFunction<T> = (
|
||||
h3: H3Event<EventHandlerRequest>,
|
||||
utils: ClientUtils
|
||||
utils: ClientUtils,
|
||||
) => Promise<T> | T;
|
||||
|
||||
type ClientUtils = {
|
||||
@ -25,7 +25,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
|
||||
let clientId: string;
|
||||
switch (method) {
|
||||
case "Debug":
|
||||
case "Debug": {
|
||||
if (!import.meta.dev) throw createError({ statusCode: 403 });
|
||||
const client = await prisma.client.findFirst({ select: { id: true } });
|
||||
if (!client)
|
||||
@ -35,7 +35,8 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
});
|
||||
clientId = client.id;
|
||||
break;
|
||||
case "Nonce":
|
||||
}
|
||||
case "Nonce": {
|
||||
clientId = parts[0];
|
||||
const nonce = parts[1];
|
||||
const signature = parts[2];
|
||||
@ -59,9 +60,8 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
}
|
||||
|
||||
const certificateAuthority = useCertificateAuthority();
|
||||
const certBundle = await certificateAuthority.fetchClientCertificate(
|
||||
clientId
|
||||
);
|
||||
const certBundle =
|
||||
await certificateAuthority.fetchClientCertificate(clientId);
|
||||
// This does the blacklist check already
|
||||
if (!certBundle)
|
||||
throw createError({
|
||||
@ -76,11 +76,13 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
statusMessage: "Invalid nonce signature.",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "No authentication",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (clientId === undefined)
|
||||
@ -95,7 +97,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
});
|
||||
if (!client)
|
||||
throw new Error(
|
||||
"client util fetch client broke - this should NOT happen"
|
||||
"client util fetch client broke - this should NOT happen",
|
||||
);
|
||||
return client;
|
||||
}
|
||||
@ -110,7 +112,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
|
||||
if (!client)
|
||||
throw new Error(
|
||||
"client util fetch client broke - this should NOT happen"
|
||||
"client util fetch client broke - this should NOT happen",
|
||||
);
|
||||
|
||||
return client.user;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { CertificateBundle } from "./ca";
|
||||
import prisma from "../db/database";
|
||||
import type { Platform } from "@prisma/client";
|
||||
import { useCertificateAuthority } from "~/server/plugins/ca";
|
||||
@ -10,25 +9,29 @@ export interface ClientMetadata {
|
||||
}
|
||||
|
||||
export class ClientHandler {
|
||||
private temporaryClientTable: {
|
||||
[key: string]: {
|
||||
private temporaryClientTable = new Map<
|
||||
string,
|
||||
{
|
||||
timeout: NodeJS.Timeout;
|
||||
data: ClientMetadata;
|
||||
userId?: string;
|
||||
authToken?: string;
|
||||
};
|
||||
} = {};
|
||||
}
|
||||
>();
|
||||
|
||||
async initiate(metadata: ClientMetadata) {
|
||||
const clientId = randomUUID();
|
||||
|
||||
this.temporaryClientTable[clientId] = {
|
||||
this.temporaryClientTable.set(clientId, {
|
||||
data: metadata,
|
||||
timeout: setTimeout(() => {
|
||||
if (this.temporaryClientTable[clientId])
|
||||
delete this.temporaryClientTable[clientId];
|
||||
}, 1000 * 60 * 10), // 10 minutes
|
||||
};
|
||||
timeout: setTimeout(
|
||||
() => {
|
||||
if (this.temporaryClientTable.has(clientId))
|
||||
this.temporaryClientTable.delete(clientId);
|
||||
},
|
||||
1000 * 60 * 10,
|
||||
), // 10 minutes
|
||||
});
|
||||
|
||||
return clientId;
|
||||
}
|
||||
@ -38,23 +41,23 @@ export class ClientHandler {
|
||||
}
|
||||
|
||||
async fetchClient(clientId: string) {
|
||||
const entry = this.temporaryClientTable[clientId];
|
||||
const entry = this.temporaryClientTable.get(clientId);
|
||||
if (!entry) return undefined;
|
||||
return entry;
|
||||
}
|
||||
|
||||
async attachUserId(clientId: string, userId: string) {
|
||||
if (!this.temporaryClientTable[clientId])
|
||||
throw new Error("Invalid clientId for attaching userId");
|
||||
this.temporaryClientTable[clientId].userId = userId;
|
||||
const clientTable = this.temporaryClientTable.get(clientId);
|
||||
if (!clientTable) throw new Error("Invalid clientId for attaching userId");
|
||||
clientTable.userId = userId;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
const token = randomUUID();
|
||||
this.temporaryClientTable[clientId].authToken = token;
|
||||
entry.authToken = token;
|
||||
|
||||
return token;
|
||||
}
|
||||
@ -66,7 +69,7 @@ export class ClientHandler {
|
||||
}
|
||||
|
||||
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.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,
|
||||
Object.assign({}, value, { versionName: rootManifest.versionName }),
|
||||
];
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -71,6 +71,7 @@ class ManifestGenerator {
|
||||
if (baseVersion.delta) {
|
||||
// Start at the same index minus one, and keep grabbing them
|
||||
// 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--) {
|
||||
const currentVersion = await prisma.gameVersion.findFirst({
|
||||
where: {
|
||||
@ -88,7 +89,7 @@ class ManifestGenerator {
|
||||
const metadata: DropManifestMetadata[] = leastToMost.map((e) => {
|
||||
return {
|
||||
manifest: JSON.parse(
|
||||
e.dropletManifest?.toString() ?? "{}"
|
||||
e.dropletManifest?.toString() ?? "{}",
|
||||
) as DropManifest,
|
||||
versionName: e.versionName,
|
||||
};
|
||||
@ -96,7 +97,7 @@ class ManifestGenerator {
|
||||
|
||||
const manifest = ManifestGenerator.generateManifestFromMetadata(
|
||||
metadata[0],
|
||||
...metadata.slice(1)
|
||||
...metadata.slice(1),
|
||||
);
|
||||
|
||||
return manifest;
|
||||
|
||||
@ -8,8 +8,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import prisma from "../db/database";
|
||||
import type { GameVersion} from "@prisma/client";
|
||||
import { Platform } from "@prisma/client";
|
||||
import type { GameVersion } from "@prisma/client";
|
||||
import { fuzzy } from "fast-fuzzy";
|
||||
import { recursivelyReaddir } from "../utils/recursivedirs";
|
||||
import taskHandler from "../tasks";
|
||||
@ -52,13 +51,13 @@ class LibraryManager {
|
||||
|
||||
async fetchUnimportedGameVersions(
|
||||
libraryBasePath: string,
|
||||
versions: Array<GameVersion>
|
||||
versions: Array<GameVersion>,
|
||||
) {
|
||||
const gameDir = path.join(this.basePath, libraryBasePath);
|
||||
const versionsDirs = fs.readdirSync(gameDir);
|
||||
const importedVersionDirs = versions.map((e) => e.versionName);
|
||||
const unimportedVersions = versionsDirs.filter(
|
||||
(e) => !importedVersionDirs.includes(e)
|
||||
(e) => !importedVersionDirs.includes(e),
|
||||
);
|
||||
|
||||
return unimportedVersions;
|
||||
@ -89,10 +88,10 @@ class LibraryManager {
|
||||
noVersions: e.versions.length == 0,
|
||||
unimportedVersions: await this.fetchUnimportedGameVersions(
|
||||
e.libraryBasePath,
|
||||
e.versions
|
||||
e.versions,
|
||||
),
|
||||
},
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@ -113,7 +112,7 @@ class LibraryManager {
|
||||
const targetDir = path.join(this.basePath, game.libraryBasePath);
|
||||
if (!fs.existsSync(targetDir))
|
||||
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 validVersions = versions.filter((versionDir) => {
|
||||
@ -124,7 +123,7 @@ class LibraryManager {
|
||||
const currentVersions = game.versions.map((e) => e.versionName);
|
||||
|
||||
const unimportedVersions = validVersions.filter(
|
||||
(e) => !currentVersions.includes(e)
|
||||
(e) => !currentVersions.includes(e),
|
||||
);
|
||||
return unimportedVersions;
|
||||
}
|
||||
@ -138,7 +137,7 @@ class LibraryManager {
|
||||
const targetDir = path.join(
|
||||
this.basePath,
|
||||
game.libraryBasePath,
|
||||
versionName
|
||||
versionName,
|
||||
);
|
||||
if (!fs.existsSync(targetDir)) return undefined;
|
||||
|
||||
@ -217,7 +216,7 @@ class LibraryManager {
|
||||
delta: boolean;
|
||||
|
||||
umuId: string;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const taskId = `import:${gameId}:${versionName}`;
|
||||
|
||||
@ -254,7 +253,7 @@ class LibraryManager {
|
||||
(err, manifest) => {
|
||||
if (err) return reject(err);
|
||||
resolve(manifest);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
import type {
|
||||
Developer,
|
||||
Publisher} from "@prisma/client";
|
||||
import {
|
||||
MetadataSource,
|
||||
PrismaClient
|
||||
} from "@prisma/client";
|
||||
import type { Developer, Publisher } from "@prisma/client";
|
||||
import { MetadataSource } from "@prisma/client";
|
||||
import prisma from "../db/database";
|
||||
import type {
|
||||
_FetchDeveloperMetadataParams,
|
||||
@ -17,11 +12,7 @@ import type {
|
||||
PublisherMetadata,
|
||||
} from "./types";
|
||||
import { ObjectTransactionalHandler } from "../objects/transactional";
|
||||
import { PriorityList, PriorityListIndexed } from "../utils/prioritylist";
|
||||
import { GiantBombProvider } from "./giantbomb";
|
||||
import { ManualMetadataProvider } from "./manual";
|
||||
import { PCGamingWikiProvider } from "./pcgamingwiki";
|
||||
import { IGDBProvider } from "./igdb";
|
||||
import { PriorityListIndexed } from "../utils/prioritylist";
|
||||
|
||||
export class MissingMetadataProviderConfig extends Error {
|
||||
private providerName: string;
|
||||
@ -47,10 +38,10 @@ export abstract class MetadataProvider {
|
||||
abstract search(query: string): Promise<GameMetadataSearchResult[]>;
|
||||
abstract fetchGame(params: _FetchGameMetadataParams): Promise<GameMetadata>;
|
||||
abstract fetchPublisher(
|
||||
params: _FetchPublisherMetadataParams
|
||||
params: _FetchPublisherMetadataParams,
|
||||
): Promise<PublisherMetadata>;
|
||||
abstract fetchDeveloper(
|
||||
params: _FetchDeveloperMetadataParams
|
||||
params: _FetchDeveloperMetadataParams,
|
||||
): Promise<DeveloperMetadata>;
|
||||
}
|
||||
|
||||
@ -81,6 +72,8 @@ export class MetadataHandler {
|
||||
for (const provider of this.providers.values()) {
|
||||
const queryTransformationPromise = new Promise<
|
||||
InternalGameMetadataResult[]
|
||||
// TODO: fix eslint error
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
>(async (resolve, reject) => {
|
||||
try {
|
||||
const results = await provider.search(query);
|
||||
@ -89,7 +82,7 @@ export class MetadataHandler {
|
||||
Object.assign({}, result, {
|
||||
sourceId: provider.id(),
|
||||
sourceName: provider.name(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
resolve(mappedResults);
|
||||
} catch (e) {
|
||||
@ -119,13 +112,13 @@ export class MetadataHandler {
|
||||
sourceId: "manual",
|
||||
sourceName: "Manual",
|
||||
},
|
||||
libraryBasePath
|
||||
libraryBasePath,
|
||||
);
|
||||
}
|
||||
|
||||
async createGame(
|
||||
result: InternalGameMetadataResult,
|
||||
libraryBasePath: string
|
||||
libraryBasePath: string,
|
||||
) {
|
||||
const provider = this.providers.get(result.sourceId);
|
||||
if (!provider)
|
||||
@ -143,7 +136,7 @@ export class MetadataHandler {
|
||||
|
||||
const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(
|
||||
{},
|
||||
["internal:read"]
|
||||
["internal:read"],
|
||||
);
|
||||
|
||||
let metadata;
|
||||
@ -197,7 +190,7 @@ export class MetadataHandler {
|
||||
return (await this.fetchDeveloperPublisher(
|
||||
query,
|
||||
"fetchDeveloper",
|
||||
"developer"
|
||||
"developer",
|
||||
)) as Developer;
|
||||
}
|
||||
|
||||
@ -205,7 +198,7 @@ export class MetadataHandler {
|
||||
return (await this.fetchDeveloperPublisher(
|
||||
query,
|
||||
"fetchPublisher",
|
||||
"publisher"
|
||||
"publisher",
|
||||
)) as Publisher;
|
||||
}
|
||||
|
||||
@ -214,8 +207,9 @@ export class MetadataHandler {
|
||||
private async fetchDeveloperPublisher(
|
||||
query: string,
|
||||
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({
|
||||
where: {
|
||||
metadataOriginalQuery: query,
|
||||
@ -229,7 +223,7 @@ export class MetadataHandler {
|
||||
|
||||
const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(
|
||||
{},
|
||||
["internal:read"]
|
||||
["internal:read"],
|
||||
);
|
||||
let result: PublisherMetadata;
|
||||
try {
|
||||
@ -243,6 +237,7 @@ export class MetadataHandler {
|
||||
// If we're successful
|
||||
await pullObjects();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = await (prisma as any)[databaseName].create({
|
||||
data: {
|
||||
metadataSource: provider.source(),
|
||||
@ -262,7 +257,7 @@ export class MetadataHandler {
|
||||
}
|
||||
|
||||
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,
|
||||
PublisherMetadata,
|
||||
_FetchDeveloperMetadataParams,
|
||||
DeveloperMetadata} from "./types";
|
||||
import {
|
||||
GameMetadataSearchResult
|
||||
DeveloperMetadata,
|
||||
} from "./types";
|
||||
import * as jdenticon from "jdenticon";
|
||||
|
||||
@ -22,13 +20,11 @@ export class ManualMetadataProvider implements MetadataProvider {
|
||||
source() {
|
||||
return MetadataSource.Manual;
|
||||
}
|
||||
async search(query: string) {
|
||||
async search(_query: string) {
|
||||
return [];
|
||||
}
|
||||
async fetchGame({
|
||||
name,
|
||||
publisher,
|
||||
developer,
|
||||
createObject,
|
||||
}: _FetchGameMetadataParams): Promise<GameMetadata> {
|
||||
const icon = jdenticon.toPng(name, 512);
|
||||
@ -52,12 +48,12 @@ export class ManualMetadataProvider implements MetadataProvider {
|
||||
};
|
||||
}
|
||||
async fetchPublisher(
|
||||
params: _FetchPublisherMetadataParams
|
||||
_params: _FetchPublisherMetadataParams,
|
||||
): Promise<PublisherMetadata> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async fetchDeveloper(
|
||||
params: _FetchDeveloperMetadataParams
|
||||
_params: _FetchDeveloperMetadataParams,
|
||||
): Promise<DeveloperMetadata> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { Developer, Publisher } from "@prisma/client";
|
||||
import { MetadataSource } from "@prisma/client";
|
||||
import type { MetadataProvider} from ".";
|
||||
import { MissingMetadataProviderConfig } from ".";
|
||||
import type { MetadataProvider } from ".";
|
||||
import type {
|
||||
GameMetadataSearchResult,
|
||||
_FetchGameMetadataParams,
|
||||
@ -48,7 +47,7 @@ interface PCGamingWikiCargoResult<T> {
|
||||
cargoquery: [
|
||||
{
|
||||
title: T;
|
||||
}
|
||||
},
|
||||
];
|
||||
error?: {
|
||||
code?: string;
|
||||
@ -61,8 +60,6 @@ interface PCGamingWikiCargoResult<T> {
|
||||
// Api Docs: https://www.pcgamingwiki.com/wiki/PCGamingWiki:API
|
||||
// Good tool for helping build cargo queries: https://www.pcgamingwiki.com/wiki/Special:CargoQuery
|
||||
export class PCGamingWikiProvider implements MetadataProvider {
|
||||
constructor() {}
|
||||
|
||||
id() {
|
||||
return "pcgamingwiki";
|
||||
}
|
||||
@ -75,7 +72,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
|
||||
private async request<T>(
|
||||
query: URLSearchParams,
|
||||
options?: AxiosRequestConfig
|
||||
options?: AxiosRequestConfig,
|
||||
) {
|
||||
const finalURL = `https://www.pcgamingwiki.com/w/api.php?${query.toString()}`;
|
||||
|
||||
@ -84,12 +81,12 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
baseURL: "",
|
||||
};
|
||||
const response = await axios.request<PCGamingWikiCargoResult<T>>(
|
||||
Object.assign({}, options, overlay)
|
||||
Object.assign({}, options, overlay),
|
||||
);
|
||||
|
||||
if (response.status !== 200)
|
||||
throw new Error(
|
||||
`Error in pcgamingwiki \nStatus Code: ${response.status}`
|
||||
`Error in pcgamingwiki \nStatus Code: ${response.status}`,
|
||||
);
|
||||
else if (response.data.error !== undefined)
|
||||
throw new Error(`Error in pcgamingwiki, malformed query`);
|
||||
@ -256,7 +253,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
}
|
||||
|
||||
async fetchDeveloper(
|
||||
params: _FetchDeveloperMetadataParams
|
||||
params: _FetchDeveloperMetadataParams,
|
||||
): Promise<DeveloperMetadata> {
|
||||
return await this.fetchPublisher(params);
|
||||
}
|
||||
|
||||
1
server/internal/metadata/types.d.ts
vendored
1
server/internal/metadata/types.d.ts
vendored
@ -1,6 +1,5 @@
|
||||
import type { Developer, Publisher } from "@prisma/client";
|
||||
import type { TransactionDataType } from "../objects/transactional";
|
||||
import { ObjectTransactionalHandler } from "../objects/transactional";
|
||||
import type { ObjectReference } from "../objects/objectHandler";
|
||||
|
||||
export interface GameMetadataSearchResult {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { triggerAsyncId } from "async_hooks";
|
||||
import prisma from "../db/database";
|
||||
import objectHandler from "../objects";
|
||||
|
||||
@ -50,7 +49,7 @@ class NewsManager {
|
||||
orderBy?: "asc" | "desc";
|
||||
tags?: string[];
|
||||
search?: string;
|
||||
} = {}
|
||||
} = {},
|
||||
) {
|
||||
return await prisma.article.findMany({
|
||||
where: {
|
||||
@ -116,7 +115,7 @@ class NewsManager {
|
||||
content?: string;
|
||||
excerpt?: string;
|
||||
image?: string;
|
||||
}
|
||||
},
|
||||
) {
|
||||
return await prisma.article.update({
|
||||
where: { id },
|
||||
|
||||
@ -15,27 +15,28 @@ export type NotificationCreateArgs = Pick<
|
||||
>;
|
||||
|
||||
class NotificationSystem {
|
||||
private listeners: {
|
||||
[key: string]: Map<string, (notification: Notification) => any>;
|
||||
} = {};
|
||||
private listeners = new Map<
|
||||
string,
|
||||
Map<string, (notification: Notification) => void>
|
||||
>();
|
||||
|
||||
listen(
|
||||
userId: string,
|
||||
id: string,
|
||||
callback: (notification: Notification) => any
|
||||
callback: (notification: Notification) => void,
|
||||
) {
|
||||
this.listeners[userId] ??= new Map();
|
||||
this.listeners[userId].set(id, callback);
|
||||
this.listeners.set(userId, new Map());
|
||||
this.listeners.get(userId)?.set(id, callback);
|
||||
|
||||
this.catchupListener(userId, id);
|
||||
}
|
||||
|
||||
unlisten(userId: string, id: string) {
|
||||
this.listeners[userId].delete(id);
|
||||
this.listeners.get(userId)?.delete(id);
|
||||
}
|
||||
|
||||
private async catchupListener(userId: string, id: string) {
|
||||
const callback = this.listeners[userId].get(id);
|
||||
const callback = this.listeners.get(userId)?.get(id);
|
||||
if (!callback)
|
||||
throw new Error("Failed to catch-up listener: callback does not exist");
|
||||
const notifications = await prisma.notification.findMany({
|
||||
@ -50,7 +51,7 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { ObjectBackend } from "./objectHandler";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Readable, Stream } from "stream";
|
||||
import { Readable } from "stream";
|
||||
import { createHash } from "crypto";
|
||||
import prisma from "../db/database";
|
||||
|
||||
@ -40,7 +40,7 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
if (source instanceof Readable) {
|
||||
const outputStream = fs.createWriteStream(objectPath);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
async create(
|
||||
id: string,
|
||||
source: Source,
|
||||
metadata: ObjectMetadata
|
||||
metadata: ObjectMetadata,
|
||||
): Promise<ObjectReference | undefined> {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
@ -104,7 +104,7 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
return true;
|
||||
}
|
||||
async fetchMetadata(
|
||||
id: ObjectReference
|
||||
id: ObjectReference,
|
||||
): Promise<ObjectMetadata | undefined> {
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (!fs.existsSync(metadataPath)) return undefined;
|
||||
@ -113,7 +113,7 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
}
|
||||
async writeMetadata(
|
||||
id: ObjectReference,
|
||||
metadata: ObjectMetadata
|
||||
metadata: ObjectMetadata,
|
||||
): Promise<boolean> {
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (!fs.existsSync(metadataPath)) return false;
|
||||
@ -153,8 +153,6 @@ class FsHashStore {
|
||||
max: 1000, // number of items
|
||||
});
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Gets hash of object
|
||||
* @param id
|
||||
@ -211,6 +209,8 @@ class FsHashStore {
|
||||
id,
|
||||
},
|
||||
});
|
||||
} catch {}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
import { parse as getMimeTypeBuffer } from "file-type-mime";
|
||||
import type { Writable } from "stream";
|
||||
import Stream, { Readable } from "stream";
|
||||
import { Readable } from "stream";
|
||||
import { getMimeType as getMimeTypeStream } from "stream-mime-type";
|
||||
|
||||
export type ObjectReference = string;
|
||||
@ -50,19 +50,19 @@ export abstract class ObjectBackend {
|
||||
abstract create(
|
||||
id: string,
|
||||
source: Source,
|
||||
metadata: ObjectMetadata
|
||||
metadata: ObjectMetadata,
|
||||
): Promise<ObjectReference | undefined>;
|
||||
abstract createWithWriteStream(
|
||||
id: string,
|
||||
metadata: ObjectMetadata
|
||||
metadata: ObjectMetadata,
|
||||
): Promise<Writable | undefined>;
|
||||
abstract delete(id: ObjectReference): Promise<boolean>;
|
||||
abstract fetchMetadata(
|
||||
id: ObjectReference
|
||||
id: ObjectReference,
|
||||
): Promise<ObjectMetadata | undefined>;
|
||||
abstract writeMetadata(
|
||||
id: ObjectReference,
|
||||
metadata: ObjectMetadata
|
||||
metadata: ObjectMetadata,
|
||||
): Promise<boolean>;
|
||||
abstract fetchHash(id: ObjectReference): Promise<string | undefined>;
|
||||
}
|
||||
@ -96,7 +96,7 @@ export class ObjectHandler {
|
||||
id: string,
|
||||
sourceFetcher: () => Promise<Source>,
|
||||
metadata: { [key: string]: string },
|
||||
permissions: Array<string>
|
||||
permissions: Array<string>,
|
||||
) {
|
||||
const { source, mime } = await this.fetchMimeType(await sourceFetcher());
|
||||
if (!mime)
|
||||
@ -112,7 +112,7 @@ export class ObjectHandler {
|
||||
async createWithStream(
|
||||
id: string,
|
||||
metadata: { [key: string]: string },
|
||||
permissions: Array<string>
|
||||
permissions: Array<string>,
|
||||
) {
|
||||
return this.backend.createWithWriteStream(id, {
|
||||
permissions,
|
||||
@ -194,7 +194,7 @@ export class ObjectHandler {
|
||||
async writeWithPermissions(
|
||||
id: ObjectReference,
|
||||
sourceFetcher: () => Promise<Source>,
|
||||
userId?: string
|
||||
userId?: string,
|
||||
) {
|
||||
const metadata = await this.backend.fetchMetadata(id);
|
||||
if (!metadata) return false;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import prisma from "../internal/db/database";
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
export default defineNitroPlugin(async (_nitro) => {
|
||||
// Ensure system user exists
|
||||
// The system user owns any user-based code
|
||||
// that we want to re-use for the app
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import prisma from "../internal/db/database";
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
export default defineNitroPlugin(async (_nitro) => {
|
||||
const userCount = await prisma.user.count({
|
||||
where: { id: { not: "system" } },
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ import { IGDBProvider } from "../internal/metadata/igdb";
|
||||
import { ManualMetadataProvider } from "../internal/metadata/manual";
|
||||
import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki";
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
export default defineNitroPlugin(async (_nitro) => {
|
||||
const metadataProviders = [
|
||||
GiantBombProvider,
|
||||
PCGamingWikiProvider,
|
||||
|
||||
Reference in New Issue
Block a user