Browse Source

rework setup

pull/1397/head
Bernd Storath 10 months ago
parent
commit
cae05b5a71
  1. 10
      src/app/app.vue
  2. 23
      src/app/components/setup/validation.vue
  3. 29
      src/app/components/setup/welcome.vue
  4. 37
      src/app/layouts/Footer.vue
  5. 111
      src/app/layouts/Header.vue
  6. 146
      src/app/layouts/default.vue
  7. 31
      src/app/layouts/setup.vue
  8. 142
      src/app/pages/setup.vue
  9. 28
      src/app/pages/setup/1.vue
  10. 19
      src/app/pages/setup/2.vue
  11. 16
      src/app/pages/setup/3.vue
  12. 38
      src/app/pages/setup/4.vue
  13. 39
      src/app/pages/setup/5.vue
  14. 31
      src/app/pages/setup/migrate.vue
  15. 14
      src/app/pages/setup/success.vue
  16. 28
      src/app/stores/setup.ts
  17. 4
      src/app/utils/api.ts
  18. 5
      src/server/api/setup/account.post.ts
  19. 16
      src/server/api/setup/hostport.post.ts
  20. 2
      src/server/middleware/session.ts
  21. 23
      src/server/middleware/setup.ts
  22. 33
      src/services/database/lowdb.ts
  23. 1
      src/services/database/migrations/1.ts
  24. 4
      src/services/database/repositories/database.ts
  25. 11
      src/services/database/repositories/setup.ts

10
src/app/app.vue

@ -1,14 +1,12 @@
<template>
<NuxtLayout>
<NuxtLayout name="header" />
<ToastProvider>
<ToastProvider>
<NuxtLayout>
<NuxtPage />
<ToastViewport
class="[--viewport-padding:_25px] fixed bottom-0 right-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none"
/>
</ToastProvider>
<NuxtLayout name="footer" />
</NuxtLayout>
</NuxtLayout>
</ToastProvider>
</template>
<script setup lang="ts">

23
src/app/components/setup/validation.vue

@ -1,23 +0,0 @@
<template>
<div>
<p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupValidation') }}
</p>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['validated']);
const props = defineProps<{
next: boolean;
}>();
const next = toRef(props, 'next');
watch(next, (newVal) => {
if (newVal) {
emit('validated', null);
}
});
</script>

29
src/app/components/setup/welcome.vue

@ -1,29 +0,0 @@
<template>
<div>
<p class="text-2xl pt-8 px-8 text-center">
{{ $t('setup.messageWelcome.whatIs') }}
</p>
<p class="text-2xl pt-2 text-center text-red-600">
{{ $t('setup.messageWelcome.warning') }}
</p>
<p class="text-2xl pt-3 text-center">
{{ $t('setup.messageWelcome.next') }}
</p>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['validated']);
const props = defineProps<{
next: boolean;
}>();
const next = toRef(props, 'next');
watch(next, (newVal) => {
if (newVal) {
emit('validated', null);
}
});
</script>

37
src/app/layouts/Footer.vue

@ -1,37 +0,0 @@
<template>
<footer>
<p class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs">
<a
class="hover:underline"
target="_blank"
href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a
>
({{ globalStore.currentRelease }}) © 2021-2024 by
<a
class="hover:underline"
target="_blank"
href="https://emilenijssen.nl/?ref=wg-easy"
>Emile Nijssen</a
>
is licensed under
<a
class="hover:underline"
target="_blank"
href="http://creativecommons.org/licenses/by-nc-sa/4.0/"
>CC BY-NC-SA 4.0</a
>
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>{{ $t('donate') }}</a
>
</p>
</footer>
</template>
<script setup lang="ts">
const globalStore = useGlobalStore();
</script>

111
src/app/layouts/Header.vue

