Browse Source

improve

pull/1666/head
Bernd Storath 6 months ago
parent
commit
dd0cb370d7
  1. 2
      src/app/components/ClientCard/Config.vue
  2. 2
      src/app/components/ClientCard/ExpireDate.vue
  3. 2
      src/app/components/ClientCard/LastSeen.vue
  4. 2
      src/app/components/ClientCard/Name.vue
  5. 2
      src/app/components/ClientCard/OneTimeLinkBtn.vue
  6. 2
      src/app/components/ClientCard/QRCode.vue
  7. 5
      src/app/components/ClientCard/Switch.vue
  8. 40
      src/app/components/Clients/CreateDialog.vue
  9. 6
      src/app/components/Clients/Empty.vue
  10. 2
      src/app/components/Clients/New.vue
  11. 49
      src/app/components/Clients/Sort.vue
  12. 0
      src/app/components/base/Container.vue
  13. 40
      src/app/components/base/Toast.vue
  14. 17
      src/app/components/form/NullTextField.vue
  15. 15
      src/app/components/form/TextField.vue
  16. 2
      src/app/components/header/ChartToggle.vue
  17. 4
      src/app/components/header/Update.vue
  18. 12
      src/app/components/panel/head/Title.vue
  19. 45
      src/app/components/ui/ChooseLang.vue
  20. 2
      src/app/components/ui/Footer.vue
  21. 27
      src/app/components/ui/UserMenu.vue
  22. 30
      src/app/composables/useSubmit.ts
  23. 1
      src/app/pages/admin.vue
  24. 6
      src/app/pages/admin/config.vue
  25. 2
      src/app/pages/admin/hooks.vue
  26. 2
      src/app/pages/admin/index.vue
  27. 12
      src/app/pages/admin/interface.vue
  28. 14
      src/app/pages/clients/[id].vue
  29. 1
      src/app/pages/index.vue
  30. 54
      src/app/pages/login.vue
  31. 16
      src/app/pages/me.vue
  32. 7
      src/app/pages/setup/1.vue
  33. 102
      src/app/pages/setup/2.vue
  34. 11
      src/app/pages/setup/3.vue
  35. 86
      src/app/pages/setup/4.vue
  36. 52
      src/app/pages/setup/migrate.vue
  37. 7
      src/app/pages/setup/success.vue
  38. 23
      src/app/stores/auth.ts
  39. 10
      src/app/stores/global.ts
  40. 36
      src/app/stores/setup.ts
  41. 93
      src/i18n/locales/en.json
  42. 7
      src/server/database/repositories/user/types.ts

2
src/app/components/ClientCard/Config.vue

@ -3,7 +3,7 @@
:href="'/api/client/' + client.id + '/configuration'"
download
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('downloadConfig')"
:title="$t('client.downloadConfig')"
>
<IconsDownload class="w-5" />
</a>

2
src/app/components/ClientCard/ExpireDate.vue

