Browse Source

update: setup page error handle

- use fetch error data to provide error message
- use translation to provider error message
pull/1397/head
tetuaoro 7 months ago
committed by Bernd Storath
parent
commit
6f41abe652
  1. 27
      src/app/components/error/Toast.vue
  2. 53
      src/app/pages/setup.vue
  3. 38
      src/locales/en.json
  4. 40
      src/locales/fr.json
  5. 2
      src/server/api/account/setup.post.ts
  6. 86
      src/server/utils/types.ts

27
src/app/components/error/Toast.vue

@ -0,0 +1,27 @@
<script setup lang="ts">
const props = defineProps<{
title: string;
message: string;
duration?: number;
}>();
const open = ref(true);
const autoCloseToast = props.duration ? Number(props.duration) : 5000; // 5 seconds
onMounted(() => {
setTimeout(() => {
open.value = false;
}, autoCloseToast);
});
</script>
<template>
<ToastRoot
v-model:open="open"
class="bg-red-800 rounded-md p-2 text-neutral-200"
>
<ToastTitle class="mb-4 font-medium text-lg">{{ title }} </ToastTitle>
<ToastDescription as-child>{{ message }}</ToastDescription>
</ToastRoot>
</template>

53
src/app/pages/setup.vue

@ -28,16 +28,12 @@
autocomplete="username" autocomplete="username"
:placeholder="$t('setup.usernamePlaceholder')" :placeholder="$t('setup.usernamePlaceholder')"
class="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 outline-none" class="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 outline-none"
required="true"
/> />
<small v-if="errorCU" class="text-danger">{{
$t('setup.usernameCondition')
}}</small>
</div> </div>
<div> <div>
<label for="password" class="inline-block py-2">{{ <Label for="password" class="inline-block py-2">{{
$t('setup.newPassword') $t('setup.newPassword')
}}</label> }}</Label>
<input <input
id="password" id="password"
v-model="password" v-model="password"
@ -46,17 +42,13 @@
autocomplete="new-password" autocomplete="new-password"
:placeholder="$t('setup.passwordPlaceholder')" :placeholder="$t('setup.passwordPlaceholder')"
class="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 outline-none" class="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 outline-none"
required="true"
/> />
<small v-if="errorPWD" class="text-danger">{{
$t('setup.passwordCondition')
}}</small>
</div> </div>
<div> <div>
<label for="accept" class="inline-block my-4 mr-4">{{ <Label for="accept" class="inline-block my-4 mr-4">{{
$t('setup.accept') $t('setup.accept')
}}</label> }}</Label>
<input id="accept" type="checkbox" name="accept" required="true" /> <input id="accept" type="checkbox" name="accept" />
</div> </div>
<button <button
type="submit" type="submit"
@ -74,16 +66,38 @@
</button> </button>
</form> </form>
</div> </div>
<ErrorToast
v-if="setupError"
:title="setupError.title"
:message="setupError.message"
:duration="12000"
/>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FetchError } from 'ofetch';
const { t } = useI18n();
const username = ref<null | string>(null); const username = ref<null | string>(null);
const password = ref<null | string>(null); const password = ref<null | string>(null);
const authStore = useAuthStore(); const authStore = useAuthStore();
const errorCU = ref(false); type SetupError = {
const errorPWD = ref(false); title: string;
message: string;
};
const setupError = ref<null | SetupError>(null);
watch(setupError, (value) => {
if (value) {
setTimeout(() => {
setupError.value = null;
}, 6000);
}
});
async function newAccount() { async function newAccount() {
if (!username.value || !password.value) return; if (!username.value || !password.value) return;
@ -94,10 +108,11 @@ async function newAccount() {
navigateTo('/login'); navigateTo('/login');
} }
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof FetchError) {
// TODO: replace alert with actual ui error message setupError.value = {
// TODO: also use errorCU & errorPWD to show prompt error title: t('setup.requirements'),
alert(error.message || error.toString()); message: error.data.message,
};
} }
} }
} }

38
src/locales/en.json