@ -1,111 +0,0 @@
<template>
<header class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6">
<div
:class="
hasOwnLogo
? 'flex justify-end'
: 'flex flex-col-reverse xxs:flex-row flex-auto items-center gap-3'
"
>
<NuxtLink to="/" class="flex-grow self-start mb-4">
<h1
v-if="!hasOwnLogo"
class="text-4xl dark:text-neutral-200 font-medium"
>
<img
src="/logo.png"
width="32"
class="inline align-middle dark:bg mr-2"
/><span class="align-middle">WireGuard</span>
</h1>
</NuxtLink>
<div class="flex items-center grow-0 gap-3 self-end xxs:self-center">
<!-- Dark / light theme -->
<button
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition"
:title="$t(`theme.${theme.preference}`)"
@click="toggleTheme"
>
<IconsSun v-if="theme.preference === 'light'" class="w-5 h-5" />
<IconsMoon
v-else-if="theme.preference === 'dark'"
class="w-5 h-5 text-neutral-400"
/>
<IconsHalfMoon
v-else
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400"
/>
</button>
<!-- Show / hide charts -->
<label
v-if="globalStore.statistics.chartType > 0"
class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group"
:title="$t('toggleCharts')"
>
<input
v-model="uiShowCharts"
type="checkbox"
value=""
class="sr-only peer"
@change="toggleCharts"
/>
<IconsChart
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition"
/>
</label>
<UiUserMenu v-if="loggedIn" />
</div>
</div>
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5" />
<div
v-if="globalStore.updateAvailable && globalStore.latestRelease"
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg"
:title="`v${globalStore.currentRelease} → v${globalStore.latestRelease.version}`"
>
<div class="container mx-auto flex flex-row flex-auto items-center">
<div class="flex-grow">
<p class="font-bold">{{ $t('updateAvailable') }}</p>
<p>{{ globalStore.latestRelease.changelog }}</p>
</div>
<a
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.latestRelease.version}`"
target="_blank"
class="p-3 rounded-md bg-white dark:bg-red-100 float-right font-sm font-semibold text-red-800 dark:text-red-600 flex-shrink-0 border-2 border-red-800 dark:border-red-600 hover:border-white dark:hover:border-red-600 hover:text-white dark:hover:text-red-100 hover:bg-red-800 dark:hover:bg-red-600 transition-all"
>
{{ $t('update') }}
</a>
</div>
</div>
</header>
</template>
<script setup lang="ts">
const globalStore = useGlobalStore();
const route = useRoute();
const hasOwnLogo = computed(
() => route.path === '/login' || route.path === '/setup'
);
const loggedIn = computed(
() => route.path !== '/login' && route.path !== '/setup'
);
const theme = useTheme();
const uiShowCharts = ref(getItem('uiShowCharts') === '1');
function toggleTheme() {
const themeCycle = {
system: 'light',
light: 'dark',
dark: 'system',
} as const;
theme.preference = themeCycle[theme.preference];
}
function toggleCharts() {
setItem('uiShowCharts', uiShowCharts.value ? '1' : '0');
}
</script>

146
src/app/layouts/default.vue

@ -0,0 +1,146 @@
<template>
<div>
<header class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6">
<div
:class="
hasOwnLogo
? 'flex justify-end'
: 'flex flex-col-reverse xxs:flex-row flex-auto items-center gap-3'
"
>
<NuxtLink to="/" class="flex-grow self-start mb-4">
<h1
v-if="!hasOwnLogo"
class="text-4xl dark:text-neutral-200 font-medium"
>
<img
src="/logo.png"
width="32"
class="inline align-middle dark:bg mr-2"
/><span class="align-middle">WireGuard</span>
</h1>
</NuxtLink>
<div class="flex items-center grow-0 gap-3 self-end xxs:self-center">
<!-- Dark / light theme -->
<button
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition"
:title="$t(`theme.${theme.preference}`)"
@click="toggleTheme"
>
<IconsSun v-if="theme.preference === 'light'" class="w-5 h-5" />
<IconsMoon
v-else-if="theme.preference === 'dark'"
class="w-5 h-5 text-neutral-400"
/>
<IconsHalfMoon
v-else
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400"
/>
</button>
<!-- Show / hide charts -->
<label
v-if="globalStore.statistics.chartType > 0"
class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group"
:title="$t('toggleCharts')"
>
<input
v-model="uiShowCharts"
type="checkbox"
value=""
class="sr-only peer"
@change="toggleCharts"
/>
<IconsChart
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition"
/>
</label>
<UiUserMenu v-if="loggedIn" />
</div>
</div>
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5" />
<div
v-if="globalStore.updateAvailable && globalStore.latestRelease"
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg"
:title="`v${globalStore.currentRelease} → v${globalStore.latestRelease.version}`"
>
<div class="container mx-auto flex flex-row flex-auto items-center">
<div class="flex-grow">
<p class="font-bold">{{ $t('updateAvailable') }}</p>
<p>{{ globalStore.latestRelease.changelog }}</p>
</div>
<a
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.latestRelease.version}`"
target="_blank"
class="p-3 rounded-md bg-white dark:bg-red-100 float-right font-sm font-semibold text-red-800 dark:text-red-600 flex-shrink-0 border-2 border-red-800 dark:border-red-600 hover:border-white dark:hover:border-red-600 hover:text-white dark:hover:text-red-100 hover:bg-red-800 dark:hover:bg-red-600 transition-all"
>
{{ $t('update') }}
</a>
</div>
</div>
</header>
<slot />
<footer>
<p class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs">
<a
class="hover:underline"
target="_blank"
href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a
>
({{ globalStore.currentRelease }}) © 2021-2024 by
<a
class="hover:underline"
target="_blank"
href="https://emilenijssen.nl/?ref=wg-easy"
>Emile Nijssen</a
>
is licensed under
<a
class="hover:underline"
target="_blank"
href="http://creativecommons.org/licenses/by-nc-sa/4.0/"
>CC BY-NC-SA 4.0</a
>
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>{{ $t('donate') }}</a
>
</p>
</footer>
</div>
</template>
<script setup lang="ts">
const globalStore = useGlobalStore();
const route = useRoute();
const hasOwnLogo = computed(
() => route.path === '/login' || route.path === '/setup'
);
const loggedIn = computed(
() => route.path !== '/login' && route.path !== '/setup'
);
const theme = useTheme();
const uiShowCharts = ref(getItem('uiShowCharts') === '1');
function toggleTheme() {
const themeCycle = {
system: 'light',
light: 'dark',
dark: 'system',
} as const;
theme.preference = themeCycle[theme.preference];
}
function toggleCharts() {
setItem('uiShowCharts', uiShowCharts.value ? '1' : '0');
}
</script>

