Browse Source

update: setup page

- add: host/port section
- i18n: french
- fix: fallback translation
pull/1397/head
tetuaoro 7 months ago
committed by Bernd Storath
parent
commit
0f38697b6b
  1. 95
      src/app/pages/setup.vue
  2. 10
      src/app/stores/auth.ts
  3. 21
      src/app/stores/setup.ts
  4. 15
      src/app/utils/api.ts
  5. 1
      src/i18n.config.ts
  6. 18
      src/locales/en.json
  7. 18
      src/locales/fr.json
  8. 8
      src/server/api/wireguard/clients/hostport.post.ts
  9. 16
      src/server/utils/types.ts
  10. 8
      src/services/database/lowdb.ts
  11. 1
      src/services/database/repositories/system.ts

95
src/app/pages/setup.vue

@ -8,28 +8,32 @@
</h2> </h2>
<div v-if="step === 1"> <div v-if="step === 1">
<p class="text-lg p-8 text-center">{{ $t('setup.msgStepOne') }}</p> <p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupLanguage') }}
</p>
<div class="flex justify-center mb-8"> <div class="flex justify-center mb-8">
<UiChooseLang :lang="lang" @update:lang="handleEventUpdateLang" /> <UiChooseLang :lang="lang" @update:lang="handleEventUpdateLang" />
</div> </div>
</div> </div>
<div v-if="step === 2"> <div v-if="step === 2">
<p class="text-lg p-8 text-center">{{ $t('setup.msgStepTwo') }}</p> <p class="text-lg p-8 text-center">
{{ $t('setup.messageSetupCreateAdminUser') }}
</p>
<div> <div>
<label for="username" class="inline-block py-2">{{ <Label for="username" class="inline-block py-2">{{
$t('username') $t('username')
}}</label> }}</Label>
<input <input
id="username" id="username"
v-model="username" v-model="username"
form="form-step-two" form="form-create-admin-user"
type="text" type="text"
name="username" name="username"
autocomplete="username" autocomplete="username"
autofocus autofocus
: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 focus:outline-0 focus:ring-0" :class="inputClass"
/> />
</div> </div>
<div> <div>
@ -39,12 +43,12 @@
<input <input
id="password" id="password"
v-model="password" v-model="password"
form="form-step-two" form="form-create-admin-user"
type="password" type="password"
name="password" name="password"
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 focus:outline-0 focus:ring-0" :class="inputClass"
/> />
</div> </div>
<div> <div>
@ -53,11 +57,41 @@
}}</Label> }}</Label>
<input id="accept" v-model="accept" type="checkbox" name="accept" /> <input id="accept" v-model="accept" type="checkbox" name="accept" />
</div> </div>
<form id="form-step-two"></form> <form id="form-create-admin-user"></form>
</div> </div>
<div v-if="step === 3"> <div v-if="step === 3">
<p class="text-lg p-8 text-center">Host/Port section</p> <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>
<div v-if="step === 4"> <div v-if="step === 4">
@ -104,8 +138,12 @@ import { FetchError } from 'ofetch';
const { t, locale, setLocale } = useI18n(); const { t, locale, setLocale } = useI18n();
const authStore = useAuthStore(); const authStore = useAuthStore();
const setupStore = useSetupStore();
const globalStore = useGlobalStore(); 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 = { type SetupError = {
title: string; title: string;
message: string; message: string;
@ -113,10 +151,15 @@ type SetupError = {
const lang = ref(locale.value); const lang = ref(locale.value);
/* STEP CREATE ADMIN USER MODELS */
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 accept = ref<boolean>(true); 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 step = ref(1);
const stepInvalide = ref<number[]>([]); const stepInvalide = ref<number[]>([]);
const setupError = ref<null | SetupError>(null); const setupError = ref<null | SetupError>(null);
@ -132,7 +175,6 @@ watch(setupError, (value) => {
function handleEventUpdateLang(value: string) { function handleEventUpdateLang(value: string) {
lang.value = value; lang.value = value;
// TODO: if the translation does not exist, it shows the key instead of default (en)
setLocale(lang.value); setLocale(lang.value);
} }
@ -149,15 +191,19 @@ async function increaseStep() {
} }
if (step.value === 3) { if (step.value === 3) {
/* host/port */ await updateHostPort();
stepInvalide.value.push(3);
} }
if (step.value === 4) { if (step.value === 4) {
/* migration */ /* migration */
stepInvalide.value.push(4);
} }
if (step.value === 5) { if (step.value === 5) {
/* validation/welcome */ /* validation/welcome */
navigateTo('/login');
stepInvalide.value.push(5);
} }
if (step.value < 5) step.value += 1; if (step.value < 5) step.value += 1;
@ -198,14 +244,25 @@ async function newAccount() {
if (!username.value || !password.value) return; if (!username.value || !password.value) return;
try { try {
const res = await authStore.signup( await setupStore.signup(username.value, password.value, accept.value);
username.value, // the next step need authentication
password.value, await authStore.login(username.value, password.value, false);
accept.value } catch (error) {
); if (error instanceof FetchError) {
if (res) { setupError.value = {
navigateTo('/login'); 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) { } catch (error) {
if (error instanceof FetchError) { if (error instanceof FetchError) {
setupError.value = { setupError.value = {

10
src/app/stores/auth.ts

@ -6,14 +6,6 @@ export const useAuthStore = defineStore('Auth', () => {
email: string | null; email: string | null;
}>(); }>();
/**
* @throws if unsuccessful
*/
async function signup(username: string, password: string, accept: boolean) {
const response = await api.setupAccount({ username, password, accept });
return response.success;
}
/** /**
* @throws if unsuccessful * @throws if unsuccessful
*/ */
@ -36,5 +28,5 @@ export const useAuthStore = defineStore('Auth', () => {
userData.value = response.value; userData.value = response.value;
} }
return { userData, login, logout, update, signup }; return { userData, login, logout, update };
}); });

21
src/app/stores/setup.ts

@ -0,0 +1,21 @@
import { defineStore } from 'pinia';
export const useSetupStore = defineStore('Setup', () => {
/**
* @throws if unsuccessful
*/
async function signup(username: string, password: string, accept: boolean) {
const response = await api.setupAdminUser({ username, password, accept });
return response.success;
}
/**
* @throws if unsuccessful
*/
async function updateHostPort(host: string, port: number) {
const response = await api.setupHostPort({ host, port });
return response.success;
}
return { signup, updateHostPort };
});

15
src/app/utils/api.ts

@ -115,7 +115,14 @@ class API {
}); });
} }
async setupAccount({ async updateLang({ lang }: { lang: string }) {
return $fetch('/api/lang', {
method: 'post',
body: { lang },
});
}
async setupAdminUser({
username, username,
password, password,
accept, accept,
@ -130,10 +137,10 @@ class API {
}); });
} }
async updateLang({ lang }: { lang: string }) { async setupHostPort({ host, port }: { host: string; port: number }) {
return $fetch('/api/lang', { return $fetch('/api/wireguard/clients/hostport', {
method: 'post', method: 'post',
body: { lang }, body: { host, port },
}); });
} }
} }

1
src/i18n.config.ts

@ -45,6 +45,7 @@ const LOCALES = [
export { LOCALES }; export { LOCALES };
export default defineI18nConfig(() => ({ export default defineI18nConfig(() => ({
fallbackLocale: 'en',
legacy: false, legacy: false,
locale: 'en', locale: 'en',
messages: { messages: {

18
src/locales/en.json

@ -14,15 +14,20 @@
"confirmPassword": "Confirm Password", "confirmPassword": "Confirm Password",
"setup": { "setup": {
"welcome": "Welcome to your first setup of wg-easy !", "welcome": "Welcome to your first setup of wg-easy !",
"msgStepOne": "Please choose a default language.", "messageSetupLanguage": "Please choose a default language.",
"msgStepTwo": "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.",
"chooseLang": "Select a language...", "chooseLang": "Select a language...",
"newPassword": "New Password", "newPassword": "New Password",
"accept": "I accept the condition", "accept": "I accept the condition",
"submitBtn": "Create admin account", "submitBtn": "Create admin account",
"usernamePlaceholder": "Administrator", "usernamePlaceholder": "Administrator",
"passwordPlaceholder": "Strong password", "passwordPlaceholder": "Strong password",
"requirements": "Setup requirements" "requirements": "Setup requirements",
"host": "Host",
"hostPlaceholder": "wg-easy.example.com",
"port": "Port",
"portPlaceholder": "443"
}, },
"zod": { "zod": {
"id": "Client ID must be a valid UUID", "id": "Client ID must be a valid UUID",
@ -51,7 +56,12 @@
"stat": "statistics must be a valid object", "stat": "statistics must be a valid object",
"statBool": "enabled must be a valid boolean", "statBool": "enabled must be a valid boolean",
"statNumber": "chartType must be a valid number", "statNumber": "chartType must be a valid number",
"body": "Body must be a valid object" "body": "Body must be a valid object",
"host": "Host must be a valid string",
"hostMin": "Host must contain at least 1 character",
"port": "Port must be a valid number",
"portMin": "Port must be at least 1",
"portMax": "Port must be at most 65535"
}, },
"name": "Name", "name": "Name",
"username": "Username", "username": "Username",

18
src/locales/fr.json

@ -14,15 +14,20 @@
"confirmPassword": "Confirmer le mot de passe", "confirmPassword": "Confirmer le mot de passe",
"setup": { "setup": {
"welcome": "Bienvenue dans votre première configuration de wg-easy !", "welcome": "Bienvenue dans votre première configuration de wg-easy !",
"msgStepOne": "Sélectionner votre langue.", "messageSetupLanguage": "Sélectionner votre langue.",
"msgStepTwo": "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.",
"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",
"submitBtn": "Créer un compte administrateur", "submitBtn": "Créer un compte administrateur",
"usernamePlaceholder": "Administrateur", "usernamePlaceholder": "Administrateur",
"passwordPlaceholder": "Mot de passe sapau", "passwordPlaceholder": "Mot de passe sapau",
"requirements": "Conditions de configuration" "requirements": "Conditions de configuration",
"host": "Hôte",
"hostPlaceholder": "wg-easy.example.com",
"port": "Port",
"portPlaceholder": "443"
}, },
"zod": { "zod": {
"id": "L'ID du client doit être un UUID valide", "id": "L'ID du client doit être un UUID valide",
@ -51,7 +56,12 @@
"stat": "Les statistiques doivent être un objet valide", "stat": "Les statistiques doivent être un objet valide",
"statBool": "Le paramètre « activé » doit être un booléen valide", "statBool": "Le paramètre « activé » doit être un booléen valide",
"statNumber": "Le type de graphique doit être un nombre valide", "statNumber": "Le type de graphique doit être un nombre valide",
"body": "Le corps doit être un objet valide" "body": "Le corps doit être un objet valide",
"host": "L'hôte doit être une chaîne valide",
"hostMin": "L'hôte doit contenir au moins 1 caractère",
"port": "Le port doit être un nombre valide",
"portMin": "Le port doit être au moins 1",
"portMax": "Le port doit être au maximum 65535"
}, },
"name": "Nom", "name": "Nom",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",

8
src/server/api/wireguard/clients/hostport.post.ts

@ -0,0 +1,8 @@
export default defineEventHandler(async (event) => {
const { host, port } = await readValidatedBody(
event,
validateZod(hostPortType, event)
);
await Database.system.updateClientsHostPort(host, port);
return { success: true };
});

16
src/server/utils/types.ts

@ -73,14 +73,30 @@ const statistics = z.object(
{ message: 'zod.stat' } // i18n key { message: 'zod.stat' } // i18n key
); );
const host = z
.string({ message: 'zod.host' })
.min(1, 'zod.hostMin')
.pipe(safeStringRefine);
const port = z
.number({ message: 'zod.port' })
.min(1, 'zod.portMin')
.max(65535, 'zod.portMax');
const objectMessage = 'zod.body'; // i18n key const objectMessage = 'zod.body'; // i18n key
const langs = LOCALES.map((lang) => lang.value); const langs = LOCALES.map((lang) => lang.value);
const lang = z.enum(['', ...langs]); const lang = z.enum(['', ...langs]);
export const langType = z.object({ export const langType = z.object({
lang: lang, lang: lang,
}); });
export const hostPortType = z.object({
host: host,
port: port,
});
export const clientIdType = z.object( export const clientIdType = z.object(
{ {
clientId: id, clientId: id,

8
src/services/database/lowdb.ts

@ -77,6 +77,14 @@ export class LowDBSystem extends SystemRepository {
v.system.general.lang = lang; v.system.general.lang = lang;
}); });
} }
async updateClientsHostPort(host: string, port: number): Promise<void> {
DEBUG('Update Clients Host and Port endpoint');
this.#db.update((v) => {
v.system.userConfig.host = host;
v.system.userConfig.port = port;
});
}
} }
export class LowDBUser extends UserRepository { export class LowDBUser extends UserRepository {

1
src/services/database/repositories/system.ts

@ -108,4 +108,5 @@ export abstract class SystemRepository {
abstract updateFeatures(features: Record<string, Feature>): Promise<void>; abstract updateFeatures(features: Record<string, Feature>): Promise<void>;
abstract updateStatistics(statistics: Statistics): Promise<void>; abstract updateStatistics(statistics: Statistics): Promise<void>;
abstract updateLang(lang: Lang): Promise<void>; abstract updateLang(lang: Lang): Promise<void>;
abstract updateClientsHostPort(host: string, port: number): Promise<void>;
} }

Loading…
Cancel
Save