mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
Setup wizard & 0.3.0 release (#146)
* fix: small merge fixes * feat: initial setup wizard * fix: last few localization items * fix: lint * fix: bump version
This commit is contained in:
@ -193,6 +193,7 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
|
|||||||
## Release 0.2.0-beta
|
## Release 0.2.0-beta
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- fix recursive dirs util #02d6346
|
- fix recursive dirs util #02d6346
|
||||||
- Fix username length requirement #0a5a649
|
- Fix username length requirement #0a5a649
|
||||||
- remove dynamic imports #0f10626
|
- remove dynamic imports #0f10626
|
||||||
@ -223,8 +224,8 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
|
|||||||
- fix FATAL: "root"... message #dbb315a
|
- fix FATAL: "root"... message #dbb315a
|
||||||
- only show versions that are directories #ef8f3ae
|
- only show versions that are directories #ef8f3ae
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- update prisma & delete games #089c3e0
|
- update prisma & delete games #089c3e0
|
||||||
- manual handshake #12e3125
|
- manual handshake #12e3125
|
||||||
- fetch game endpoint #1f4d075
|
- fetch game endpoint #1f4d075
|
||||||
@ -271,9 +272,9 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
|
|||||||
- add support for overriding UMU id #fd4a7d1
|
- add support for overriding UMU id #fd4a7d1
|
||||||
- add .sh for linux #fe9373a
|
- add .sh for linux #fe9373a
|
||||||
|
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
- quexeky <git@quexeky.dev>
|
|
||||||
|
- quexeky <git@quexeky.dev>
|
||||||
- fixed manifest generation #03a37f7
|
- fixed manifest generation #03a37f7
|
||||||
- manual ci/cd #03b0b0c
|
- manual ci/cd #03b0b0c
|
||||||
- ability to fetch client certs for p2p #0a715fe
|
- ability to fetch client certs for p2p #0a715fe
|
||||||
@ -379,7 +380,6 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
|
|||||||
- better server side signin redirects #ef13b68
|
- better server side signin redirects #ef13b68
|
||||||
- patch signin #f3672f8
|
- patch signin #f3672f8
|
||||||
|
|
||||||
|
|
||||||
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
|
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
|
||||||
|
|
||||||
## Release 0.1.0-beta
|
## Release 0.1.0-beta
|
||||||
|
|||||||
@ -1,80 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Listbox v-model="wiredLocale" as="div">
|
<LanguageSelectorListbox />
|
||||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-400">{{
|
|
||||||
$t("selectLanguage")
|
|
||||||
}}</ListboxLabel>
|
|
||||||
<div class="relative mt-2">
|
|
||||||
<ListboxButton
|
|
||||||
class="grid w-full cursor-default grid-cols-1 rounded-md bg-zinc-900 py-1.5 pr-2 pl-3 text-left text-zinc-300 outline-1 -outline-offset-1 outline-zinc-700 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
|
||||||
>
|
|
||||||
<span class="col-start-1 row-start-1 flex items-center gap-3 pr-6">
|
|
||||||
<EmojiText
|
|
||||||
:emoji="localeToEmoji(wiredLocale)"
|
|
||||||
class="-mt-0.5 shrink-0 max-w-6"
|
|
||||||
/>
|
|
||||||
<span class="block truncate">{{
|
|
||||||
currentLocaleInformation?.name ?? wiredLocale
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<ChevronUpDownIcon
|
|
||||||
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</ListboxButton>
|
|
||||||
|
|
||||||
<transition
|
|
||||||
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-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
|
|
||||||
>
|
|
||||||
<ListboxOption
|
|
||||||
v-for="listLocale in locales"
|
|
||||||
:key="listLocale.code"
|
|
||||||
v-slot="{ active, selected }"
|
|
||||||
as="template"
|
|
||||||
:value="listLocale.code"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
:class="[
|
|
||||||
active
|
|
||||||
? 'bg-blue-600 text-white outline-hidden'
|
|
||||||
: 'text-zinc-300',
|
|
||||||
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<EmojiText
|
|
||||||
:emoji="localeToEmoji(listLocale.code)"
|
|
||||||
class="-mt-0.5 shrink-0 max-w-6"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
selected ? 'font-semibold' : 'font-normal',
|
|
||||||
'ml-3 block truncate',
|
|
||||||
]"
|
|
||||||
>{{ listLocale.name }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
:class="[
|
|
||||||
active ? 'text-white' : 'text-blue-600',
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ListboxOption>
|
|
||||||
</ListboxOptions>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</Listbox>
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="mt-1 transition text-blue-500 hover:text-blue-600 text-sm"
|
class="mt-1 transition text-blue-500 hover:text-blue-600 text-sm"
|
||||||
to="https://translate.droposs.org/projects/drop/"
|
to="https://translate.droposs.org/projects/drop/"
|
||||||
@ -97,80 +23,3 @@
|
|||||||
</DevOnly>
|
</DevOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
Listbox,
|
|
||||||
ListboxButton,
|
|
||||||
ListboxLabel,
|
|
||||||
ListboxOption,
|
|
||||||
ListboxOptions,
|
|
||||||
} from "@headlessui/vue";
|
|
||||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
|
||||||
import {
|
|
||||||
ArrowTopRightOnSquareIcon,
|
|
||||||
CheckIcon,
|
|
||||||
} from "@heroicons/vue/24/outline";
|
|
||||||
import type { Locale } from "vue-i18n";
|
|
||||||
|
|
||||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
|
||||||
|
|
||||||
function changeLocale(locale: Locale) {
|
|
||||||
setLocale(locale);
|
|
||||||
|
|
||||||
// dynamically update the HTML attributes for language and direction
|
|
||||||
// this is necessary for proper rendering of the page in the new language
|
|
||||||
useHead({
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: locale,
|
|
||||||
dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function localeToEmoji(local: string): string {
|
|
||||||
switch (local) {
|
|
||||||
// Default locale
|
|
||||||
case "en":
|
|
||||||
case "en-us":
|
|
||||||
return "🇺🇸";
|
|
||||||
|
|
||||||
case "en-gb":
|
|
||||||
return "🇬🇧";
|
|
||||||
case "en-ca":
|
|
||||||
return "🇨🇦";
|
|
||||||
case "en-au":
|
|
||||||
return "🇦🇺";
|
|
||||||
case "en-pirate":
|
|
||||||
return "🏴☠️";
|
|
||||||
case "fr":
|
|
||||||
return "🇫🇷";
|
|
||||||
case "de":
|
|
||||||
return "🇩🇪";
|
|
||||||
case "es":
|
|
||||||
return "🇪🇸";
|
|
||||||
case "it":
|
|
||||||
return "🇮🇹";
|
|
||||||
case "zh":
|
|
||||||
return "🇨🇳";
|
|
||||||
case "zh-tw":
|
|
||||||
return "🇹🇼";
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return "❓";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wiredLocale = computed({
|
|
||||||
get() {
|
|
||||||
return currLocale.value;
|
|
||||||
},
|
|
||||||
set(v) {
|
|
||||||
changeLocale(v);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const currentLocaleInformation = computed(() =>
|
|
||||||
locales.value.find((e) => e.code == wiredLocale.value),
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|||||||
155
components/LanguageSelectorListbox.vue
Normal file
155
components/LanguageSelectorListbox.vue
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<Listbox v-model="wiredLocale" as="div">
|
||||||
|
<ListboxLabel
|
||||||
|
v-if="showText"
|
||||||
|
class="block text-sm/6 font-medium text-zinc-400"
|
||||||
|
>{{ $t("selectLanguage") }}</ListboxLabel
|
||||||
|
>
|
||||||
|
<div class="relative mt-2">
|
||||||
|
<ListboxButton
|
||||||
|
class="grid w-full cursor-default grid-cols-1 rounded-md bg-zinc-900 py-1.5 pr-2 pl-3 text-left text-zinc-300 outline-1 -outline-offset-1 outline-zinc-700 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||||
|
>
|
||||||
|
<span class="col-start-1 row-start-1 flex items-center gap-3 pr-6">
|
||||||
|
<EmojiText
|
||||||
|
:emoji="localeToEmoji(wiredLocale)"
|
||||||
|
class="-mt-0.5 shrink-0 max-w-6"
|
||||||
|
/>
|
||||||
|
<span class="block truncate">{{
|
||||||
|
currentLocaleInformation?.name ?? wiredLocale
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<ChevronUpDownIcon
|
||||||
|
class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
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-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
|
||||||
|
>
|
||||||
|
<ListboxOption
|
||||||
|
v-for="listLocale in locales"
|
||||||
|
:key="listLocale.code"
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
as="template"
|
||||||
|
:value="listLocale.code"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-blue-600 text-white outline-hidden'
|
||||||
|
: 'text-zinc-300',
|
||||||
|
'relative cursor-default py-2 pr-9 pl-3 select-none',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<EmojiText
|
||||||
|
:emoji="localeToEmoji(listLocale.code)"
|
||||||
|
class="-mt-0.5 shrink-0 max-w-6"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
selected ? 'font-semibold' : 'font-normal',
|
||||||
|
'ml-3 block truncate',
|
||||||
|
]"
|
||||||
|
>{{ listLocale.name }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
:class="[
|
||||||
|
active ? 'text-white' : 'text-blue-600',
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<CheckIcon class="size-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxLabel,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||||
|
import { CheckIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import type { Locale } from "vue-i18n";
|
||||||
|
|
||||||
|
const { showText = true } = defineProps<{ showText?: boolean }>();
|
||||||
|
|
||||||
|
const { locales, locale: currLocale, setLocale } = useI18n();
|
||||||
|
|
||||||
|
function changeLocale(locale: Locale) {
|
||||||
|
setLocale(locale);
|
||||||
|
|
||||||
|
// dynamically update the HTML attributes for language and direction
|
||||||
|
// this is necessary for proper rendering of the page in the new language
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: locale,
|
||||||
|
dir: locales.value.find((l) => l.code === locale)?.dir || "ltr",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function localeToEmoji(local: string): string {
|
||||||
|
switch (local) {
|
||||||
|
// Default locale
|
||||||
|
case "en":
|
||||||
|
case "en-us":
|
||||||
|
return "🇺🇸";
|
||||||
|
|
||||||
|
case "en-gb":
|
||||||
|
return "🇬🇧";
|
||||||
|
case "en-ca":
|
||||||
|
return "🇨🇦";
|
||||||
|
case "en-au":
|
||||||
|
return "🇦🇺";
|
||||||
|
case "en-pirate":
|
||||||
|
return "🏴☠️";
|
||||||
|
case "fr":
|
||||||
|
return "🇫🇷";
|
||||||
|
case "de":
|
||||||
|
return "🇩🇪";
|
||||||
|
case "es":
|
||||||
|
return "🇪🇸";
|
||||||
|
case "it":
|
||||||
|
return "🇮🇹";
|
||||||
|
case "zh":
|
||||||
|
return "🇨🇳";
|
||||||
|
case "zh-tw":
|
||||||
|
return "🇹🇼";
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return "❓";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wiredLocale = computed({
|
||||||
|
get() {
|
||||||
|
return currLocale.value;
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
changeLocale(v);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const currentLocaleInformation = computed(() =>
|
||||||
|
locales.value.find((e) => e.code == wiredLocale.value),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
159
components/Setup/Account.vue
Normal file
159
components/Setup/Account.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2 lg:p-4">
|
||||||
|
<div class="px-4 py-2 max-w-xl">
|
||||||
|
<h1 class="font-semibold text-zinc-100 text-xl">
|
||||||
|
{{ $t("setup.auth.title") }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-zinc-400">
|
||||||
|
{{ $t("setup.auth.description") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-2 xl:grid-cols-3 h-fit p-4 gap-4">
|
||||||
|
<div class="p-4 border-1 border-zinc-800 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-zinc-100 font-semibold text-lg">
|
||||||
|
{{ $t("setup.auth.simple.title") }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-zinc-400">
|
||||||
|
{{ $t("setup.auth.simple.description") }}
|
||||||
|
</p>
|
||||||
|
<NuxtLink
|
||||||
|
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||||
|
href="https://docs.droposs.org/docs/authentication/simple"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
keypath="setup.auth.docs"
|
||||||
|
tag="span"
|
||||||
|
class="inline-flex items-center gap-x-1"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<template #arrow>
|
||||||
|
<ArrowTopRightOnSquareIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="w-full flex justify-between items-center">
|
||||||
|
<span class="text-zinc-100 font-semibold text-sm">{{
|
||||||
|
$t("setup.auth.enabled")
|
||||||
|
}}</span>
|
||||||
|
<CheckIcon
|
||||||
|
v-if="enabledAuth.Simple"
|
||||||
|
class="size-5 text-green-600"
|
||||||
|
/>
|
||||||
|
<XMarkIcon v-else class="size-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<LoadingButton
|
||||||
|
class="mt-4"
|
||||||
|
:loading="invitationLoading"
|
||||||
|
:disabled="!enabledAuth.Simple"
|
||||||
|
@click="() => registerAsAdmin()"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
keypath="setup.auth.simple.register"
|
||||||
|
tag="span"
|
||||||
|
class="inline-flex items-center gap-x-1"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<template #arrow>
|
||||||
|
{{ $t("chars.arrow") }}
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 border-1 border-zinc-800 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-zinc-100 font-semibold text-lg">
|
||||||
|
{{ $t("setup.auth.openid.title") }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-zinc-400">
|
||||||
|
{{ $t("setup.auth.openid.description") }}
|
||||||
|
</p>
|
||||||
|
<NuxtLink
|
||||||
|
class="mt-4 rounded-md inline-flex items-center text-sm font-semibold text-blue-500 hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
|
||||||
|
href="https://docs.droposs.org/docs/authentication/oidc"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
keypath="setup.auth.docs"
|
||||||
|
tag="span"
|
||||||
|
class="inline-flex items-center gap-x-1"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<template #arrow>
|
||||||
|
<ArrowTopRightOnSquareIcon class="size-4" />
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="w-full flex justify-between items-center">
|
||||||
|
<span class="text-zinc-100 font-semibold text-sm">{{
|
||||||
|
$t("setup.auth.enabled")
|
||||||
|
}}</span>
|
||||||
|
<CheckIcon
|
||||||
|
v-if="enabledAuth.OpenID"
|
||||||
|
class="size-5 text-green-600"
|
||||||
|
/>
|
||||||
|
<XMarkIcon v-else class="size-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<LoadingButton
|
||||||
|
class="mt-4"
|
||||||
|
:loading="false"
|
||||||
|
:disabled="!enabledAuth.OpenID"
|
||||||
|
@click="() => (complete = true)"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
keypath="setup.auth.openid.skip"
|
||||||
|
tag="span"
|
||||||
|
class="inline-flex items-center gap-x-1"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<template #arrow>
|
||||||
|
{{ $t("chars.arrow") }}
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ArrowTopRightOnSquareIcon,
|
||||||
|
CheckIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
} from "@heroicons/vue/24/outline";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
const complete = defineModel<boolean>({ required: true });
|
||||||
|
|
||||||
|
const { token } = defineProps<{ token: string }>();
|
||||||
|
|
||||||
|
const invitationLoading = ref(false);
|
||||||
|
|
||||||
|
const enabledAuth = await $dropFetch("/api/v1/admin/auth", {
|
||||||
|
headers: { Authorization: token },
|
||||||
|
});
|
||||||
|
|
||||||
|
async function registerAsAdmin() {
|
||||||
|
invitationLoading.value = true;
|
||||||
|
const expiryDate = DateTime.now().plus({ year: 5000 }).toJSON();
|
||||||
|
|
||||||
|
const invitation = await $dropFetch("/api/v1/admin/auth/invitation", {
|
||||||
|
method: "POST",
|
||||||
|
body: { isAdmin: true, expires: expiryDate },
|
||||||
|
headers: { Authorization: token },
|
||||||
|
failTitle: "Failed to create admin invitation",
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(`${invitation.inviteUrl}&after=close`, "_blank")?.focus();
|
||||||
|
invitationLoading.value = false;
|
||||||
|
complete.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
15
components/Setup/Library.vue
Normal file
15
components/Setup/Library.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-8">
|
||||||
|
<AdminSourcesPage :token="token" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AdminSourcesPage from "~/pages/admin/library/sources/index.vue";
|
||||||
|
|
||||||
|
const complete = defineModel<boolean>({ required: true });
|
||||||
|
// Only runs on component load, so it's fine
|
||||||
|
complete.value = true;
|
||||||
|
|
||||||
|
const { token } = defineProps<{ token: string }>();
|
||||||
|
</script>
|
||||||
@ -67,7 +67,7 @@ export const $dropFetch: DropFetch = async (rawRequest, opts) => {
|
|||||||
try {
|
try {
|
||||||
const data = await $fetch(request, {
|
const data = await $fetch(request, {
|
||||||
...opts,
|
...opts,
|
||||||
headers: { ...opts?.headers, ...headers },
|
headers: { ...headers, ...opts?.headers },
|
||||||
});
|
});
|
||||||
if (import.meta.server) state.value = data;
|
if (import.meta.server) state.value = data;
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@ -1 +1,5 @@
|
|||||||
{}
|
{
|
||||||
|
"setup": {
|
||||||
|
"welcome": "G'day."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -63,6 +63,38 @@
|
|||||||
"signout": "Signout",
|
"signout": "Signout",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
},
|
},
|
||||||
|
"setup": {
|
||||||
|
"welcome": "Hey there.",
|
||||||
|
"welcomeDescription": "Welcome to Drop setup wizard. It will walk you through configuring Drop for the first time, and how it works.",
|
||||||
|
"finish": "Let's go {arrow}",
|
||||||
|
"noPage": "no page",
|
||||||
|
"auth": {
|
||||||
|
"title": "Authentication",
|
||||||
|
"description": "Authentication in Drop happens through multiple configured 'providers'. Each one can allow users to sign-in through their method. To get started, have at least one authentication provider enabled, and create an account through it.",
|
||||||
|
"docs": "Documentation {arrow}",
|
||||||
|
"enabled": "Enabled?",
|
||||||
|
"simple": {
|
||||||
|
"title": "Simple authentication",
|
||||||
|
"description": "Simple authentication uses username/password to authentication users. It is enabled by default if no other authentication provider is enabled.",
|
||||||
|
"register": "Register as admin {arrow}"
|
||||||
|
},
|
||||||
|
"openid": {
|
||||||
|
"title": "OpenID Connect",
|
||||||
|
"description": "OpenID Connect (OIDC) is an OAuth2 extension commonly supported. Drop requires OIDC configuration to be done via environment variables.",
|
||||||
|
"skip": "I have a user with OIDC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"account": {
|
||||||
|
"name": "Setup your admin account.",
|
||||||
|
"description": "You need at least one account to start using Drop."
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"name": "Create a library.",
|
||||||
|
"description": "Add at least one library source to use Drop."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"chars": {
|
"chars": {
|
||||||
"arrow": "→",
|
"arrow": "→",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "drop",
|
"name": "drop",
|
||||||
"version": "0.3.0-alpha.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
|||||||
@ -296,8 +296,15 @@ useHead({
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// Optional token for setup wizard
|
||||||
|
const { token = undefined } = defineProps<{ token?: string }>();
|
||||||
|
|
||||||
|
const headers = token ? { Authorization: token } : undefined;
|
||||||
|
|
||||||
const sources = ref(
|
const sources = ref(
|
||||||
await $dropFetch<WorkingLibrarySource[]>("/api/v1/admin/library/sources"),
|
await $dropFetch<WorkingLibrarySource[]>("/api/v1/admin/library/sources", {
|
||||||
|
headers,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const editIndex = ref<undefined | number>(undefined);
|
const editIndex = ref<undefined | number>(undefined);
|
||||||
@ -345,6 +352,7 @@ async function performActionSource() {
|
|||||||
options: sourceConfig.value,
|
options: sourceConfig.value,
|
||||||
},
|
},
|
||||||
method: createMode ? "POST" : "PATCH",
|
method: createMode ? "POST" : "PATCH",
|
||||||
|
headers,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (createMode) {
|
if (createMode) {
|
||||||
@ -394,6 +402,7 @@ async function deleteSource(index: number) {
|
|||||||
await $dropFetch("/api/v1/admin/library/sources", {
|
await $dropFetch("/api/v1/admin/library/sources", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: { id: source.id },
|
body: { id: source.id },
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
createModal(
|
createModal(
|
||||||
|
|||||||
@ -264,6 +264,10 @@ function register_wrapper() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
register()
|
register()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
if (route.query.after == "close") {
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.push("/auth/signin");
|
router.push("/auth/signin");
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
|
|||||||
@ -105,7 +105,6 @@ const news = useNews();
|
|||||||
|
|
||||||
if (!news.value) {
|
if (!news.value) {
|
||||||
await fetchNews();
|
await fetchNews();
|
||||||
console.log("fetched news");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
227
pages/setup.vue
Normal file
227
pages/setup.vue
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex flex-col">
|
||||||
|
<div class="grow grid grid-cols-1 lg:grid-cols-2">
|
||||||
|
<div class="border-b lg:border-b-0 lg:border-r border-zinc-700">
|
||||||
|
<header
|
||||||
|
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
|
||||||
|
>
|
||||||
|
<DropWordmark />
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl font-display font-bold text-zinc-100">
|
||||||
|
{{ $t("setup.welcome") }}
|
||||||
|
</h1>
|
||||||
|
<LanguageSelectorListbox :show-text="false" class="mt-4 max-w-sm" />
|
||||||
|
<p class="mt-6 text-zinc-400 max-w-xl">
|
||||||
|
{{ $t("setup.welcomeDescription") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul role="list" class="mt-10 divide-y divide-zinc-700/5">
|
||||||
|
<li
|
||||||
|
v-for="(action, actionIdx) in actions"
|
||||||
|
:key="action.name"
|
||||||
|
class="relative flex gap-x-6 py-6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex size-10 flex-none items-center justify-center rounded-lg shadow-xs outline-1 outline-zinc-100/10"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="action.icon"
|
||||||
|
v-if="!actionsComplete[actionIdx]"
|
||||||
|
class="size-6 text-blue-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<CheckIcon v-else class="size-6 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<h3 class="text-sm/6 font-semibold text-zinc-100">
|
||||||
|
<button
|
||||||
|
:class="
|
||||||
|
actionsComplete[actionIdx]
|
||||||
|
? 'line-through text-zinc-300'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="() => (currentAction = actionIdx)"
|
||||||
|
>
|
||||||
|
<span class="absolute inset-0" aria-hidden="true" />
|
||||||
|
{{ action.name }}
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm/6 text-zinc-400">
|
||||||
|
{{ action.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none self-center">
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="size-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<LoadingButton
|
||||||
|
:disabled="!finished"
|
||||||
|
:loading="finishLoading"
|
||||||
|
@click="() => finish()"
|
||||||
|
>
|
||||||
|
<i18n-t keypath="setup.finish" tag="span" scope="global">
|
||||||
|
<template #arrow>
|
||||||
|
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</LoadingButton>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<component
|
||||||
|
:is="actions[currentAction].page"
|
||||||
|
v-if="actions[currentAction] && !useModal"
|
||||||
|
v-model="actionsComplete[currentAction]"
|
||||||
|
:token="bearerToken"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="!useModal"
|
||||||
|
class="bg-zinc-950/30 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<p class="uppercase text-sm font-display text-zinc-700 font-bold">
|
||||||
|
{{ $t("setup.noPage") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Transition>
|
||||||
|
<div v-if="useModal && open" class="relative z-10">
|
||||||
|
<div class="fixed inset-0 bg-zinc-900/75 transition-opacity" />
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative transform overflow-hidden rounded-lg bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<component
|
||||||
|
:is="actions[currentAction].page"
|
||||||
|
v-model="actionsComplete[currentAction]"
|
||||||
|
:token="bearerToken"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 sm:mt-6 p-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
|
@click="currentAction = -1"
|
||||||
|
>
|
||||||
|
{{ $t("common.close") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SetupAccount, SetupLibrary } from "#components";
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ServerStackIcon,
|
||||||
|
UserCircleIcon,
|
||||||
|
} from "@heroicons/vue/24/outline";
|
||||||
|
import type { Component } from "vue";
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
const useModal = computed(() => !breakpoints.lg.value);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: t("setup.welcome"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const token = route.query.token;
|
||||||
|
if (!token)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "No token.",
|
||||||
|
fatal: true,
|
||||||
|
});
|
||||||
|
const bearerToken = `Bearer ${token}`;
|
||||||
|
|
||||||
|
const allowed = await $dropFetch("/api/v1/admin", {
|
||||||
|
headers: { Authorization: bearerToken },
|
||||||
|
});
|
||||||
|
if (!allowed)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Invalid setup token. Please check the URL you opened.",
|
||||||
|
fatal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentAction = ref(-1);
|
||||||
|
const actions = ref<
|
||||||
|
Array<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: Component;
|
||||||
|
page: Component;
|
||||||
|
}>
|
||||||
|
>([
|
||||||
|
{
|
||||||
|
name: t("setup.stages.account.name"),
|
||||||
|
description: t("setup.stages.account.description"),
|
||||||
|
icon: UserCircleIcon,
|
||||||
|
page: SetupAccount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t("setup.stages.library.name"),
|
||||||
|
description: t("setup.stages.library.description"),
|
||||||
|
icon: ServerStackIcon,
|
||||||
|
page: SetupLibrary,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const actionsComplete = ref(Array(actions.value.length).fill(false));
|
||||||
|
|
||||||
|
const finished = computed(
|
||||||
|
() => actionsComplete.value.filter((e) => !e).length == 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const open = computed(() => currentAction.value != -1);
|
||||||
|
definePageMeta({
|
||||||
|
layout: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const finishLoading = ref(false);
|
||||||
|
async function finish() {
|
||||||
|
currentAction.value = -1;
|
||||||
|
finishLoading.value = true;
|
||||||
|
await $dropFetch("/api/v1/setup", {
|
||||||
|
headers: { Authorization: bearerToken },
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
router.push("/signin");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,101 +0,0 @@
|
|||||||
enum MetadataSource {
|
|
||||||
Manual
|
|
||||||
GiantBomb
|
|
||||||
}
|
|
||||||
|
|
||||||
model Game {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
metadataSource MetadataSource
|
|
||||||
metadataId String
|
|
||||||
created DateTime @default(now())
|
|
||||||
|
|
||||||
// Any field prefixed with m is filled in from metadata
|
|
||||||
// Acts as a cache so we can search and filter it
|
|
||||||
mName String // Name of game
|
|
||||||
mShortDescription String // Short description
|
|
||||||
mDescription String // Supports markdown
|
|
||||||
mDevelopers Developer[]
|
|
||||||
mPublishers Publisher[]
|
|
||||||
mReleased DateTime // When the game was released
|
|
||||||
|
|
||||||
mReviewCount Int
|
|
||||||
mReviewRating Float // 0 to 1
|
|
||||||
|
|
||||||
mIconId String // linked to objects in s3
|
|
||||||
mBannerId String // linked to objects in s3
|
|
||||||
mCoverId String
|
|
||||||
mImageCarousel String[] // linked to below array
|
|
||||||
mImageLibrary String[] // linked to objects in s3
|
|
||||||
|
|
||||||
versions GameVersion[]
|
|
||||||
libraryBasePath String @unique // Base dir for all the game versions
|
|
||||||
|
|
||||||
collections CollectionEntry[]
|
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A particular set of files that relate to the version
|
|
||||||
model GameVersion {
|
|
||||||
gameId String
|
|
||||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
|
||||||
versionName String // Sub directory for the game files
|
|
||||||
|
|
||||||
created DateTime @default(now())
|
|
||||||
|
|
||||||
platform Platform
|
|
||||||
|
|
||||||
launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine
|
|
||||||
launchArgs String[]
|
|
||||||
setupCommand String @default("") // Command to setup game (dependencies and such)
|
|
||||||
setupArgs String[]
|
|
||||||
onlySetup Boolean @default(false)
|
|
||||||
|
|
||||||
umuIdOverride String?
|
|
||||||
|
|
||||||
dropletManifest Json // Results from droplet
|
|
||||||
|
|
||||||
versionIndex Int
|
|
||||||
delta Boolean @default(false)
|
|
||||||
|
|
||||||
@@id([gameId, versionName])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Developer {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
metadataSource MetadataSource
|
|
||||||
metadataId String
|
|
||||||
metadataOriginalQuery String
|
|
||||||
|
|
||||||
mName String
|
|
||||||
mShortDescription String
|
|
||||||
mDescription String
|
|
||||||
mLogo String
|
|
||||||
mBanner String
|
|
||||||
mWebsite String
|
|
||||||
|
|
||||||
games Game[]
|
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Publisher {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
metadataSource MetadataSource
|
|
||||||
metadataId String
|
|
||||||
metadataOriginalQuery String
|
|
||||||
|
|
||||||
mName String
|
|
||||||
mShortDescription String
|
|
||||||
mDescription String
|
|
||||||
mLogo String
|
|
||||||
mBanner String
|
|
||||||
mWebsite String
|
|
||||||
|
|
||||||
games Game[]
|
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey")
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@ import aclManager from "~/server/internal/acls";
|
|||||||
import authManager from "~/server/internal/auth";
|
import authManager from "~/server/internal/auth";
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]);
|
const allowed = await aclManager.allowSystemACL(h3, ["auth:read", "setup"]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
const enabledAuthManagers = authManager.getAuthProviders();
|
const enabledAuthManagers = authManager.getAuthProviders();
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const CreateInvite = SharedRegisterValidator.partial()
|
|||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, [
|
const allowed = await aclManager.allowSystemACL(h3, [
|
||||||
"auth:simple:invitation:new",
|
"auth:simple:invitation:new",
|
||||||
|
"setup",
|
||||||
]);
|
]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
|||||||
7
server/api/v1/admin/index.get.ts
Normal file
7
server/api/v1/admin/index.get.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, []);
|
||||||
|
if (!allowed) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
@ -12,6 +12,7 @@ export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>(
|
|||||||
async (h3) => {
|
async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, [
|
const allowed = await aclManager.allowSystemACL(h3, [
|
||||||
"library:sources:delete",
|
"library:sources:delete",
|
||||||
|
"setup",
|
||||||
]);
|
]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import libraryManager from "~/server/internal/library";
|
|||||||
export type WorkingLibrarySource = LibraryModel & { working: boolean };
|
export type WorkingLibrarySource = LibraryModel & { working: boolean };
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, ["library:sources:read"]);
|
const allowed = await aclManager.allowSystemACL(h3, [
|
||||||
|
"library:sources:read",
|
||||||
|
"setup",
|
||||||
|
]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
const sources = await libraryManager.fetchLibraries();
|
const sources = await libraryManager.fetchLibraries();
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default defineEventHandler<{ body: typeof UpdateLibrarySource.infer }>(
|
|||||||
async (h3) => {
|
async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, [
|
const allowed = await aclManager.allowSystemACL(h3, [
|
||||||
"library:sources:update",
|
"library:sources:update",
|
||||||
|
"setup",
|
||||||
]);
|
]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export default defineEventHandler<{ body: typeof CreateLibrarySource.infer }>(
|
|||||||
async (h3) => {
|
async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, [
|
const allowed = await aclManager.allowSystemACL(h3, [
|
||||||
"library:sources:new",
|
"library:sources:new",
|
||||||
|
"setup",
|
||||||
]);
|
]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
|||||||
20
server/api/v1/setup.post.ts
Normal file
20
server/api/v1/setup.post.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { APITokenMode } from "~/prisma/client/enums";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["setup"]);
|
||||||
|
if (!allowed)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Must use a setup token.",
|
||||||
|
});
|
||||||
|
await prisma.aPIToken.deleteMany({
|
||||||
|
where: {
|
||||||
|
mode: APITokenMode.System,
|
||||||
|
acls: {
|
||||||
|
hasSome: ["setup"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -44,6 +44,9 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||||
|
setup:
|
||||||
|
"All permissions required to setup a new Drop instance (setup wizard).",
|
||||||
|
|
||||||
"auth:read":
|
"auth:read":
|
||||||
"Fetch the list of enabled authentication mechanisms configured.",
|
"Fetch the list of enabled authentication mechanisms configured.",
|
||||||
"auth:simple:invitation:read": "Fetch simple auth invitations.",
|
"auth:simple:invitation:read": "Fetch simple auth invitations.",
|
||||||
|
|||||||
@ -41,6 +41,8 @@ const userACLPrefix = "user:";
|
|||||||
export type UserACL = Array<(typeof userACLs)[number]>;
|
export type UserACL = Array<(typeof userACLs)[number]>;
|
||||||
|
|
||||||
export const systemACLs = [
|
export const systemACLs = [
|
||||||
|
"setup",
|
||||||
|
|
||||||
"auth:read",
|
"auth:read",
|
||||||
"auth:simple:invitation:read",
|
"auth:simple:invitation:read",
|
||||||
"auth:simple:invitation:new",
|
"auth:simple:invitation:new",
|
||||||
@ -167,9 +169,11 @@ class ACLManager {
|
|||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userSession.userId },
|
where: { id: userSession.userId },
|
||||||
});
|
});
|
||||||
if (!user) return false;
|
if (user) {
|
||||||
if (user.admin) return true;
|
if (!user) return false;
|
||||||
return false;
|
if (user.admin) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorizationToken = this.getAuthorizationToken(request);
|
const authorizationToken = this.getAuthorizationToken(request);
|
||||||
@ -179,6 +183,10 @@ class ACLManager {
|
|||||||
});
|
});
|
||||||
if (!token) return false;
|
if (!token) return false;
|
||||||
if (token.mode != APITokenMode.System) return false;
|
if (token.mode != APITokenMode.System) return false;
|
||||||
|
|
||||||
|
// If empty, we just want to check we are an admin *at all*, not specific ACLs
|
||||||
|
if (acls.length == 0) return true;
|
||||||
|
|
||||||
for (const acl of acls) {
|
for (const acl of acls) {
|
||||||
const tokenACLIndex = token.acls.findIndex((e) => e == acl);
|
const tokenACLIndex = token.acls.findIndex((e) => e == acl);
|
||||||
if (tokenACLIndex != -1) return true;
|
if (tokenACLIndex != -1) return true;
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
|
import { APITokenMode } from "~/prisma/client/enums";
|
||||||
import prisma from "~/server/internal/db/database";
|
import prisma from "~/server/internal/db/database";
|
||||||
|
import { systemConfig } from "../internal/config/sys-conf";
|
||||||
|
import { logger } from "../internal/logging";
|
||||||
|
|
||||||
export default defineNitroPlugin(async (_nitro) => {
|
export default defineNitroPlugin(async (_nitro) => {
|
||||||
|
await prisma.aPIToken.deleteMany({
|
||||||
|
where: {
|
||||||
|
acls: {
|
||||||
|
hasSome: ["setup"],
|
||||||
|
},
|
||||||
|
mode: APITokenMode.System,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const userCount = await prisma.user.count({
|
const userCount = await prisma.user.count({
|
||||||
where: { id: { not: "system" } },
|
where: { id: { not: "system" } },
|
||||||
});
|
});
|
||||||
@ -10,18 +22,14 @@ export default defineNitroPlugin(async (_nitro) => {
|
|||||||
// but has not been configured
|
// but has not been configured
|
||||||
// so it should be in-place
|
// so it should be in-place
|
||||||
|
|
||||||
// Create admin invitation
|
const token = await prisma.aPIToken.create({
|
||||||
await prisma.invitation.upsert({
|
data: {
|
||||||
where: {
|
name: "Setup Wizard",
|
||||||
id: "admin",
|
mode: APITokenMode.System,
|
||||||
},
|
acls: ["setup"],
|
||||||
create: {
|
|
||||||
id: "admin",
|
|
||||||
isAdmin: true,
|
|
||||||
expires: new Date("4096-01-01"),
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
isAdmin: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setupUrl = `${systemConfig.getExternalUrl()}/setup?token=${token.token}`;
|
||||||
|
logger.info(`Open ${setupUrl} in a browser to get started with Drop.`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user