Browse Source

update: setup page

- redirect to login when the setup is done
- allow user to return to previous steps
- prompt error message
- i18n french
pull/1397/head
tetuaoro 7 months ago
committed by Bernd Storath
parent
commit
6d0192e7f8
  1. 15
      src/app/components/icons/CheckCircle.vue
  2. 31
      src/app/components/setup/createAdminUser.vue
  3. 277
      src/app/components/setup/original.save
  4. 19
      src/app/components/setup/updateHostPort.vue
  5. 40
      src/app/components/setup/validation.vue
  6. 76
      src/app/pages/setup.vue
  7. 2
      src/locales/en.json
  8. 2
      src/locales/fr.json

15
src/app/components/icons/CheckCircle.vue

@ -0,0 +1,15 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</template>

31
src/app/components/setup/createAdminUser.vue

@ -3,17 +3,30 @@
<p class="text-lg p-8 text-center"> <p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupCreateAdminUser') }} {{ $t('setup.messageSetupCreateAdminUser') }}
</p> </p>
<form id="newAccount"></form>
<div> <div>
<Label for="username">{{ $t('username') }}</Label> <Label for="username">{{ $t('username') }}</Label>
<input v-model="username" type="text" :class="inputClass" /> <input
v-model="username"
form="newAccount"
type="text"
autocomplete="username"
:class="inputClass"
/>
</div> </div>
<div> <div>
<Label for="password">{{ $t('setup.newPassword') }}</Label> <Label for="password">{{ $t('setup.newPassword') }}</Label>
<input v-model="password" type="password" :class="inputClass" /> <input
v-model="password"
form="newAccount"
type="password"
autocomplete="new-password"
:class="inputClass"
/>
</div> </div>
<div> <div>
<Label for="accept">{{ $t('setup.accept') }}</Label> <Label for="accept">{{ $t('setup.accept') }}</Label>
<input v-model="accept" type="checkbox" /> <input v-model="accept" form="newAccount" type="checkbox" />
</div> </div>
</div> </div>
</template> </template>
@ -26,7 +39,7 @@ const authStore = useAuthStore();
const { t } = useI18n(); const { t } = useI18n();
const inputClass = const inputClass =
'px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 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'; '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 emit = defineEmits(['validated']);
@ -47,9 +60,15 @@ const password = ref<null | string>(null);
const accept = ref<boolean>(true); const accept = ref<boolean>(true);
async function newAccount() { async function newAccount() {
if (!username.value || !password.value) return;
try { try {
if (!username.value || !password.value) {
emit('validated', {
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
await setupStore.signup(username.value, password.value, accept.value); await setupStore.signup(username.value, password.value, accept.value);
// the next step need authentication // the next step need authentication
await authStore.login(username.value, password.value, false); await authStore.login(username.value, password.value, false);

277
src/app/components/setup/original.save

@ -1,277 +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>
<div v-if="step === 1">
<p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupLanguage') }}
</p>
<div class="flex justify-center mb-8">
<UiChooseLang :lang="lang" @update:lang="handleEventUpdateLang" />
</div>
</div>
<div v-if="step === 2">
<p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupCreateAdminUser') }}
</p>
<div>
<Label for="username" class="inline-block py-2">{{
$t('username')
}}</Label>
<input
id="username"
v-model="username"
form="form-create-admin-user"
type="text"
name="username"
autocomplete="username"
autofocus
:placeholder="$t('setup.usernamePlaceholder')"
:class="inputClass"
/>
</div>
<div>
<Label for="password" class="inline-block py-2">{{
$t('setup.newPassword')
}}</Label>
<input
id="password"
v-model="password"
form="form-create-admin-user"
type="password"
name="password"
autocomplete="new-password"
:placeholder="$t('setup.passwordPlaceholder')"
:class="inputClass"
/>
</div>
<div>
<Label for="accept" class="inline-block my-4 mr-4">{{
$t('setup.accept')
}}</Label>
<input id="accept" v-model="accept" type="checkbox" name="accept" />
</div>
<form id="form-create-admin-user"></form>
</div>
<div v-if="step === 3">
<p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupHostPort') }}
</p>
<div>
<Label for="host">{{ $t('setup.host') }}</Label>
<input
id="host"
v-model="host"
form="form-set-host-port"
type="text"
name="host"
autofocus
:placeholder="t('setup.hostPlaceholder')"
:class="inputClass"
/>
</div>
<div>
<Label for="port">{{ $t('setup.port') }}</Label>
<input
id="port"
v-model="port"
form="form-set-host-port"
type="number"
name="port"
min="1"
max="65535"
:placeholder="t('setup.portPlaceholder')"
:class="inputClass"
/>
</div>
<form id="form-set-host-port"></form>
</div>
<div v-if="step === 4">
<p class="text-lg p-8 text-center">Migration section</p>
</div>
<div v-if="step === 5">
<p class="text-lg p-8 text-center">Validation section</p>
</div>
<div class="flex justify-between items-center">
<IconsArrowLeftCircle
:class="[
'size-12',
step === 1 || towardStepValide()
? 'text-gray-500'
: 'text-red-800 dark:text-white',
]"
@click="decreaseStep"
/>
<UiStepProgress :step="step" />
<IconsArrowRightCircle
:class="[
'size-12',
step === 4 ? 'text-gray-500' : 'text-red-800 dark:text-white',
]"
@click="increaseStep"
/>
</div>
</PanelBody>
</Panel>
<ErrorToast
v-if="setupError"
:title="setupError.title"
:message="setupError.message"
:duration="12000"
/>
</main>
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
const { t, locale, setLocale } = useI18n();
const authStore = useAuthStore();
const setupStore = useSetupStore();
const globalStore = useGlobalStore();
const inputClass =
'px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 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';
type SetupError = {
title: string;
message: string;
};
const lang = ref(locale.value);
/* STEP CREATE ADMIN USER MODELS */
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
/* STEP SET HOST & PORT MODELS */
const host = ref<null | string>(null);
const port = ref<null | number>(null);
const step = ref(1);
const stepInvalide = ref<number[]>([]);
const setupError = ref<null | SetupError>(null);
// TODO: improve error handling
watch(setupError, (value) => {
if (value) {
setTimeout(() => {
setupError.value = null;
}, 13000);
}
});
function handleEventUpdateLang(value: string) {
lang.value = value;
setLocale(lang.value);
}
async function increaseStep() {
try {
if (step.value === 1) {
await updateLang();
stepInvalide.value.push(1);
}
if (step.value === 2) {
await newAccount();
stepInvalide.value.push(2);
}
if (step.value === 3) {
await updateHostPort();
stepInvalide.value.push(3);
}
if (step.value === 4) {
/* migration */
stepInvalide.value.push(4);
}
if (step.value === 5) {
/* validation/welcome */
navigateTo('/login');
stepInvalide.value.push(5);
}
if (step.value < 5) step.value += 1;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
/* throw in functions */
}
}
// TODO: improve while user reload the page, might use server check
/* Check if previous steps are invalide (mean successful executed). */
function towardStepValide() {
return stepInvalide.value.includes(step.value - 1);
}
function decreaseStep() {
if (towardStepValide()) return;
if (step.value > 1) step.value -= 1;
}
async function updateLang() {
try {
await globalStore.updateLang(lang.value);
} catch (error) {
if (error instanceof FetchError) {
setupError.value = {
title: t('setup.requirements'),
message: error.data.message,
};
}
// increaseStep fn
throw error;
}
}
async function newAccount() {
if (!username.value || !password.value) return;
try {
await setupStore.signup(username.value, password.value, accept.value);
// the next step need authentication
await authStore.login(username.value, password.value, false);
} catch (error) {
if (error instanceof FetchError) {
setupError.value = {
title: t('setup.requirements'),
message: error.data.message,
};
}
// increaseStep fn
throw error;
}
}
async function updateHostPort() {
if (!host.value || !port.value) return;
try {
await setupStore.updateHostPort(host.value, port.value);
} catch (error) {
if (error instanceof FetchError) {
setupError.value = {
title: t('setup.requirements'),
message: error.data.message,
};
}
// increaseStep fn
throw error;
}
}
</script>

19
src/app/components/setup/updateHostPort.vue

@ -5,7 +5,12 @@
</p> </p>
<div> <div>
<Label for="host">{{ $t('setup.host') }}</Label> <Label for="host">{{ $t('setup.host') }}</Label>
<input v-model="host" type="text" :class="inputClass" /> <input
v-model="host"
type="text"
:class="inputClass"
placeholder="vpn.example.com"
/>
</div> </div>
<div> <div>
<Label for="port">{{ $t('setup.port') }}</Label> <Label for="port">{{ $t('setup.port') }}</Label>
@ -15,6 +20,7 @@
:min="1" :min="1"
:max="65535" :max="65535"
:class="inputClass" :class="inputClass"
placeholder="51820"
/> />
</div> </div>
</div> </div>
@ -27,7 +33,7 @@ const setupStore = useSetupStore();
const { t } = useI18n(); const { t } = useI18n();
const inputClass = const inputClass =
'px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 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'; '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 emit = defineEmits(['validated']);
@ -47,7 +53,14 @@ const host = ref<null | string>(null);
const port = ref<null | number>(null); const port = ref<null | number>(null);
async function updateHostPort() { async function updateHostPort() {
if (!host.value || !port.value) return; if (!host.value || !port.value) {
emit('validated', {
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
try { try {
await setupStore.updateHostPort(host.value, port.value); await setupStore.updateHostPort(host.value, port.value);
emit('validated', null); emit('validated', null);

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

@ -0,0 +1,40 @@
<template>
<div>
<p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupValidation') }}
</p>
</div>
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
const { t } = useI18n();
const emit = defineEmits(['validated']);
const props = defineProps<{
next: boolean;
}>();
const next = toRef(props, 'next');
watch(next, async (newVal) => {
if (newVal) {
await runNext();
}
});
async function runNext() {
try {
emit('validated', null);
} catch (error) {
if (error instanceof FetchError) {
emit('validated', {
title: t('setup.requirements'),
message: error.data.message,
});
}
}
}
</script>

76
src/app/pages/setup.vue

@ -25,30 +25,31 @@
@validated="handleValidatedStep" @validated="handleValidatedStep"
/> />
<div v-if="step === 4"> <SetupValidation
<p class="text-lg p-8 text-center">Migration section</p> v-if="step === 4"
</div> :next="nextStep"
@validated="handleValidatedStep"
<div v-if="step === 5"> />
<p class="text-lg p-8 text-center">Validation section</p>
</div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center mt-12">
<IconsArrowLeftCircle <IconsArrowLeftCircle
:class="[ :class="[
'size-12', 'size-12',
step === 1 || towardStepValide() step === 1
? 'text-gray-500' ? 'text-gray-500'
: 'text-red-800 dark:text-white', : 'text-red-800 hover:text-red-600 dark:text-white dark:hover:text-red-800',
]" ]"
@click="decreaseStep" @click="decreaseStep"
/> />
<UiStepProgress :step="step" /> <UiStepProgress :step="step" :total-steps="totalSteps" />
<IconsArrowRightCircle <IconsArrowRightCircle
:class="[ v-if="step < totalSteps"
'size-12', class="size-12 text-red-800 hover:text-red-600 dark:text-white dark:hover:text-red-800"
step === 4 ? 'text-gray-500' : 'text-red-800 dark:text-white', @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" @click="increaseStep"
/> />
</div> </div>
@ -71,32 +72,57 @@ type SetupError = {
/* STEP MANAGEMENT */ /* STEP MANAGEMENT */
const step = ref(1); const step = ref(1);
const stepInvalide = ref<number[]>([]); const totalSteps = ref(4);
const stepValide = ref<number[]>([]);
const setupError = ref<null | SetupError>(null); const setupError = ref<null | SetupError>(null);
const nextStep = ref(false); const nextStep = ref(false);
watch(setupError, (newVal) => {
if (newVal) {
const id = setTimeout(() => {
setupError.value = null;
clearTimeout(id);
}, 8000);
}
});
async function increaseStep() { 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; nextStep.value = true;
} }
function decreaseStep() { function decreaseStep() {
if (stepInvalide.value.includes(step.value - 1)) return; if (step.value > 1) {
if (step.value > 1) step.value -= 1; nextStep.value = false;
} step.value -= 1;
}
function towardStepValide() {
return stepInvalide.value.includes(step.value - 1);
} }
function handleValidatedStep(error: null | SetupError) { function handleValidatedStep(error: null | SetupError) {
nextStep.value = false;
if (error) { if (error) {
setupError.value = error; setupError.value = error;
return;
} }
if (!error) { if (!error) {
nextStep.value = false; if (step.value === 2 || step.value === 3) {
if (step.value < 5) { // if new admin user has been created, allow to skip this step if user returns to the previous steps
stepInvalide.value.push(step.value); stepValide.value.push(step.value);
}
if (step.value < totalSteps.value) {
step.value += 1; step.value += 1;
} }
} }

2
src/locales/en.json

@ -17,6 +17,8 @@
"messageSetupLanguage": "Please choose a default language.", "messageSetupLanguage": "Please choose a default language.",
"messageSetupCreateAdminUser": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.", "messageSetupCreateAdminUser": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.",
"messageSetupHostPort": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.", "messageSetupHostPort": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.",
"messageSetupValidation": "Welcome to wg-easy ! The easiest way to run WireGuard VPN and Web-based Admin UI.",
"emptyFields": "The fields are required",
"chooseLang": "Select a language...", "chooseLang": "Select a language...",
"newPassword": "New Password", "newPassword": "New Password",
"accept": "I accept the condition", "accept": "I accept the condition",

2
src/locales/fr.json

@ -17,6 +17,8 @@
"messageSetupLanguage": "Sélectionner votre langue.", "messageSetupLanguage": "Sélectionner votre langue.",
"messageSetupCreateAdminUser": "Veuillez renseigner votre nom d'utilisateur et votre mot de passe. Ces informations seront utilisées pour vous connecter à votre page d'administration.", "messageSetupCreateAdminUser": "Veuillez renseigner votre nom d'utilisateur et votre mot de passe. Ces informations seront utilisées pour vous connecter à votre page d'administration.",
"messageSetupHostPort": "Veuillez entrer les informations de l'hôte et du port. Cela sera utilisé pour la configuration du client lors de la configuration de WireGuard sur leurs appareils.", "messageSetupHostPort": "Veuillez entrer les informations de l'hôte et du port. Cela sera utilisé pour la configuration du client lors de la configuration de WireGuard sur leurs appareils.",
"messageSetupValidation": "Bienvenue sur wg-easy ! La meilleur application pour administrer son serveur VPN WireGuard.",
"emptyFields": "Des champs sont requis",
"chooseLang": "Choisir une langue...", "chooseLang": "Choisir une langue...",
"newPassword": "Nouveau mot de passe", "newPassword": "Nouveau mot de passe",
"accept": "J'accepte les conditions d'utilisation", "accept": "J'accepte les conditions d'utilisation",

Loading…
Cancel
Save