31
src/app/layouts/setup.vue

@ -0,0 +1,31 @@
<template>
<main class="container mx-auto px-4">
<UiBanner />
<Panel>
<PanelBody class="md:w-[70%] lg:w-[60%] mx-auto mt-10 p-4">
<h2 class="mt-8 mb-16 text-3xl font-medium">
{{ $t('setup.welcome') }}
</h2>
<slot />
<div class="mt-12 flex">
<UiStepProgress
:step="setupStore.step"
:total-steps="setupStore.totalSteps"
/>
</div>
</PanelBody>
</Panel>
<ErrorToast
v-if="setupStore.error"
:title="setupStore.error.title"
:message="setupStore.error.message"
/>
</main>
</template>
<script lang="ts" setup>
const setupStore = useSetupStore();
</script>

142
src/app/pages/setup.vue

@ -1,142 +0,0 @@
<template>
<main class="container mx-auto px-4">
<UiBanner />
<Panel>
<PanelBody class="md:w-[70%] lg:w-[60%] mx-auto mt-10 p-4">
<h2 class="mt-8 mb-16 text-3xl font-medium">
{{ $t('setup.welcome') }}
</h2>
<SetupLang
v-if="step === 1"
:next="nextStep"
@validated="handleValidatedStep"
/>
<SetupWelcome
v-if="step === 2"
:next="nextStep"
@validated="handleValidatedStep"
/>
<SetupAdminUser
v-if="step === 3"
:next="nextStep"
@validated="handleValidatedStep"
/>
<SetupHostPort
v-if="step === 4"
:next="nextStep"
@validated="handleValidatedStep"
/>
<SetupMigration
v-if="step === 5"
:next="nextStep"
@validated="handleValidatedStep"
/>
<SetupValidation
v-if="step === 6"
:next="nextStep"
@validated="handleValidatedStep"
/>
<div class="flex justify-between items-center mt-12">
<IconsArrowLeftCircle
:class="[
'size-12',
step === 1
? 'text-gray-500'
: 'text-red-800 hover:text-red-600 dark:text-white dark:hover:text-red-800',
]"
@click="decreaseStep"
/>
<UiStepProgress :step="step" :total-steps="totalSteps" />
<IconsArrowRightCircle
v-if="step < totalSteps"
class="size-12 text-red-800 hover:text-red-600 dark:text-white dark:hover:text-red-800"
@click="increaseStep"
/>
<IconsCheckCircle
v-if="step == totalSteps"
class="size-12 text-red-800 hover:text-red-600 dark:text-white dark:hover:text-red-800"
@click="increaseStep"
/>
</div>
</PanelBody>
</Panel>
<ErrorToast
v-if="setupError"
:title="setupError.title"
:message="setupError.message"
/>
</main>
</template>
<script setup lang="ts">
type SetupError = {
title: string;
message: string;
};
/* STEP MANAGEMENT */
const step = ref(1);
const totalSteps = ref(6);
const stepValide = ref<number[]>([]);
const setupError = ref<null | SetupError>(null);
const nextStep = ref(false);
watch(setupError, (newVal) => {
if (newVal) {
const id = setTimeout(() => {
setupError.value = null;
clearTimeout(id);
}, 8000);
}
});
async function increaseStep() {
if (step.value === totalSteps.value) {
navigateTo('/login');
}
if (stepValide.value.includes(step.value)) {
nextStep.value = false;
step.value += 1;
return;
}
// handleValidatedStep()
nextStep.value = true;
}
function decreaseStep() {
if (step.value > 1) {
nextStep.value = false;
step.value -= 1;
}
}
function handleValidatedStep(error: null | SetupError) {
nextStep.value = false;
if (error) {
setupError.value = error;
return;
}
if (!error) {
if (step.value === 3 || step.value === 4) {
// if new admin user has been created, allow to skip this step if user returns to the previous steps
stepValide.value.push(step.value);
}
if (step.value < totalSteps.value) {
step.value += 1;
}
}
}
</script>