@ -12,7 +12,7 @@ defineProps<{ client: LocalClient }>();
const { t, locale } = useI18n();
function expiredDateFormat(value: string | null) {
if (value === null) return t('Permanent');
if (value === null) return t('client.permanent');
const dateTime = new Date(value);
return dateTime.toLocaleDateString(locale.value, {
year: 'numeric',

2
src/app/components/ClientCard/LastSeen.vue

@ -2,7 +2,7 @@
<span
v-if="client.latestHandshakeAt"
class="whitespace-nowrap text-gray-400 dark:text-neutral-500"
:title="$t('lastSeen') + $d(new Date(client.latestHandshakeAt))"
:title="$t('client.lastSeen') + $d(new Date(client.latestHandshakeAt))"
>
· {{ timeago(new Date(client.latestHandshakeAt)) }}
</span>

2
src/app/components/ClientCard/Name.vue

@ -1,7 +1,7 @@
<template>
<div
class="text-sm text-gray-700 md:text-base dark:text-neutral-200"
:title="$t('createdOn') + $d(new Date(client.createdAt))"
:title="$t('client.createdOn') + $d(new Date(client.createdAt))"
>
<span class="border-b-2 border-t-2 border-transparent">
{{ client.name }}

2
src/app/components/ClientCard/OneTimeLinkBtn.vue

@ -1,7 +1,7 @@
<template>
<button
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('OneTimeLink')"
:title="$t('client.otlDesc')"
@click="showOneTimeLink(client)"
>
<svg

2
src/app/components/ClientCard/QRCode.vue

@ -2,7 +2,7 @@
<ClientsQRCodeDialog :qr-code="`./api/client/${client.id}/qrcode.svg`">
<button
class="rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('showQR')"
:title="$t('client.showQR')"
>
<IconsQRCode class="w-5" />
</button>

5
src/app/components/ClientCard/Switch.vue

@ -1,7 +1,9 @@
<template>
<BaseSwitch
v-model="enabled"
:title="client.enabled ? $t('disableClient') : $t('enableClient')"
:title="
client.enabled ? $t('client.disableClient') : $t('client.enableClient')
"
@click="toggleClient"
/>
</template>
@ -16,6 +18,7 @@ const enabled = ref(props.client.enabled);
const clientsStore = useClientsStore();
async function toggleClient() {
// Improve
try {
if (props.client.enabled) {
await $fetch(`/api/client/${props.client.id}/disable`, {

40
src/app/components/Clients/CreateDialog.vue

@ -28,36 +28,26 @@
</template>
<script lang="ts" setup>
import { FetchError } from 'ofetch';
const name = ref<string>('');
const expiresAt = ref<string | null>(null);
const toast = useToast();
const clientsStore = useClientsStore();
const { t } = useI18n();
defineProps<{ triggerClass?: string }>();
async function createClient() {
try {
await $fetch('/api/client', {
method: 'post',
body: { name: name.value, expiresAt: expiresAt.value },
});
toast.showToast({
type: 'success',
title: 'Success',
message: 'Client created',
});
await clientsStore.refresh();
} catch (e) {
if (e instanceof FetchError) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.data.message,
});
}
// TODO: handle errors better
}
function createClient() {
return _createClient({ name: name.value, expiresAt: expiresAt.value });
}
const _createClient = useSubmit(
'/api/client',
{
method: 'post',
},
{
revert: () => clientsStore.refresh(),
successMsg: t('client.created'),
}
);
</script>

6
src/app/components/Clients/Empty.vue

@ -1,10 +1,10 @@
<template>
<p class="m-10 text-center text-sm text-gray-400 dark:text-neutral-400">
{{ $t('noClients') }}<br /><br />
{{ $t('client.empty') }}<br /><br />
<ClientsCreateDialog>
<BaseButton>
<BaseButton as="span">
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm">{{ $t('new') }}</span>
<span class="text-sm">{{ $t('client.new') }}</span>
</BaseButton>
</ClientsCreateDialog>
</p>

2
src/app/components/Clients/New.vue

@ -2,7 +2,7 @@
<ClientsCreateDialog>
<BaseButton as="span">
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden">{{ $t('new') }}</span>
<span class="text-sm max-md:hidden">{{ $t('client.newShort') }}</span>
</BaseButton>
</ClientsCreateDialog>
</template>

49
src/app/components/Clients/Sort.vue

@ -1,52 +1,15 @@
<template>
<button
class="inline-flex items-center border-2 border-gray-100 px-4 py-2 text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white max-md:border-x-0 md:rounded dark:border-neutral-600 dark:text-neutral-200"
@click="toggleSort"
>
<svg
<BaseButton @click="toggleSort">
<IconsArrowDown
v-if="globalStore.sortClient === true"
inline
class="w-4 md:mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
d="M12 19.75C11.9015 19.7504 11.8038 19.7312 11.7128 19.6934C11.6218 19.6557 11.5392 19.6001 11.47 19.53L5.47 13.53C5.33752 13.3878 5.2654 13.1997 5.26882 13.0054C5.27225 12.8111 5.35096 12.6258 5.48838 12.4883C5.62579 12.3509 5.81118 12.2722 6.00548 12.2688C6.19978 12.2654 6.38782 12.3375 6.53 12.47L12 17.94L17.47 12.47C17.6122 12.3375 17.8002 12.2654 17.9945 12.2688C18.1888 12.2722 18.3742 12.3509 18.5116 12.4883C18.649 12.6258 18.7277 12.8111 18.7312 13.0054C18.7346 13.1997 18.6625 13.3878 18.53 13.53L12.53 19.53C12.4608 19.6001 12.3782 19.6557 12.2872 19.6934C12.1962 19.7312 12.0985 19.7504 12 19.75Z"
fill="#000000"
/>
<path
d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z"
fill="#000000"
/>
</svg>
<svg
v-if="globalStore.sortClient === false"
inline
class="w-4 md:mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
d="M18 11.75C17.9015 11.7505 17.8038 11.7313 17.7128 11.6935C17.6218 11.6557 17.5392 11.6001 17.47 11.53L12 6.06001L6.53 11.53C6.38782 11.6625 6.19978 11.7346 6.00548 11.7312C5.81118 11.7278 5.62579 11.649 5.48838 11.5116C5.35096 11.3742 5.27225 11.1888 5.26882 10.9945C5.2654 10.8002 5.33752 10.6122 5.47 10.47L11.47 4.47001C11.6106 4.32956 11.8012 4.25067 12 4.25067C12.1987 4.25067 12.3894 4.32956 12.53 4.47001L18.53 10.47C18.6705 10.6106 18.7493 10.8013 18.7493 11C18.7493 11.1988 18.6705 11.3894 18.53 11.53C18.4608 11.6001 18.3782 11.6557 18.2872 11.6935C18.1962 11.7313 18.0985 11.7505 18 11.75Z"
fill="#000000"
/>
<path
d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z"
fill="#000000"
/>
</svg>
<span class="text-sm max-md:hidden">{{ $t('sort') }}</span>
</button>
/>
<IconsArrowUp v-else class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden"> {{ $t('client.sort') }}</span>
</BaseButton>
</template>
<script setup lang="ts">
// TODO: improve
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();

0
src/app/components/base/Container.vue

40
src/app/components/base/Toast.vue

@ -1,23 +1,3 @@
<script setup lang="ts">
import {
ToastAction,
ToastClose,
ToastDescription,
ToastRoot,
ToastTitle,
} from 'radix-vue';
defineExpose({
publish,
});
const count = reactive<ToastParams[]>([]);
function publish(e: ToastParams) {
count.push({ type: e.type, title: e.title, message: e.message });
}
</script>
<template>
<ToastRoot
v-for="(e, i) in count"
@ -44,3 +24,23 @@ function publish(e: ToastParams) {
</ToastClose>
</ToastRoot>
</template>
<script setup lang="ts">
import {
ToastAction,
ToastClose,
ToastDescription,
ToastRoot,
ToastTitle,
} from 'radix-vue';
defineExpose({
publish,
});
const count = reactive<ToastParams[]>([]);
function publish(e: ToastParams) {
count.push({ type: e.type, title: e.title, message: e.message });
}
</script>

17
src/app/components/form/NullTextField.vue

@ -7,11 +7,24 @@
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseInput :id="id" v-model.trim="data" :name="id" type="text" />
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="text"
:autcomplete="autocomplete"
:placeholder="placeholder"
/>
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
defineProps<{
id: string;
label: string;
description?: string;
autocomplete?: string;
placeholder?: string;
}>();
const data = defineModel<string | null>({
set(value) {

15
src/app/components/form/TextField.vue

@ -7,11 +7,22 @@
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseInput :id="id" v-model.trim="data" :name="id" type="text" />
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="text"
:autcomplete="autocomplete"
/>
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
defineProps<{
id: string;
label: string;
description?: string;
autocomplete?: string;
}>();
const data = defineModel<string>();
</script>

2
src/app/components/header/ChartToggle.vue

@ -2,7 +2,7 @@
<Toggle
v-model:pressed="globalStore.uiShowCharts"
class="group inline-flex h-8 w-8 cursor-pointer items-center justify-center whitespace-nowrap rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
:title="$t('toggleCharts')"
:title="$t('layout.toggleCharts')"
@update:pressed="globalStore.toggleCharts"
>
<IconsChart

4
src/app/components/header/Update.vue

@ -6,7 +6,7 @@
>
<div class="container mx-auto flex flex-auto flex-row items-center">
<div class="flex-grow">
<p class="font-bold">{{ $t('updateAvailable') }}</p>
<p class="font-bold">{{ $t('update.updateAvailable') }}</p>
<p>{{ globalStore.latestRelease.changelog }}</p>
</div>
@ -15,7 +15,7 @@
target="_blank"
class="font-sm float-right flex-shrink-0 rounded-md border-2 border-red-800 bg-white p-3 font-semibold text-red-800 transition-all hover:border-white hover:bg-red-800 hover:text-white dark:border-red-600 dark:bg-red-100 dark:text-red-600 dark:hover:border-red-600 dark:hover:bg-red-600 dark:hover:text-red-100"
>
{{ $t('update') }}
{{ $t('update.update') }}
</a>
</div>
</div>

12
src/app/components/panel/head/Title.vue

@ -1,11 +1,11 @@
<script setup lang="ts">
const { text } = defineProps<{
text: string;
}>();
</script>
<template>
<h2 class="flex-1 text-2xl font-medium">
{{ text }}
</h2>
</template>
<script setup lang="ts">
const { text } = defineProps<{
text: string;
}>();
</script>

45
src/app/components/ui/ChooseLang.vue

@ -1,45 +0,0 @@
<template>
<SelectRoot v-model="langProxy" :default-value="locale">
<SelectTrigger
class="inline-flex h-[35px] min-w-[160px] items-center justify-between gap-[5px] rounded px-[15px] text-[13px] leading-none dark:bg-neutral-500 dark:text-white"
aria-label="Customize language"
>
<SelectValue :placeholder="$t('setup.chooseLang')" />
<IconsArrowDown class="size-4" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="min-w-[160px] rounded bg-white dark:bg-neutral-500"
:side-offset="5"
>
<SelectViewport class="p-[5px]">
<SelectItem
v-for="(option, index) in langs"
:key="index"
:value="option.code"
class="text-grass11 relative flex h-[25px] items-center rounded-[3px] pl-[25px] pr-[35px] text-[13px] leading-none hover:bg-red-800 hover:text-white dark:text-white"
>
<SelectItemText>
{{ option.name }}
</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
<script setup lang="ts">
// TODO: improve
const { locales, locale, setLocale } = useI18n();
const langProxy = ref(locale);
watchEffect(() => {
setLocale(langProxy.value);
});
const langs = locales.value.sort((a, b) => a.code.localeCompare(b.code));
</script>

2
src/app/components/ui/Footer.vue

@ -26,7 +26,7 @@
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>{{ $t('donate') }}</a
>{{ $t('layout.donate') }}</a
>
</p>
</footer>

27
src/app/components/ui/UserMenu.vue

@ -52,10 +52,10 @@
<DropdownMenuItem>
<button
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white"
@click.prevent="logout"
@click.prevent="submit"
>
<IconsLogout class="h-5" />
{{ $t('logout') }}
{{ $t('general.logout') }}
</button>
</DropdownMenuItem>
</DropdownMenuContent>
@ -67,16 +67,21 @@
const authStore = useAuthStore();
const toggleState = ref(false);
async function logout() {
try {
await authStore.logout();
navigateTo('/login');
} catch (err) {
if (err instanceof Error) {
// TODO: better ui
alert(err.message || err.toString());
}
const _submit = useSubmit(
'/api/session',
{
method: 'delete',
},
{
revert: async () => {
await navigateTo('/login');
},
noSuccessToast: true,
}
);
function submit() {
return _submit(undefined);
}
const fallbackName = computed(() => {

30
src/app/composables/useSubmit.ts

@ -1,12 +1,19 @@
import type { NitroFetchRequest, NitroFetchOptions } from 'nitropack/types';
import { FetchError } from 'ofetch';
type RevertFn = () => Promise<void>;
type RevertFn = (success: boolean) => Promise<void>;
type SubmitOpts = {
revert: RevertFn;
successMsg?: string;
errorMsg?: string;
noSuccessToast?: boolean;
};
export function useSubmit<
R extends NitroFetchRequest,
O extends NitroFetchOptions<R> & { body?: never },
>(url: R, options: O, revert: RevertFn, success?: string, error?: string) {
>(url: R, options: O, opts: SubmitOpts) {
const toast = useToast();
const { t: $t } = useI18n();
@ -16,15 +23,20 @@ export function useSubmit<
...options,
body: data,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(res as any).success) {
throw new Error(error || $t('toast.errored'));
throw new Error(opts.errorMsg || $t('toast.errored'));
}
toast.showToast({
type: 'success',
message: success,
});
await revert();
if (!opts.noSuccessToast) {
toast.showToast({
type: 'success',
message: opts.successMsg,
});
}
await opts.revert(true);
} catch (e) {
if (e instanceof FetchError) {
toast.showToast({
@ -39,7 +51,7 @@ export function useSubmit<
} else {
console.error(e);
}
await revert();
await opts.revert(false);
}
};
}

1
src/app/pages/admin.vue

@ -37,6 +37,7 @@
<script setup lang="ts">
const authStore = useAuthStore();
authStore.update();
const { t } = useI18n();
const route = useRoute();

6
src/app/pages/admin/config.vue

@ -6,13 +6,13 @@
<FormTextField
id="host"
v-model="data.host"
:label="$t('admin.config.host')"
:label="$t('general.host')"
:description="$t('admin.config.hostDesc')"
/>
<FormNumberField
id="port"
v-model="data.port"
:label="$t('admin.generic.port')"
:label="$t('general.port')"
:description="$t('admin.config.portDesc')"
/>
</FormGroup>
@ -67,7 +67,7 @@ const _submit = useSubmit(
{
method: 'post',
},
revert
{ revert }
);
function submit() {

2
src/app/pages/admin/hooks.vue

@ -28,7 +28,7 @@ const _submit = useSubmit(
{
method: 'post',
},
revert
{ revert }
);
async function submit() {

2
src/app/pages/admin/index.vue

@ -50,7 +50,7 @@ const _submit = useSubmit(
{
method: 'post',
},
revert
{ revert }
);
function submit() {

12
src/app/pages/admin/interface.vue

@ -11,7 +11,7 @@
<FormNumberField
id="port"
v-model="data.port"
:label="$t('admin.generic.port')"
:label="$t('general.port')"
:description="$t('admin.interface.portDesc')"
/>
<FormTextField
@ -55,7 +55,7 @@ const _submit = useSubmit(
{
method: 'post',
},
revert
{ revert }
);
function submit() {
@ -72,9 +72,11 @@ const _changeCidr = useSubmit(
{
method: 'post',
},
revert,
t('admin.interface.cidrSuccess'),
t('admin.interface.cidrError')
{
revert,
successMsg: t('admin.interface.cidrSuccess'),
errorMsg: t('admin.interface.cidrError'),
}
);
async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) {

14
src/app/pages/clients/[id].vue

@ -92,8 +92,6 @@
const authStore = useAuthStore();
authStore.update();
const router = useRouter();
const route = useRoute();
const id = route.params.id as string;
@ -107,8 +105,10 @@ const _submit = useSubmit(
{
method: 'post',
},
async () => {
router.push('/');
{
revert: async () => {
await navigateTo('/');
},
}
);
@ -126,8 +126,10 @@ const _deleteClient = useSubmit(
{
method: 'delete',
},
async () => {
router.push('/');
{
revert: async () => {
await navigateTo('/');
},
}
);

1
src/app/pages/index.vue

@ -30,6 +30,7 @@
<script setup lang="ts">
const authStore = useAuthStore();
authStore.update();
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();

54
src/app/pages/login.vue

@ -3,7 +3,7 @@
<UiBanner />
<form
class="mx-auto mt-10 flex w-64 flex-col gap-5 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200"
@submit.prevent="login"
@submit.prevent="submit"
>
<!-- Avatar -->
<div
@ -54,40 +54,38 @@
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
const { t } = useI18n();
const authenticating = ref(false);
const remember = ref(false);
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const authStore = useAuthStore();
const toast = useToast();
async function login() {
const _submit = useSubmit(
'/api/session',
{
method: 'post',
},
{
revert: async (success) => {
authenticating.value = false;
password.value = null;
if (success) {
await navigateTo('/');
}
},
noSuccessToast: true,
}
);
async function submit() {
if (!username.value || !password.value || authenticating.value) return;
authenticating.value = true;
try {
const res = await authStore.login(
username.value,
password.value,
remember.value
);
if (res) {
await navigateTo('/');
}
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('error.login'),
message: error.data.message,
});
}
}
authenticating.value = false;
password.value = null;
return _submit({
username: username.value,
password: password.value,
remember: remember.value,
});
}
</script>

16
src/app/pages/me.vue

@ -65,8 +65,10 @@ const _submit = useSubmit(
{
method: 'post',
},
async () => {
authStore.update();
{
revert: () => {
return authStore.update();
},
}
);
@ -83,10 +85,12 @@ const _updatePassword = useSubmit(
{
method: 'post',
},
async () => {
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
{
revert: async () => {
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
},
}
);

7
src/app/pages/setup/1.vue

@ -1,9 +1,11 @@
<template>
<div>
<p class="px-8 pt-8 text-center text-2xl">
{{ $t('setup.messageWelcome.whatIs') }}
{{ $t('setup.welcomeDesc') }}
</p>
<NuxtLink to="/setup/2"><BaseButton>Continue</BaseButton></NuxtLink>
<NuxtLink to="/setup/2">
<BaseButton>{{ $t('general.continue') }}</BaseButton>
</NuxtLink>
</div>
</template>
@ -11,6 +13,7 @@
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(1);
</script>

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

@ -1,83 +1,59 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupCreateAdminUser') }}
{{ $t('setup.createAdminDesc') }}
</p>
<form id="newAccount"></form>
<div>
<Label for="username">{{ $t('username') }}</Label>
<input
id="username"
v-model="username"
form="newAccount"
type="text"
autocomplete="username"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
<div class="flex flex-col gap-3">
<div class="flex flex-col">
<FormNullTextField
id="username"
v-model="username"
autocomplete="username"
:label="$t('general.username')"
/>
</div>
<div class="flex flex-col">
<FormPasswordField
id="password"
v-model="password"
autocomplete="new-password"
:label="$t('general.password')"
/>
</div>
<div>
<BaseButton @click="submit">{{ $t('setup.createAccount') }}</BaseButton>
</div>
</div>
<div>
<Label for="password">{{ $t('setup.newPassword') }}</Label>
<input
id="password"
v-model="password"
form="newAccount"
type="password"
autocomplete="new-password"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<div>
<Label for="accept">{{ $t('setup.accept') }}</Label>
<input
id="accept"
v-model="accept"
form="newAccount"
type="checkbox"
class="ml-2"
/>
</div>
<BaseButton @click="newAccount">Create Account</BaseButton>
</div>
</template>
<script lang="ts" setup>
import { FetchError } from 'ofetch';
const { t } = useI18n();
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(2);
const router = useRouter();
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const accept = ref<boolean>(true);
const toast = useToast();
async function newAccount() {
try {
if (!username.value || !password.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
const username = ref<null | string>(null);
const password = ref<string>('');
await setupStore.step2(username.value, password.value, accept.value);
await router.push('/setup/3');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
const _submit = useSubmit(
'/api/setup/2',
{
method: 'post',
},
{
revert: async (success) => {
if (success) {
await navigateTo('/setup/3');
}
},
noSuccessToast: true,
}
);
function submit() {
return _submit({ username: username.value, password: password.value });
}
</script>

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

@ -1,11 +1,15 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ 'Do you have a existing Setup?' }}
{{ $t('setup.existingSetup') }}
</p>
<div class="mb-8 flex justify-center">
<NuxtLink to="/setup/4"><BaseButton>No</BaseButton></NuxtLink>
<NuxtLink to="/setup/migrate"><BaseButton>Yes</BaseButton></NuxtLink>
<NuxtLink to="/setup/4">
<BaseButton>{{ $t('general.no') }}</BaseButton>
</NuxtLink>
<NuxtLink to="/setup/migrate">
<BaseButton>{{ $t('general.yes') }}</BaseButton>
</NuxtLink>
</div>
</div>
</template>
@ -14,6 +18,7 @@
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(3);
</script>

86
src/app/pages/setup/4.vue

@ -1,71 +1,59 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupHostPort') }}
{{ $t('setup.setupConfigDesc') }}
</p>
<div>
<Label for="host">{{ $t('setup.host') }}</Label>
<input
id="host"
v-model="host"
type="text"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
placeholder="vpn.example.com"
/>
<div class="flex flex-col gap-3">
<div class="flex flex-col">
<FormNullTextField
id="host"
v-model="host"
type="text"
:label="$t('general.host')"
placeholder="vpn.example.com"
/>
</div>
<div class="flex flex-col">
<FormNumberField
id="port"
v-model="port"
type="number"
:label="$t('general.port')"
/>
</div>
<div>
<BaseButton @click="submit">{{ $t('general.continue') }}</BaseButton>
</div>
</div>
<div>
<Label for="port">{{ $t('setup.port') }}</Label>
<input
id="port"
v-model="port"
type="number"
:min="1"
:max="65535"
class="mb-5 w-full rounded-lg border-2 border-gray-100 px-3 py-2 text-sm text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-gray-200 dark:placeholder:text-neutral-400 dark:focus:border-red-800"
/>
</div>
<BaseButton @click="updateHostPort">Continue</BaseButton>
</div>
</template>
<script setup lang="ts">
import { FetchError } from 'ofetch';
definePageMeta({
layout: 'setup',
});
const { t } = useI18n();
const setupStore = useSetupStore();
setupStore.setStep(4);
const router = useRouter();
const host = ref<null | string>(null);
const port = ref<number>(51820);
const toast = useToast();
async function updateHostPort() {
if (!host.value || !port.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
const _submit = useSubmit(
'/api/setup/4',
{
method: 'post',
},
{
revert: async (success) => {
if (success) {
await navigateTo('/setup/5');
}
},
}
);
try {
await setupStore.step4(host.value, port.value);
await router.push('/setup/success');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
function submit() {
return _submit({ host: host.value, port: port.value });
}
</script>

52
src/app/pages/setup/migrate.vue

@ -1,68 +1,56 @@
<template>
<div>
<p class="p-8 text-center text-lg">
{{ $t('setup.messageSetupMigration') }}
{{ $t('setup.setupMigrationDesc') }}
</p>
<div>
<Label for="migration">{{ $t('setup.migration') }}</Label>
<input id="migration" type="file" @change="onChangeFile" />
</div>
<BaseButton @click="sendFile">Upload</BaseButton>
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton>
</div>
</template>
<script lang="ts" setup>
import { FetchError } from 'ofetch';
definePageMeta({
layout: 'setup',
});
const { t } = useI18n();
const setupStore = useSetupStore();
setupStore.setStep(5);
const backupFile = ref<null | File>(null);
function onChangeFile(evt: Event) {
const target = evt.target as HTMLInputElement;
const file = target.files?.[0];
console.log('file', file);
if (file) {
backupFile.value = file;
console.log('backupFile.value', backupFile.value);
console.log('selected file', backupFile.value);
}
}
const router = useRouter();
const toast = useToast();
const _submit = useSubmit(
'/api/setup/migrate',
{
method: 'post',
},
{
revert: async (success) => {
if (success) {
await navigateTo('/setup/success');
}
},
}
);
async function sendFile() {
async function submit() {
if (!backupFile.value) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: t('setup.emptyFields'),
});
return;
}
try {
const content = await readFileContent(backupFile.value);
await setupStore.runMigration(content);
await router.push('/setup/success');
} catch (error) {
if (error instanceof FetchError) {
toast.showToast({
type: 'error',
title: t('setup.requirements'),
message: error.data.message,
});
}
}
const content = await readFileContent(backupFile.value);
return _submit({ file: content });
}
async function readFileContent(file: File): Promise<string> {

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

@ -1,7 +1,9 @@
<template>
<div>
<p>Setup successfully</p>
<NuxtLink to="/login"><BaseButton>Login</BaseButton></NuxtLink>
<p>{{ $t('setup.successful') }}</p>
<NuxtLink to="/login">
<BaseButton>{{ $t('login.signIn') }}</BaseButton>
</NuxtLink>
</div>
</template>
@ -9,6 +11,7 @@
definePageMeta({
layout: 'setup',
});
const setupStore = useSetupStore();
setupStore.setStep(6);
</script>

23
src/app/stores/auth.ts

@ -14,26 +14,5 @@ export const useAuthStore = defineStore('Auth', () => {
}
}
/**
* @throws if unsuccessful
*/
async function login(username: string, password: string, remember: boolean) {
await $fetch('/api/session', {
method: 'post',
body: { username, password, remember },
});
return true as const;
}
/**
* @throws if unsuccessful
*/
async function logout() {
const response = await $fetch('/api/session', {
method: 'delete',
});
return response.success;
}
return { userData, login, logout, update, getSession };
return { userData, update, getSession };
});

10
src/app/stores/global.ts

@ -1,6 +1,8 @@
import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('Global', () => {
const { data: release } = useFetch('/api/release', {
method: 'get',
});
const sortClient = ref(true); // Sort clients by name, true = asc, false = desc
const currentRelease = ref<null | string>(null);
@ -10,10 +12,6 @@ export const useGlobalStore = defineStore('Global', () => {
const updateAvailable = ref(false);
async function fetchRelease() {
const { data: release } = await useFetch('/api/release', {
method: 'get',
});
if (!release.value) {
return;
}

36
src/app/stores/setup.ts

@ -1,39 +1,6 @@
import { defineStore } from 'pinia';
export const useSetupStore = defineStore('Setup', () => {
/**
* @throws if unsuccessful
*/
async function step2(username: string, password: string, accept: boolean) {
const response = await $fetch('/api/setup/2', {
method: 'post',
body: { username, password, accept },
});
return response.success;
}
/**
* @throws if unsuccessful
*/
async function step4(host: string, port: number) {
const response = await $fetch('/api/setup/4', {
method: 'post',
body: { host, port },
});
return response.success;
}
/**
* @throws if unsuccessful
*/
async function runMigration(file: string) {
const response = await $fetch('/api/setup/migrate', {
method: 'post',
body: { file },
});
return response.success;
}
const step = ref(1);
const totalSteps = ref(5);
function setStep(i: number) {
@ -41,9 +8,6 @@ export const useSetupStore = defineStore('Setup', () => {
}
return {
step2,
step4,
runMigration,
step,
totalSteps,
setStep,

93
src/i18n/locales/en.json

@ -25,60 +25,39 @@
"updatePassword": "Update Password",
"mtu": "MTU",
"allowedIps": "Allowed IPs",
"persistentKeepalive": "Persistent Keepalive"
"persistentKeepalive": "Persistent Keepalive",
"logout": "Logout",
"continue": "Continue",
"host": "Host",
"port": "Port",
"yes": "Yes",
"no": "No"
},
"setup": {
"welcome": "Welcome to your first setup of wg-easy !",
"messageWelcome": {
"whatIs": "You have found the easiest way to install and manage WireGuard on any Linux host!",
"warning": "First of all, make sure you have a backup of your data if you want to migrate your users to your new wg-easy.",
"next": "Click on the arrow button to proceed to the next step."
},
"messageSetupLanguage": "Please choose a language for the setup.",
"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.",
"messageSetupMigration": "Please provide the backup file if you want to migrate your data from your previous wg-easy version to your new setup.",
"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...",
"accept": "I accept the condition",
"submitBtn": "Create admin account",
"usernamePlaceholder": "Administrator",
"passwordPlaceholder": "Strong password",
"requirements": "Setup requirements",
"host": "Host",
"hostPlaceholder": "wg-easy.example.com",
"port": "Port",
"portPlaceholder": "443",
"migration": "Restore the backup"
"welcomeDesc": "You have found the easiest way to install and manage WireGuard on any Linux host!",
"existingSetup": "Do you have an existing setup?",
"createAdminDesc": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.",
"setupConfigDesc": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.",
"setupMigrationDesc": "Please provide the backup file if you want to migrate your data from your previous wg-easy version to your new setup.",
"upload": "Upload",
"migration": "Restore the backup",
"createAccount": "Create Account",
"successful": "Setup successful"
},
"update": {
"updateAvailable": "There is an update available!",
"update": "Update"
},
"logout": "Logout",
"updateAvailable": "There is an update available!",
"update": "Update",
"new": "New",
"createdOn": "Created on ",
"lastSeen": "Last seen on ",
"totalDownload": "Total Download: ",
"totalUpload": "Total Upload: ",
"newClient": "New Client",
"disableClient": "Disable Client",
"enableClient": "Enable Client",
"noClients": "There are no clients yet.",
"noPrivKey": "This client has no known private key. Cannot create Configuration.",
"showQR": "Show QR Code",
"downloadConfig": "Download Configuration",
"madeBy": "Made by",
"donate": "Donate",
"toggleCharts": "Show/hide Charts",
"theme": {
"dark": "Dark theme",
"light": "Light theme",
"system": "System theme"
},
"sort": "Sort",
"Permanent": "Permanent",
"OneTimeLink": "Generate short one time link",
"errorInit": "Initialization failed.",
"layout": {
"toggleCharts": "Show/hide Charts",
"donate": "Donate"
},
"login": {
"signIn": "Sign In",
"rememberMe": "Remember me",
@ -89,7 +68,11 @@
"login": "Log in error"
},
"client": {
"empty": "There are no clients yet.",
"newShort": "New",
"sort": "Sort",
"create": "Create Client",
"created": "Client created",
"new": "New Client",
"name": "Name",
"expireDate": "Expire Date",
@ -98,7 +81,19 @@
"deleteDialog2": "This action cannot be undone.",
"enabled": "Enabled",
"address": "Address",
"serverAllowedIps": "Server Allowed IPs"
"serverAllowedIps": "Server Allowed IPs",
"otlDesc": "Generate short one time link",
"permanent": "Permanent",
"createdOn": "Created on ",
"lastSeen": "Last seen on ",
"totalDownload": "Total Download: ",
"totalUpload": "Total Upload: ",
"newClient": "New Client",
"disableClient": "Disable Client",
"enableClient": "Enable Client",
"noPrivKey": "This client has no known private key. Cannot create Configuration.",
"showQR": "Show QR Code",
"downloadConfig": "Download Configuration"
},
"dialog": {
"change": "Change",
@ -132,7 +127,6 @@
},
"config": {
"connection": "Connection",
"host": "Host",
"hostDesc": "Public hostname clients will connect to (invalidates config)",
"portDesc": "Public UDP port clients will connect to (invalidates config)",
"allowedIpsDesc": "Allowed IPs clients will use (invalidates config)",
@ -149,9 +143,6 @@
"mtuDesc": "MTU WireGuard will use",
"portDesc": "UDP Port WireGuard will listen on (could invalidate config)",
"changeCidr": "Change CIDR"
},
"generic": {
"port": "Port"
}
},
"zod": {
@ -180,8 +171,6 @@
"passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character",
"remember": "Remember",
"accept": "Accept",
"acceptTrue": "Accept Conditions to continue",
"name": "Name",
"email": "Email",
"emailInvalid": "Email must be a valid email",

7
src/server/database/repositories/user/types.ts

@ -29,17 +29,10 @@ export const UserLoginSchema = z.object(
{ message: objectMessage }
);
const accept = z
.boolean({ message: t('zod.user.accept') })
.refine((val) => val === true, {
message: t('zod.user.acceptTrue'),
});
export const UserSetupSchema = z.object(
{
username: username,
password: password,
accept: accept,
},
{ message: objectMessage }
);

Loading…
Cancel
Save