@ -14,10 +14,37 @@
"newPassword": "New Password", "newPassword": "New Password",
"accept": "I accept the condition", "accept": "I accept the condition",
"submitBtn": "Create admin account", "submitBtn": "Create admin account",
"usernameCondition": "The username must contain at least 8 characters.",
"passwordCondition": "The password must contain at least 12 characters, including 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 special character.",
"usernamePlaceholder": "Administrator", "usernamePlaceholder": "Administrator",
"passwordPlaceholder": "Strong password" "passwordPlaceholder": "Strong password",
"requirements": "Setup requirements"
},
"zod": {
"id": "Client ID must be a valid UUID",
"address4": "IPv4 Address must be a valid string",
"name": "Name must be a valid string",
"nameMin": "Name must be at least 1 Character",
"file": "File must be a valid string",
"username": "Username must be a valid string",
"usernameMin": "Username must be at least 8 Characters",
"password": "Password must be a valid string",
"passwordMin": "Password must be at least 12 Characters",
"passwordUppercase": "Password must have at least 1 uppercase letter",
"passwordLowercase": "Password must have at least 1 lowercase letter",
"passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character",
"remember": "Remember must be a valid boolean",
"expireDate": "expiredDate must be a valid string",
"expireDateMin": "expiredDate must be at least 1 Character",
"otl": "oneTimeLink must be a valid string",
"otlMin": "oneTimeLink must be at least 1 Character",
"features": "key must be a valid string",
"ftBool": "enabled must be a valid boolean",
"ftObj": "value must be a valid object",
"ftObj2": "features must be a valid record",
"stat": "statistics must be a valid object",
"statBool": "enabled must be a valid boolean",
"statNumber": "chartType must be a valid number",
"body": "Body must be a valid object"
}, },
"name": "Name", "name": "Name",
"username": "Username", "username": "Username",
@ -61,5 +88,8 @@
"ExpireDate": "Expire Date", "ExpireDate": "Expire Date",
"Permanent": "Permanent", "Permanent": "Permanent",
"OneTimeLink": "Generate short one time link", "OneTimeLink": "Generate short one time link",
"errorInit": "Initialization failed." "errorInit": "Initialization failed.",
"error": {
"clear": "Clear"
}
} }

40
src/locales/fr.json