28
src/app/components/setup/lang.vue → src/app/pages/setup/1.vue

@ -6,40 +6,34 @@
<div class="flex justify-center mb-8">
<UiChooseLang @update:lang="handleEventUpdateLang" />
</div>
<div><BaseButton @click="updateLang">Continue</BaseButton></div>
</div>
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
const globalStore = useGlobalStore();
const { t, locale, setLocale } = useI18n();
const emit = defineEmits(['validated']);
const props = defineProps<{
next: boolean;
}>();
const next = toRef(props, 'next');
watch(next, async (newVal) => {
if (newVal) {
await updateLang();
}
definePageMeta({
layout: 'setup',
});
const { t, locale, setLocale } = useI18n();
function handleEventUpdateLang(value: string) {
setLocale(value);
}
const setupStore = useSetupStore();
setupStore.setStep(1);
const globalStore = useGlobalStore();
const router = useRouter();
async function updateLang() {
try {
await globalStore.updateLang(locale.value);
emit('validated', null);
router.push('/setup/2');
} catch (error) {
if (error instanceof FetchError) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: error.data.message,
});

19
src/app/pages/setup/2.vue

@ -0,0 +1,19 @@
<template>
<div>
<p class="text-lg p-8 text-center">
{{ 'Do you have a existing Setup?' }}
</p>
<div class="flex justify-center mb-8">
<NuxtLink to="/setup/3"><BaseButton>No</BaseButton></NuxtLink>
<NuxtLink to="/setup/migrate"><BaseButton>Yes</BaseButton></NuxtLink>
</div>
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(2);
</script>

16
src/app/pages/setup/3.vue

@ -0,0 +1,16 @@
<template>
<div>
<p class="text-2xl pt-8 px-8 text-center">
{{ $t('setup.messageWelcome.whatIs') }}
</p>
<NuxtLink to="/setup/4"><BaseButton>Continue</BaseButton></NuxtLink>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(3);
</script>

38
src/app/components/setup/adminUser.vue → src/app/pages/setup/4.vue

@ -12,7 +12,7 @@
form="newAccount"
type="text"
autocomplete="username"
:class="inputClass"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-200 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0"
/>
</div>
<div>
@ -23,7 +23,7 @@
form="newAccount"
type="password"
autocomplete="new-password"
:class="inputClass"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-200 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0"
/>
</div>
<div>
@ -36,33 +36,21 @@
class="ml-2"
/>
</div>
<BaseButton @click="newAccount">Create Account</BaseButton>
</div>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { FetchError } from 'ofetch';
const setupStore = useSetupStore();
const authStore = useAuthStore();
const { t } = useI18n();
const inputClass =
'px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-200 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0';
const emit = defineEmits(['validated']);
const props = defineProps<{
next: boolean;
}>();
const next = toRef(props, 'next');
watch(next, async (newVal) => {
if (newVal) {
await newAccount();
}
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(4);
const router = useRouter();
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
@ -70,7 +58,7 @@ const accept = ref<boolean>(true);
async function newAccount() {
try {
if (!username.value || !password.value) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
@ -78,12 +66,10 @@ async function newAccount() {
}
await setupStore.signup(username.value, password.value, accept.value);
// the next step need authentication
await authStore.login(username.value, password.value, false);
emit('validated', null);
await router.push('/setup/5');
} catch (error) {
if (error instanceof FetchError) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: error.data.message,
});

39
src/app/components/setup/hostPort.vue → src/app/pages/setup/5.vue

@ -9,7 +9,7 @@
id="host"
v-model="host"
type="text"
:class="inputClass"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-200 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0"
placeholder="vpn.example.com"
/>
</div>
@ -21,42 +21,31 @@
type="number"
:min="1"
:max="65535"
:class="inputClass"
placeholder="51820"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-200 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0"
/>
</div>
<BaseButton @click="updateHostPort">Continue</BaseButton>
</div>
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
const setupStore = useSetupStore();
const { t } = useI18n();
const inputClass =
'px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-200 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0';
const emit = defineEmits(['validated']);
const props = defineProps<{
next: boolean;
}>();
const next = toRef(props, 'next');
watch(next, async (newVal) => {
if (newVal) {
await updateHostPort();
}
definePageMeta({
layout: 'setup',
});
const { t } = useI18n();
const setupStore = useSetupStore();
setupStore.setStep(5);
const router = useRouter();
const host = ref<null | string>(null);
const port = ref<null | number>(null);
const port = ref<number>(51820);
async function updateHostPort() {
if (!host.value || !port.value) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
@ -65,10 +54,10 @@ async function updateHostPort() {
try {
await setupStore.updateHostPort(host.value, port.value);
emit('validated', null);
await router.push('/setup/success');
} catch (error) {
if (error instanceof FetchError) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: error.data.message,
});

31
src/app/components/setup/migration.vue → src/app/pages/setup/migrate.vue

@ -7,30 +7,23 @@
<Label for="migration">{{ $t('setup.migration') }}</Label>
<input id="migration" type="file" @change="onChangeFile" />
</div>
<BaseButton @click="sendFile">Upload</BaseButton>
</div>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { FetchError } from 'ofetch';
const setupStore = useSetupStore();
const { t } = useI18n();
const emit = defineEmits(['validated']);
definePageMeta({
layout: 'setup',
});
const props = defineProps<{
next: boolean;
}>();
const { t } = useI18n();
const next = toRef(props, 'next');
const setupStore = useSetupStore();
setupStore.setStep(5);
const backupFile = ref<null | File>(null);
watch(next, async (newVal) => {
if (newVal) {
await sendFile();
}
});
function onChangeFile(evt: Event) {
const target = evt.target as HTMLInputElement;
const file = target.files?.[0];
@ -43,9 +36,11 @@ function onChangeFile(evt: Event) {
}
}
const router = useRouter();
async function sendFile() {
if (!backupFile.value) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
@ -56,10 +51,10 @@ async function sendFile() {
const content = await readFileContent(backupFile.value);
await setupStore.runMigration(content);
emit('validated', null);
await router.push('/setup/success');
} catch (error) {
if (error instanceof FetchError) {
emit('validated', {
setupStore.handleError({
title: t('setup.requirements'),
message: error.data.message,
});

14
src/app/pages/setup/success.vue

@ -0,0 +1,14 @@
<template>
<div>
<p>Setup successfully</p>
<NuxtLink to="/login"><BaseButton>Login</BaseButton></NuxtLink>
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(6);
</script>

28
src/app/stores/setup.ts

@ -25,5 +25,31 @@ export const useSetupStore = defineStore('Setup', () => {
return response.success;
}
return { signup, updateHostPort, runMigration };
type SetupError = {
title: string;
message: string;
};
const error = ref<null | SetupError>(null);
function handleError(e: SetupError) {
error.value = e;
}
const step = ref(1);
const totalSteps = ref(6);
function setStep(i: number) {
step.value = i;
}
return {
signup,
updateHostPort,
runMigration,
error,
handleError,
step,
totalSteps,
setStep,
};
});

4
src/app/utils/api.ts

@ -131,14 +131,14 @@ class API {
password: string;
accept: boolean;
}) {
return $fetch('/api/account/setup', {
return $fetch('/api/setup/account', {
method: 'post',
body: { username, password, accept },
});
}
async setupHostPort({ host, port }: { host: string; port: number }) {
return $fetch('/api/admin/hostport', {
return $fetch('/api/setup/hostport', {
method: 'post',
body: { host, port },
});

5
src/server/api/account/setup.post.ts → src/server/api/setup/account.post.ts

@ -3,13 +3,14 @@ export default defineEventHandler(async (event) => {
event,
validateZod(passwordSetupType, event)
);
const users = await Database.user.findAll();
if (users.length !== 0) {
const setupDone = await Database.setup.done();
if (setupDone) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state',
});
}
await Database.user.create(username, password);
await Database.setup.set(5);
return { success: true };
});

16
src/server/api/setup/hostport.post.ts

@ -0,0 +1,16 @@
export default defineEventHandler(async (event) => {
const { host, port } = await readValidatedBody(
event,
validateZod(hostPortType, event)
);
const setupDone = await Database.setup.done();
if (setupDone) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state',
});
}
await Database.system.updateClientsHostPort(host, port);
await Database.setup.set('success');
return { success: true };
});

2
src/server/middleware/session.ts

@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
// Handle api routes
if (
!url.pathname.startsWith('/api/') ||
url.pathname === '/api/account/setup' ||
url.pathname.startsWith('/api/setup/') ||
url.pathname === '/api/session' ||
url.pathname === '/api/lang' ||
url.pathname === '/api/release' ||

23
src/server/middleware/setup.ts

@ -7,22 +7,21 @@ export default defineEventHandler(async (event) => {
return;
}
const users = await Database.user.findAll();
if (users.length === 0) {
// If not setup
if (url.pathname.startsWith('/setup')) {
return;
const setupDone = await Database.setup.done();
if (!setupDone) {
const parsedSetup = url.pathname.match(/\/setup\/(\d)/);
if (!parsedSetup) {
return sendRedirect(event, `/setup/1`, 302);
}
if (url.pathname.startsWith('/api/')) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid State',
});
const [_, currentSetup] = parsedSetup;
const step = await Database.setup.get();
if (step.toString() === currentSetup) {
return;
}
return sendRedirect(event, '/setup', 302);
return sendRedirect(event, `/setup/${step}`, 302);
} else {
// If already set up
if (!url.pathname.startsWith('/setup')) {
if (!url.pathname.startsWith('/setup/')) {
return;
}
return sendRedirect(event, '/login', 302);

33
src/services/database/lowdb.ts

@ -27,10 +27,35 @@ import {
type Lang,
type Statistics,
} from './repositories/system';
import { SetupRepository, type Steps } from './repositories/setup';
const DEBUG = debug('LowDB');
export class LowDBSystem extends SystemRepository {
export class LowDBSetup extends SetupRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
this.#db = db;
}
async done() {
if (this.#db.data.setup === 'success') {
return true;
}
return false;
}
async get() {
return this.#db.data.setup;
}
async set(step: Steps) {
this.#db.update((v) => {
v.setup = step;
});
}
}
class LowDBSystem extends SystemRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
@ -87,7 +112,7 @@ export class LowDBSystem extends SystemRepository {
}
}
export class LowDBUser extends UserRepository {
class LowDBUser extends UserRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
@ -155,7 +180,7 @@ export class LowDBUser extends UserRepository {
}
}
export class LowDBClient extends ClientRepository {
class LowDBClient extends ClientRepository {
#db: Low<Database>;
constructor(db: Low<Database>) {
super();
@ -252,6 +277,7 @@ export class LowDBClient extends ClientRepository {
export default class LowDB extends DatabaseProvider {
#db: Low<Database>;
setup: LowDBSetup;
system: LowDBSystem;
user: LowDBUser;
client: LowDBClient;
@ -259,6 +285,7 @@ export default class LowDB extends DatabaseProvider {
private constructor(db: Low<Database>) {
super();
this.#db = db;
this.setup = new LowDBSetup(this.#db);
this.system = new LowDBSystem(this.#db);
this.user = new LowDBUser(this.#db);
this.client = new LowDBClient(this.#db);

1
src/services/database/migrations/1.ts

@ -15,6 +15,7 @@ export async function run1(db: Low<Database>) {
const database: Database = {
migrations: [],
setup: 1,
system: {
general: {
sessionTimeout: 3600, // 1 hour

4
src/services/database/repositories/database.ts

@ -1,10 +1,12 @@
import type { ClientRepository, Client } from './client';
import type { SetupRepository, Steps } from './setup';
import type { System, SystemRepository } from './system';
import type { User, UserRepository } from './user';
// Represent data structure
export type Database = {
migrations: string[];
setup: Steps;
system: System;
users: User[];
clients: Record<string, Client>;
@ -12,6 +14,7 @@ export type Database = {
export const DEFAULT_DATABASE: Database = {
migrations: [],
setup: 1,
system: null as never,
users: [],
clients: {},
@ -37,6 +40,7 @@ export abstract class DatabaseProvider {
*/
abstract disconnect(): Promise<void>;
abstract setup: SetupRepository;
abstract system: SystemRepository;
abstract user: UserRepository;
abstract client: ClientRepository;

11
src/services/database/repositories/setup.ts

@ -0,0 +1,11 @@
export type Steps = 1 | 2 | 3 | 4 | 5 | 'success';
/**
* Interface for setup-related database operations.
* This interface provides methods for managing setup data.
*/
export abstract class SetupRepository {
abstract done(): Promise<boolean>;
abstract get(): Promise<Steps>;
abstract set(step: Steps): Promise<void>;
}
Loading…
Cancel
Save