@ -7,7 +7,7 @@
"save": "Enregistrer", "save": "Enregistrer",
"updatePassword": "Changer le mot de passe", "updatePassword": "Changer le mot de passe",
"currentPassword": "Mot de passe actuel", "currentPassword": "Mot de passe actuel",
"confirmPassword": "Confirmer le mot de passse", "confirmPassword": "Confirmer le mot de passe",
"setup": { "setup": {
"welcome": "Bienvenue à la page de configuration de votre wg-easy !", "welcome": "Bienvenue à la page de configuration de votre wg-easy !",
"msg": "Veuillez renseigner votre nom d'utilisateur et votre mot de passe. Ces informations seront utilisées pour vous connecter à votre page d'administration.", "msg": "Veuillez renseigner votre nom d'utilisateur et votre mot de passe. Ces informations seront utilisées pour vous connecter à votre page d'administration.",
@ -17,15 +17,44 @@
"usernameCondition": "Le nom d'utilisateur doit contenir au moins 8 caractères.", "usernameCondition": "Le nom d'utilisateur doit contenir au moins 8 caractères.",
"passwordCondition": "Le mot de passe doit contenir au moins 12 caractères dont 1 majuscule, 1 minuscule, 1 chiffre et 1 caractère spécial.", "passwordCondition": "Le mot de passe doit contenir au moins 12 caractères dont 1 majuscule, 1 minuscule, 1 chiffre et 1 caractère spécial.",
"usernamePlaceholder": "Administrateur", "usernamePlaceholder": "Administrateur",
"passwordPlaceholder": "Mot de passe sapau" "passwordPlaceholder": "Mot de passe sapau",
"requirements": "Conditions de configuration"
},
"zod": {
"id": "L'ID du client doit être un UUID valide",
"address4": "L'adresse IPv4 doit être une chaîne valide",
"name": "Le nom doit être une chaîne valide",
"nameMin": "Le nom doit contenir au moins 1 caractère",
"file": "Le fichier doit être une chaîne valide",
"username": "Le nom d'utilisateur doit être une chaîne valide",
"usernameMin": "Le nom d'utilisateur doit contenir au moins 8 caractères",
"password": "Le mot de passe doit être une chaîne valide",
"passwordMin": "Le mot de passe doit contenir au moins 12 caractères",
"passwordUppercase": "Le mot de passe doit contenir au moins 1 lettre majuscule",
"passwordLowercase": "Le mot de passe doit contenir au moins 1 lettre minuscule",
"passwordNumber": "Le mot de passe doit contenir au moins 1 chiffre",
"passwordSpecial": "Le mot de passe doit contenir au moins 1 caractère spécial",
"remember": "La case « se souvenir de moi » doit être un booléen valide",
"expireDate": "La date d'expiration doit être une chaîne valide",
"expireDateMin": "La date d'expiration doit contenir au moins 1 caractère",
"otl": "Le lien à usage unique doit être une chaîne valide",
"otlMin": "Le lien à usage unique doit contenir au moins 1 caractère",
"features": "La clé doit être une chaîne valide",
"ftBool": "Le paramètre « activé » doit être un booléen valide",
"ftObj": "La valeur doit être un objet valide",
"ftObj2": "Les fonctionnalités doivent être un enregistrement valide",
"stat": "Les statistiques doivent être un objet valide",
"statBool": "Le paramètre « activé » doit être un booléen valide",
"statNumber": "Le type de graphique doit être un nombre valide",
"body": "Le corps doit être un objet valide"
}, },
"name": "Nom", "name": "Nom",
"username": "Nom d'utilisateur",
"password": "Mot de passe", "password": "Mot de passe",
"signIn": "Se Connecter", "signIn": "Se Connecter",
"logout": "Se déconnecter", "logout": "Se déconnecter",
"updateAvailable": "Une mise à jour est disponible !", "updateAvailable": "Une mise à jour est disponible !",
"update": "Mise à jour", "update": "Mise à jour",
"clients": "Clients",
"new": "Nouveau", "new": "Nouveau",
"deleteClient": "Supprimer ce client", "deleteClient": "Supprimer ce client",
"deleteDialog1": "Êtes-vous sûr de vouloir supprimer", "deleteDialog1": "Êtes-vous sûr de vouloir supprimer",
@ -61,5 +90,8 @@
"ExpireDate": "Date d'expiration", "ExpireDate": "Date d'expiration",
"Permanent": "Permanent", "Permanent": "Permanent",
"OneTimeLink": "Générer un lien court à usage unique", "OneTimeLink": "Générer un lien court à usage unique",
"errorInit": "Échec de l'initialisation." "errorInit": "Échec de l'initialisation.",
"error": {
"clear": "Effacer"
}
} }

2
src/server/api/account/setup.post.ts

@ -1,7 +1,7 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { username, password } = await readValidatedBody( const { username, password } = await readValidatedBody(
event, event,
validateZod(passwordType) validateZod(passwordType, event)
); );
const users = await Database.user.findAll(); const users = await Database.user.findAll();
if (users.length !== 0) { if (users.length !== 0) {

86
src/server/utils/types.ts

@ -1,5 +1,6 @@
import type { ZodSchema } from 'zod'; import type { ZodSchema } from 'zod';
import { z, ZodError } from 'zod'; import { z, ZodError } from 'zod';
import type { H3Event, EventHandlerRequest } from 'h3';
// TODO: use i18n for messages // TODO: use i18n for messages
@ -10,74 +11,64 @@ const safeStringRefine = z
{ message: 'String is malformed' } { message: 'String is malformed' }
); );
const id = z const id = z.string().uuid('zod.id').pipe(safeStringRefine);
.string()
.uuid('Client ID must be a valid UUID')
.pipe(safeStringRefine);
const address4 = z const address4 = z.string({ message: 'zod.address4' }).pipe(safeStringRefine);
.string({ message: 'IPv4 Address must be a valid string' })
.pipe(safeStringRefine);
const name = z const name = z
.string({ message: 'Name must be a valid string' }) .string({ message: 'zod.name' })
.min(1, 'Name must be at least 1 Character') .min(1, 'zod.nameMin')
.pipe(safeStringRefine); .pipe(safeStringRefine);
const file = z const file = z.string({ message: 'zod.file' }).pipe(safeStringRefine);
.string({ message: 'File must be a valid string' })
.pipe(safeStringRefine);
const username = z const username = z
.string({ message: 'Username must be a valid string' }) .string({ message: 'zod.username' })
.min(8, 'Username must be at least 8 Characters') .min(8, 'zod.usernameMin') // i18n key
.pipe(safeStringRefine); .pipe(safeStringRefine);
const password = z const password = z
.string({ message: 'Password must be a valid string' }) .string({ message: 'zod.password' })
.min(12, 'Password must be at least 12 Characters') .min(12, 'zod.passwordMin') // i18n key
.regex(/[A-Z]/, 'Password must have at least 1 uppercase letter') .regex(/[A-Z]/, 'zod.passwordUppercase') // i18n key
.regex(/[a-z]/, 'Password must have at least 1 lowercase letter') .regex(/[a-z]/, 'zod.passwordLowercase') // i18n key
.regex(/\d/, 'Password must have at least 1 number') .regex(/\d/, 'zod.passwordNumber') // i18n key
.regex( .regex(/[!@#$%^&*(),.?":{}|<>]/, 'zod.passwordSpecial') // i18n key
/[!@#$%^&*(),.?":{}|<>]/,
'Password must have at least 1 special character'
)
.pipe(safeStringRefine); .pipe(safeStringRefine);
const remember = z.boolean({ message: 'Remember must be a valid boolean' }); const remember = z.boolean({ message: 'zod.remember' }); // i18n key
const expireDate = z const expireDate = z
.string({ message: 'expiredDate must be a valid string' }) .string({ message: 'zod.expireDate' }) // i18n key
.min(1, 'expiredDate must be at least 1 Character') .min(1, 'zod.expireDateMin') // i18n key
.pipe(safeStringRefine) .pipe(safeStringRefine)
.nullable(); .nullable();
const oneTimeLink = z const oneTimeLink = z
.string({ message: 'oneTimeLink must be a valid string' }) .string({ message: 'zod.otl' }) // i18n key
.min(1, 'oneTimeLink must be at least 1 Character') .min(1, 'zod.otlMin') // i18n key
.pipe(safeStringRefine); .pipe(safeStringRefine);
const features = z.record( const features = z.record(
z.string({ message: 'key must be a valid string' }), z.string({ message: 'zod.features' }), // i18n key
z.object( z.object(
{ {
enabled: z.boolean({ message: 'enabled must be a valid boolean' }), enabled: z.boolean({ message: 'zod.ftBool' }), // i18n key
}, },
{ message: 'value must be a valid object' } { message: 'zod.ftObj' } // i18n key
), ),
{ message: 'features must be a valid record' } { message: 'zod.ftObj2' } // i18n key
); );
const statistics = z.object( const statistics = z.object(
{ {
enabled: z.boolean({ message: 'enabled must be a valid boolean' }), enabled: z.boolean({ message: 'zod.statBool' }), // i18n key
chartType: z.number({ message: 'chartType must be a valid number' }), chartType: z.number({ message: 'zod.statNumber' }), // i18n key
}, },
{ message: 'statistics must be a valid object' } { message: 'zod.stat' } // i18n key
); );
const objectMessage = 'Body must be a valid object'; const objectMessage = 'zod.body'; // i18n key
export const clientIdType = z.object( export const clientIdType = z.object(
{ {
@ -160,14 +151,33 @@ export const statisticsType = z.object(
{ message: objectMessage } { message: objectMessage }
); );
export function validateZod<T>(schema: ZodSchema<T>) { export function validateZod<T>(
schema: ZodSchema<T>,
event?: H3Event<EventHandlerRequest>
) {
return async (data: unknown) => { return async (data: unknown) => {
let t: null | ((key: string) => string) = null;
if (event) {
t = await useTranslation(event);
}
try { try {
return await schema.parseAsync(data); return await schema.parseAsync(data);
} catch (error) { } catch (error) {
let message = 'Unexpected Error'; let message = 'Unexpected Error';
if (error instanceof ZodError) { if (error instanceof ZodError) {
message = error.issues.map((v) => v.message).join('; '); message = error.issues
.map((v) => {
let m = v.message;
if (t) {
m = t(m); // m key else v.message
}
return m;
})
.join('; ');
} }
throw new Error(message); throw new Error(message);
} }

Loading…
Cancel
Save