mirror of https://github.com/wg-easy/wg-easy
Browse Source
* update: setup ui page * remove script addition * add admin panel * basic user menu and admin page * make usable admin panel * add radix vue, improve ui * fix features, add toast * rewrite middleware logic, support basic auth * add todo marker * active tailwind forms * remove some console.logs * check if user is enabled frontend doesn't handle this state yet, nothing will work as api routes will fail * add email to user, basic account page * better group database * group even more * basic statistics page * update: admin ui - add: common panel components to get same UI - i18n: french * update: setup page error handle - use fetch error data to provide error message - use translation to provider error message * update: me page * fix: :text props * update: login page * update: i18n french support * fix: use radix toast duration * update: reduce templates - remake: setup page to add others step configuration (host/port/migration) * udpate: setup page use wizard form step * update: ui * update: step page - first step to choose a language - use red color in light mode - validate step before move toward * update: setup page - use radix select component to reduce boilerplate * update: setup page - add: database langugage method - update: api lang & export supported languages * update: setup page - update ui select language - change lang on selection * fix: use global store * fix: initial value - update: sort langs by value * fix: ui center paragraph * fix: remove file extension & some revert - add: script to run checks script * update: setup page - add: host/port section - i18n: french - fix: fallback translation * refactor: split setup into files * update: setup page - redirect to login when the setup is done - allow user to return to previous steps - prompt error message - i18n french * add: migration UI step - rename: components - fix: label for & form child id - i18n french sup * add: migration server * fix: use string instead of File * improve: with zod validation * restore: clients * rework setup * add client page, move api routes * improve setup * switch to agpl * add step back * update licensed under texts cc -> agpl * make db results readonly avoid weird side effects, when modifying the db object as its only allowed inside e.g. lowdb.ts * update footer links * improve client edit page, add mtu * reorder tailwind classes * update packages * update comments * better toast, better avatar * delete feature toggle * remove chart, statistics from server let user decide what he wants to display * move into own components * switch from AGPL-3.0-or-later to AGPL-3.0-only AGPL-3.0-or-later is not OSI approved * fix building source fixes https://github.com/wg-easy/wg-easy/issues/1563 * update packages --------- Co-authored-by: tetuaoro <[email protected]>pull/1618/head
committed by
Bernd Storath
175 changed files with 5891 additions and 4455 deletions
File diff suppressed because it is too large
@ -5,5 +5,5 @@ |
|||||
"dev": "docker compose -f docker-compose.dev.yml up", |
"dev": "docker compose -f docker-compose.dev.yml up", |
||||
"build": "docker build -t wg-easy ." |
"build": "docker build -t wg-easy ." |
||||
}, |
}, |
||||
"packageManager": "[email protected]0.0" |
"packageManager": "[email protected]5.0" |
||||
} |
} |
||||
|
@ -1,60 +0,0 @@ |
|||||
<template> |
|
||||
<span class="group"> |
|
||||
<!-- Show --> |
|
||||
<input |
|
||||
v-show="clientEditAddress4Id === client.id" |
|
||||
ref="clientAddress4Input" |
|
||||
v-model="clientEditAddress4" |
|
||||
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-20 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500" |
|
||||
@keyup.enter=" |
|
||||
updateClientAddress4(client, clientEditAddress4); |
|
||||
clientEditAddress4 = null; |
|
||||
clientEditAddress4Id = null; |
|
||||
" |
|
||||
@keyup.escape=" |
|
||||
clientEditAddress4 = null; |
|
||||
clientEditAddress4Id = null; |
|
||||
" |
|
||||
/> |
|
||||
<span v-show="clientEditAddress4Id !== client.id" class="inline-block">{{ |
|
||||
client.address4 |
|
||||
}}</span> |
|
||||
|
|
||||
<!-- Edit --> |
|
||||
<span |
|
||||
v-show="clientEditAddress4Id !== client.id" |
|
||||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|
||||
@click=" |
|
||||
clientEditAddress4 = client.address4; |
|
||||
clientEditAddress4Id = client.id; |
|
||||
nextTick(() => clientAddress4Input?.select()); |
|
||||
" |
|
||||
> |
|
||||
<IconsEdit |
|
||||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" |
|
||||
/> |
|
||||
</span> |
|
||||
</span> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
|
|
||||
const clientsStore = useClientsStore(); |
|
||||
|
|
||||
const clientAddress4Input = ref<HTMLInputElement | null>(null); |
|
||||
const clientEditAddress4 = ref<null | string>(null); |
|
||||
const clientEditAddress4Id = ref<null | string>(null); |
|
||||
|
|
||||
function updateClientAddress4(client: WGClient, address4: string | null) { |
|
||||
if (address4 === null) { |
|
||||
return; |
|
||||
} |
|
||||
api |
|
||||
.updateClientAddress4({ clientId: client.id, address4 }) |
|
||||
.catch((err) => alert(err.message || err.toString())) |
|
||||
.finally(() => clientsStore.refresh().catch(console.error)); |
|
||||
} |
|
||||
</script> |
|
@ -1,31 +0,0 @@ |
|||||
<template> |
|
||||
<div class="h-10 w-10 mt-2 self-start rounded-full bg-gray-50 relative"> |
|
||||
<IconsAvatar class="w-6 m-2 text-gray-300" /> |
|
||||
<img |
|
||||
v-if="client.avatar" |
|
||||
:src="client.avatar" |
|
||||
class="w-10 rounded-full absolute top-0 left-0" |
|
||||
/> |
|
||||
|
|
||||
<div |
|
||||
v-if=" |
|
||||
client.latestHandshakeAt && |
|
||||
new Date().getTime() - new Date(client.latestHandshakeAt).getTime() < |
|
||||
1000 * 60 * 10 |
|
||||
" |
|
||||
> |
|
||||
<div |
|
||||
class="animate-ping w-4 h-4 p-1 bg-red-100 dark:bg-red-100 rounded-full absolute -bottom-1 -right-1" |
|
||||
/> |
|
||||
<div |
|
||||
class="w-2 h-2 bg-red-800 dark:bg-red-600 rounded-full absolute bottom-0 right-0" |
|
||||
/> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
</script> |
|
@ -1,58 +0,0 @@ |
|||||
<template> |
|
||||
<ClientCharts :client="client" /> |
|
||||
<div |
|
||||
class="relative py-3 md:py-5 px-3 z-10 flex flex-col sm:flex-row justify-between gap-3" |
|
||||
> |
|
||||
<div class="flex gap-3 md:gap-4 w-full items-center"> |
|
||||
<ClientAvatar :client="client" /> |
|
||||
<!-- Name & Info --> |
|
||||
<div class="flex flex-col xxs:flex-row w-full gap-2"> |
|
||||
<div class="flex flex-col flex-grow gap-1"> |
|
||||
<ClientName :client="client" /> |
|
||||
<div |
|
||||
class="block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs" |
|
||||
> |
|
||||
<ClientAddress4 :client="client" /> |
|
||||
<ClientInlineTransfer |
|
||||
v-if="!globalStore.features.trafficStats.enabled" |
|
||||
:client="client" |
|
||||
/> |
|
||||
<ClientLastSeen :client="client" /> |
|
||||
</div> |
|
||||
<ClientOneTimeLink :client="client" /> |
|
||||
<ClientExpireDate :client="client" /> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Info --> |
|
||||
<div |
|
||||
v-if="globalStore.features.trafficStats.enabled" |
|
||||
class="flex gap-2 items-center shrink-0 text-gray-400 dark:text-neutral-400 text-xs mt-px justify-end" |
|
||||
> |
|
||||
<ClientTransfer :client="client" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<!-- </div> --> |
|
||||
<!-- <div class="flex flex-grow items-center"> --> |
|
||||
</div> |
|
||||
|
|
||||
<div class="flex items-center justify-end"> |
|
||||
<div |
|
||||
class="text-gray-400 dark:text-neutral-400 flex gap-1 items-center justify-between" |
|
||||
> |
|
||||
<ClientSwitch :client="client" /> |
|
||||
<ClientQRCode :client="client" /> |
|
||||
<ClientConfig :client="client" /> |
|
||||
<ClientOneTimeLinkBtn :client="client" /> |
|
||||
<ClientDelete :client="client" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
|
|
||||
const globalStore = useGlobalStore(); |
|
||||
</script> |
|
@ -1,27 +0,0 @@ |
|||||
<template> |
|
||||
<a |
|
||||
:disabled="!client.downloadableConfig" |
|
||||
:href="'./api/wireguard/client/' + client.id + '/configuration'" |
|
||||
:download="client.downloadableConfig ? 'configuration' : null" |
|
||||
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition" |
|
||||
:class="{ |
|
||||
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': |
|
||||
client.downloadableConfig, |
|
||||
'is-disabled': !client.downloadableConfig, |
|
||||
}" |
|
||||
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('downloadConfig')" |
|
||||
@click=" |
|
||||
if (!client.downloadableConfig) { |
|
||||
$event.preventDefault(); |
|
||||
} |
|
||||
" |
|
||||
> |
|
||||
<IconsDownload class="w-5" /> |
|
||||
</a> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
</script> |
|
@ -1,17 +0,0 @@ |
|||||
<template> |
|
||||
<button |
|
||||
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition" |
|
||||
:title="$t('deleteClient')" |
|
||||
@click="modalStore.clientDelete = client" |
|
||||
> |
|
||||
<IconsDelete class="w-5" /> |
|
||||
</button> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
|
|
||||
const modalStore = useModalStore(); |
|
||||
</script> |
|
@ -1,91 +0,0 @@ |
|||||
<template> |
|
||||
<div |
|
||||
v-show="globalStore.features.clientExpiration.enabled" |
|
||||
class="block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs" |
|
||||
> |
|
||||
<span class="group"> |
|
||||
<!-- Show --> |
|
||||
<input |
|
||||
v-show="clientEditExpireDateId === client.id" |
|
||||
ref="clientExpireDateInput" |
|
||||
v-model="clientEditExpireDate" |
|
||||
type="text" |
|
||||
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-70 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500 text-xs p-0" |
|
||||
@keyup.enter=" |
|
||||
updateClientExpireDate(client, clientEditExpireDate); |
|
||||
clientEditExpireDate = null; |
|
||||
clientEditExpireDateId = null; |
|
||||
" |
|
||||
@keyup.escape=" |
|
||||
clientEditExpireDate = null; |
|
||||
clientEditExpireDateId = null; |
|
||||
" |
|
||||
/> |
|
||||
<span |
|
||||
v-show="clientEditExpireDateId !== client.id" |
|
||||
class="inline-block" |
|
||||
>{{ expiredDateFormat(client.expiresAt) }}</span |
|
||||
> |
|
||||
|
|
||||
<!-- Edit --> |
|
||||
<span |
|
||||
v-show="clientEditExpireDateId !== client.id" |
|
||||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|
||||
@click=" |
|
||||
clientEditExpireDate = client.expiresAt |
|
||||
? client.expiresAt.slice(0, 10) |
|
||||
: 'yyyy-mm-dd'; |
|
||||
clientEditExpireDateId = client.id; |
|
||||
nextTick(() => clientExpireDateInput?.select()); |
|
||||
" |
|
||||
> |
|
||||
<svg |
|
||||
xmlns="http://www.w3.org/2000/svg" |
|
||||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" |
|
||||
fill="none" |
|
||||
viewBox="0 0 24 24" |
|
||||
stroke="currentColor" |
|
||||
> |
|
||||
<path |
|
||||
stroke-linecap="round" |
|
||||
stroke-linejoin="round" |
|
||||
stroke-width="2" |
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" |
|
||||
/> |
|
||||
</svg> |
|
||||
</span> |
|
||||
</span> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ client: LocalClient }>(); |
|
||||
|
|
||||
const globalStore = useGlobalStore(); |
|
||||
const clientsStore = useClientsStore(); |
|
||||
const clientEditExpireDate = ref<string | null>(null); |
|
||||
const clientEditExpireDateId = ref<string | null>(null); |
|
||||
const { t, locale } = useI18n(); |
|
||||
|
|
||||
const clientExpireDateInput = ref<HTMLInputElement | null>(null); |
|
||||
|
|
||||
function updateClientExpireDate( |
|
||||
client: LocalClient, |
|
||||
expireDate: string | null |
|
||||
) { |
|
||||
api |
|
||||
.updateClientExpireDate({ clientId: client.id, expireDate }) |
|
||||
.catch((err) => alert(err.message || err.toString())) |
|
||||
.finally(() => clientsStore.refresh().catch(console.error)); |
|
||||
} |
|
||||
|
|
||||
function expiredDateFormat(value: string | null) { |
|
||||
if (value === null) return t('Permanent'); |
|
||||
const dateTime = new Date(value); |
|
||||
return dateTime.toLocaleDateString(locale.value, { |
|
||||
year: 'numeric', |
|
||||
month: 'long', |
|
||||
day: 'numeric', |
|
||||
}); |
|
||||
} |
|
||||
</script> |
|
@ -1,29 +0,0 @@ |
|||||
<template> |
|
||||
<!-- Inline Transfer TX --> |
|
||||
<span |
|
||||
v-if="client.transferTx" |
|
||||
class="whitespace-nowrap" |
|
||||
:title="$t('totalDownload') + bytes(client.transferTx)" |
|
||||
> |
|
||||
· |
|
||||
<IconsArrowDown class="align-middle h-3 inline" /> |
|
||||
{{ bytes(client.transferTxCurrent) }}/s |
|
||||
</span> |
|
||||
|
|
||||
<!-- Inline Transfer RX --> |
|
||||
<span |
|
||||
v-if="client.transferRx" |
|
||||
class="whitespace-nowrap" |
|
||||
:title="$t('totalUpload') + bytes(client.transferRx)" |
|
||||
> |
|
||||
· |
|
||||
<IconsArrowUp class="align-middle h-3 inline" /> |
|
||||
{{ bytes(client.transferRxCurrent) }}/s |
|
||||
</span> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
</script> |
|
@ -1,65 +0,0 @@ |
|||||
<template> |
|
||||
<div |
|
||||
class="text-gray-700 dark:text-neutral-200 group text-sm md:text-base" |
|
||||
:title="$t('createdOn') + dateTime(new Date(client.createdAt))" |
|
||||
> |
|
||||
<!-- Show --> |
|
||||
<input |
|
||||
v-show="clientEditNameId === client.id" |
|
||||
ref="clientNameInput" |
|
||||
v-model="clientEditName" |
|
||||
class="rounded px-1 border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 dark:placeholder:text-neutral-500 outline-none w-30" |
|
||||
@keyup.enter=" |
|
||||
updateClientName(client, clientEditName); |
|
||||
clientEditName = null; |
|
||||
clientEditNameId = null; |
|
||||
" |
|
||||
@keyup.escape=" |
|
||||
clientEditName = null; |
|
||||
clientEditNameId = null; |
|
||||
" |
|
||||
/> |
|
||||
<span |
|
||||
v-show="clientEditNameId !== client.id" |
|
||||
class="border-t-2 border-b-2 border-transparent" |
|
||||
>{{ client.name }}</span |
|
||||
> |
|
||||
|
|
||||
<!-- Edit --> |
|
||||
<span |
|
||||
v-show="clientEditNameId !== client.id" |
|
||||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|
||||
@click=" |
|
||||
clientEditName = client.name; |
|
||||
clientEditNameId = client.id; |
|
||||
nextTick(() => clientNameInput?.select()); |
|
||||
" |
|
||||
> |
|
||||
<IconsEdit |
|
||||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" |
|
||||
/> |
|
||||
</span> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
|
|
||||
const clientsStore = useClientsStore(); |
|
||||
|
|
||||
const clientNameInput = ref<HTMLInputElement | null>(null); |
|
||||
const clientEditName = ref<null | string>(null); |
|
||||
const clientEditNameId = ref<null | string>(null); |
|
||||
|
|
||||
function updateClientName(client: LocalClient, name: string | null) { |
|
||||
if (name === null) { |
|
||||
return; |
|
||||
} |
|
||||
api |
|
||||
.updateClientName({ clientId: client.id, name }) |
|
||||
.catch((err) => alert(err.message || err.toString())) |
|
||||
.finally(() => clientsStore.refresh().catch(console.error)); |
|
||||
} |
|
||||
</script> |
|
@ -1,24 +0,0 @@ |
|||||
<template> |
|
||||
<div |
|
||||
v-if=" |
|
||||
globalStore.features.oneTimeLinks.enabled && client.oneTimeLink !== null |
|
||||
" |
|
||||
:ref="'client-' + client.id + '-link'" |
|
||||
class="text-gray-400 text-xs" |
|
||||
> |
|
||||
<a :href="'./cnf/' + client.oneTimeLink + ''">{{ path }}</a> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
const props = defineProps<{ client: LocalClient }>(); |
|
||||
|
|
||||
const globalStore = useGlobalStore(); |
|
||||
|
|
||||
const path = computed(() => { |
|
||||
if (import.meta.client) { |
|
||||
return `${document.location.protocol}//${document.location.host}/cnf/${props.client.oneTimeLink}`; |
|
||||
} |
|
||||
return ''; |
|
||||
}); |
|
||||
</script> |
|
@ -1,25 +0,0 @@ |
|||||
<template> |
|
||||
<button |
|
||||
:disabled="!client.downloadableConfig" |
|
||||
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition" |
|
||||
:class="{ |
|
||||
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': |
|
||||
client.downloadableConfig, |
|
||||
'is-disabled': !client.downloadableConfig, |
|
||||
}" |
|
||||
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('showQR')" |
|
||||
@click=" |
|
||||
modalStore.qrcode = `./api/wireguard/client/${client.id}/qrcode.svg` |
|
||||
" |
|
||||
> |
|
||||
<IconsQRCode class="w-5" /> |
|
||||
</button> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
|
|
||||
const modalStore = useModalStore(); |
|
||||
</script> |
|
@ -1,40 +0,0 @@ |
|||||
<template> |
|
||||
<div |
|
||||
v-if="client.enabled === true" |
|
||||
:title="$t('disableClient')" |
|
||||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all" |
|
||||
@click="disableClient(client)" |
|
||||
> |
|
||||
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white" /> |
|
||||
</div> |
|
||||
|
|
||||
<div |
|
||||
v-if="client.enabled === false" |
|
||||
:title="$t('enableClient')" |
|
||||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 dark:bg-neutral-400 cursor-pointer hover:bg-gray-300 dark:hover:bg-neutral-500 transition-all" |
|
||||
@click="enableClient(client)" |
|
||||
> |
|
||||
<div class="rounded-full w-4 h-4 m-1 bg-white" /> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
defineProps<{ |
|
||||
client: LocalClient; |
|
||||
}>(); |
|
||||
|
|
||||
const clientsStore = useClientsStore(); |
|
||||
|
|
||||
function enableClient(client: WGClient) { |
|
||||
api |
|
||||
.enableClient({ clientId: client.id }) |
|
||||
.catch((err) => alert(err.message || err.toString())) |
|
||||
.finally(() => clientsStore.refresh().catch(console.error)); |
|
||||
} |
|
||||
function disableClient(client: WGClient) { |
|
||||
api |
|
||||
.disableClient({ clientId: client.id }) |
|
||||
.catch((err) => alert(err.message || err.toString())) |
|
||||
.finally(() => clientsStore.refresh().catch(console.error)); |
|
||||
} |
|
||||
</script> |
|
@ -0,0 +1,11 @@ |
|||||
|
<template> |
||||
|
<span class="inline-block"> |
||||
|
{{ client.address4 }}, {{ client.address6 }} |
||||
|
</span> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
</script> |
@ -0,0 +1,30 @@ |
|||||
|
<template> |
||||
|
<div class="relative mt-2 h-10 w-10 self-start rounded-full bg-gray-50"> |
||||
|
<BaseAvatar :img="client.avatar" class="h-10 w-10"> |
||||
|
<IconsAvatar class="h-6 w-6 text-gray-300" /> |
||||
|
</BaseAvatar> |
||||
|
|
||||
|
<div |
||||
|
v-if=" |
||||
|
client.latestHandshakeAt && |
||||
|
new Date().getTime() - new Date(client.latestHandshakeAt).getTime() < |
||||
|
1000 * 60 * 10 |
||||
|
" |
||||
|
> |
||||
|
<div |
||||
|
class="absolute -bottom-1 -right-1 h-4 w-4 animate-ping rounded-full bg-red-100 p-1 dark:bg-red-100" |
||||
|
/> |
||||
|
<div |
||||
|
class="absolute bottom-0 right-0 h-2 w-2 rounded-full bg-red-800 dark:bg-red-600" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const props = defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
|
||||
|
console.log(props.client.avatar); |
||||
|
</script> |
@ -0,0 +1,47 @@ |
|||||
|
<template> |
||||
|
<ClientCardCharts :client="client" /> |
||||
|
<div |
||||
|
class="relative z-10 flex flex-col justify-between gap-3 px-3 py-3 sm:flex-row md:py-5" |
||||
|
> |
||||
|
<div class="flex w-full items-center gap-3 md:gap-4"> |
||||
|
<ClientCardAvatar :client="client" /> |
||||
|
<div class="flex w-full flex-col gap-2 xxs:flex-row"> |
||||
|
<div class="flex flex-grow flex-col gap-1"> |
||||
|
<ClientCardName :client="client" /> |
||||
|
<div |
||||
|
class="block pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400" |
||||
|
> |
||||
|
<ClientCardAddress :client="client" /> |
||||
|
<ClientCardLastSeen :client="client" /> |
||||
|
</div> |
||||
|
<ClientCardOneTimeLink :client="client" /> |
||||
|
<ClientCardExpireDate :client="client" /> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="mt-px flex shrink-0 items-center justify-end gap-2 text-xs text-gray-400 dark:text-neutral-400" |
||||
|
> |
||||
|
<ClientCardTransfer :client="client" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flex items-center justify-end"> |
||||
|
<div |
||||
|
class="flex items-center justify-between gap-1 text-gray-400 dark:text-neutral-400" |
||||
|
> |
||||
|
<ClientCardSwitch :client="client" /> |
||||
|
<ClientCardEdit :client="client" /> |
||||
|
<ClientCardQRCode :client="client" /> |
||||
|
<ClientCardConfig :client="client" /> |
||||
|
<ClientCardOneTimeLinkBtn :client="client" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
</script> |
@ -0,0 +1,16 @@ |
|||||
|
<template> |
||||
|
<NuxtLink |
||||
|
:to="'/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')" |
||||
|
> |
||||
|
<IconsDownload class="w-5" /> |
||||
|
</NuxtLink> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
</script> |
@ -0,0 +1,14 @@ |
|||||
|
<template> |
||||
|
<NuxtLink |
||||
|
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" |
||||
|
:to="`/clients/${client.id}`" |
||||
|
> |
||||
|
<IconsEdit class="w-5" /> |
||||
|
</NuxtLink> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
</script> |
@ -0,0 +1,23 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
class="block pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400" |
||||
|
> |
||||
|
<span class="inline-block">{{ expiredDateFormat(client.expiresAt) }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ client: LocalClient }>(); |
||||
|
|
||||
|
const { t, locale } = useI18n(); |
||||
|
|
||||
|
function expiredDateFormat(value: string | null) { |
||||
|
if (value === null) return t('Permanent'); |
||||
|
const dateTime = new Date(value); |
||||
|
return dateTime.toLocaleDateString(locale.value, { |
||||
|
year: 'numeric', |
||||
|
month: 'long', |
||||
|
day: 'numeric', |
||||
|
}); |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,16 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
class="text-sm text-gray-700 md:text-base dark:text-neutral-200" |
||||
|
:title="$t('createdOn') + dateTime(new Date(client.createdAt))" |
||||
|
> |
||||
|
<span class="border-b-2 border-t-2 border-transparent"> |
||||
|
{{ client.name }} |
||||
|
</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
</script> |
@ -0,0 +1,17 @@ |
|||||
|
<template> |
||||
|
<div v-if="client.oneTimeLink !== null" class="text-xs text-gray-400"> |
||||
|
<a :href="'./cnf/' + client.oneTimeLink.oneTimeLink">{{ path }}</a> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const props = defineProps<{ client: LocalClient }>(); |
||||
|
|
||||
|
const path = computed(() => { |
||||
|
if (import.meta.client) { |
||||
|
// TODO: show how long its still valid |
||||
|
return `${document.location.protocol}//${document.location.host}/cnf/${props.client.oneTimeLink?.oneTimeLink}`; |
||||
|
} |
||||
|
return 'Loading...'; |
||||
|
}); |
||||
|
</script> |
@ -0,0 +1,17 @@ |
|||||
|
<template> |
||||
|
<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')" |
||||
|
@click="modalStore.qrcode = `./api/client/${client.id}/qrcode.svg`" |
||||
|
> |
||||
|
<IconsQRCode class="w-5" /> |
||||
|
</button> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
|
||||
|
const modalStore = useModalStore(); |
||||
|
</script> |
@ -0,0 +1,35 @@ |
|||||
|
<template> |
||||
|
<BaseSwitch |
||||
|
v-model="enabled" |
||||
|
:title="client.enabled ? $t('disableClient') : $t('enableClient')" |
||||
|
@click="toggleClient" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const props = defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
|
||||
|
const enabled = ref(props.client.enabled); |
||||
|
|
||||
|
const clientsStore = useClientsStore(); |
||||
|
|
||||
|
async function toggleClient() { |
||||
|
try { |
||||
|
if (props.client.enabled) { |
||||
|
await $fetch(`/api/client/${props.client.id}/disable`, { |
||||
|
method: 'post', |
||||
|
}); |
||||
|
} else { |
||||
|
await $fetch(`/api/client/${props.client.id}/enable`, { |
||||
|
method: 'post', |
||||
|
}); |
||||
|
} |
||||
|
} catch (err) { |
||||
|
alert(err); |
||||
|
} finally { |
||||
|
clientsStore.refresh().catch(console.error); |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,21 @@ |
|||||
|
<template> |
||||
|
<AvatarRoot |
||||
|
class="mr-2 inline-flex select-none items-center justify-center overflow-hidden rounded-full align-middle" |
||||
|
> |
||||
|
<AvatarImage |
||||
|
v-if="img" |
||||
|
class="h-full w-full rounded-[inherit] object-cover" |
||||
|
:src="img" |
||||
|
/> |
||||
|
<AvatarFallback |
||||
|
class="leading-1 flex h-full w-full items-center justify-center bg-white text-sm font-medium" |
||||
|
:delay-ms="600" |
||||
|
> |
||||
|
<slot /> |
||||
|
</AvatarFallback> |
||||
|
</AvatarRoot> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
defineProps<{ img?: string }>(); |
||||
|
</script> |
@ -0,0 +1,16 @@ |
|||||
|
<template> |
||||
|
<SwitchRoot |
||||
|
:id="id" |
||||
|
v-model:checked="data" |
||||
|
class="relative flex h-6 w-10 cursor-default rounded-full bg-gray-200 shadow-sm focus-within:outline focus-within:outline-red-700 data-[state=checked]:bg-red-800 dark:bg-neutral-400" |
||||
|
> |
||||
|
<SwitchThumb |
||||
|
class="my-auto block h-4 w-4 translate-x-1 rounded-full bg-white shadow-sm transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[20px]" |
||||
|
/> |
||||
|
</SwitchRoot> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
defineProps<{ id?: string }>(); |
||||
|
const data = defineModel<boolean>(); |
||||
|
</script> |
@ -0,0 +1,44 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { |
||||
|
ToastAction, |
||||
|
ToastClose, |
||||
|
ToastDescription, |
||||
|
ToastRoot, |
||||
|
ToastTitle, |
||||
|
} from 'radix-vue'; |
||||
|
|
||||
|
defineExpose({ |
||||
|
publish, |
||||
|
}); |
||||
|
|
||||
|
const count = reactive<{ title: string; message: string }[]>([]); |
||||
|
|
||||
|
function publish(e: { title: string; message: string }) { |
||||
|
count.push({ title: e.title, message: e.message }); |
||||
|
console.log(count.length); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<ToastRoot |
||||
|
v-for="(e, i) in count" |
||||
|
:key="i" |
||||
|
class="data-[state=open]:animate-slideIn data-[state=closed]:animate-hide data-[swipe=end]:animate-swipeOut grid grid-cols-[auto_max-content] items-center gap-x-[15px] rounded-md bg-red-800 p-[15px] text-neutral-200 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] [grid-template-areas:_'title_action'_'description_action'] data-[swipe=cancel]:translate-x-0 data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:transition-[transform_200ms_ease-out]" |
||||
|
> |
||||
|
<ToastTitle |
||||
|
class="text-slate12 mb-[5px] text-[15px] font-medium [grid-area:_title]" |
||||
|
> |
||||
|
{{ e.title }} |
||||
|
</ToastTitle> |
||||
|
<ToastDescription |
||||
|
class="text-slate11 m-0 text-[13px] leading-[1.3] [grid-area:_description]" |
||||
|
>{{ e.message }}</ToastDescription |
||||
|
> |
||||
|
<ToastAction as-child alt-text="toast" class="[grid-area:_action]"> |
||||
|
<slot /> |
||||
|
</ToastAction> |
||||
|
<ToastClose aria-label="Close"> |
||||
|
<span aria-hidden>×</span> |
||||
|
</ToastClose> |
||||
|
</ToastRoot> |
||||
|
</template> |
@ -0,0 +1,11 @@ |
|||||
|
<template> |
||||
|
<input |
||||
|
:value="label" |
||||
|
type="button" |
||||
|
class="col-span-2 rounded-lg border-2 border-gray-100 py-2 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
defineProps<{ label: string }>(); |
||||
|
</script> |
@ -0,0 +1,38 @@ |
|||||
|
<template> |
||||
|
<div class="flex flex-col"> |
||||
|
<div v-for="(item, i) in data" :key="item"> |
||||
|
<input |
||||
|
:value="item" |
||||
|
type="text" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
@input="update(i)" |
||||
|
/> |
||||
|
<input type="button" value="-" @click="del(i)" /> |
||||
|
</div> |
||||
|
<input type="button" value="Add" @click="add" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const data = defineModel<string[] | null>(); |
||||
|
|
||||
|
function update(i: number) { |
||||
|
return (v: string) => { |
||||
|
if (!data.value) { |
||||
|
return; |
||||
|
} |
||||
|
data.value[i] = v; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function add() { |
||||
|
data.value?.push(''); |
||||
|
} |
||||
|
|
||||
|
function del(i: number) { |
||||
|
if (!data.value) { |
||||
|
return; |
||||
|
} |
||||
|
data.value.splice(i, 1); |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,9 @@ |
|||||
|
<template> |
||||
|
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
||||
|
<slot /> |
||||
|
<Separator |
||||
|
decorative |
||||
|
class="col-span-2 h-px w-full bg-gray-100 dark:bg-neutral-600" |
||||
|
/> |
||||
|
</section> |
||||
|
</template> |
@ -0,0 +1,5 @@ |
|||||
|
<template> |
||||
|
<h4 class="col-span-full py-6 text-2xl"> |
||||
|
<slot /> |
||||
|
</h4> |
||||
|
</template> |
@ -0,0 +1,17 @@ |
|||||
|
<template> |
||||
|
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
||||
|
{{ label }} |
||||
|
</Label> |
||||
|
<input |
||||
|
:id="id" |
||||
|
v-model.number="data" |
||||
|
type="number" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
defineProps<{ id: string; label: string }>(); |
||||
|
|
||||
|
const data = defineModel<number>(); |
||||
|
</script> |
@ -0,0 +1,11 @@ |
|||||
|
<template> |
||||
|
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
||||
|
{{ label }} |
||||
|
</Label> |
||||
|
<BaseSwitch :id="id" v-model="data" /> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
defineProps<{ id: string; label: string }>(); |
||||
|
const data = defineModel<boolean>(); |
||||
|
</script> |
@ -0,0 +1,17 @@ |
|||||
|
<template> |
||||
|
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
||||
|
{{ label }} |
||||
|
</Label> |
||||
|
<input |
||||
|
:id="id" |
||||
|
v-model="data" |
||||
|
type="text" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
defineProps<{ id: string; label: string }>(); |
||||
|
|
||||
|
const data = defineModel<string>(); |
||||
|
</script> |
@ -0,0 +1,16 @@ |
|||||
|
<template> |
||||
|
<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')" |
||||
|
@update:pressed="globalStore.toggleCharts" |
||||
|
> |
||||
|
<IconsChart |
||||
|
class="h-5 w-5 fill-gray-400 transition group-data-[state=on]:fill-gray-600 dark:fill-neutral-600 dark:group-data-[state=on]:fill-neutral-400" |
||||
|
/> |
||||
|
</Toggle> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const globalStore = useGlobalStore(); |
||||
|
</script> |
@ -0,0 +1,11 @@ |
|||||
|
<template> |
||||
|
<NuxtLink to="/" class="mb-4 flex-grow self-start"> |
||||
|
<h1 class="text-4xl font-medium dark:text-neutral-200"> |
||||
|
<img |
||||
|
src="/logo.png" |
||||
|
width="32" |
||||
|
class="dark:bg mr-2 inline align-middle" |
||||
|
/><span class="align-middle">WireGuard</span> |
||||
|
</h1> |
||||
|
</NuxtLink> |
||||
|
</template> |
@ -0,0 +1,28 @@ |
|||||
|
<template> |
||||
|
<button |
||||
|
class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600" |
||||
|
:title="$t(`theme.${theme.preference}`)" |
||||
|
@click="toggleTheme" |
||||
|
> |
||||
|
<IconsSun v-if="theme.preference === 'light'" class="h-5 w-5" /> |
||||
|
<IconsMoon |
||||
|
v-else-if="theme.preference === 'dark'" |
||||
|
class="h-5 w-5 text-neutral-400" |
||||
|
/> |
||||
|
<IconsHalfMoon v-else class="h-5 w-5 fill-gray-600 dark:fill-neutral-400" /> |
||||
|
</button> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const theme = useTheme(); |
||||
|
|
||||
|
function toggleTheme() { |
||||
|
const themeCycle = { |
||||
|
system: 'light', |
||||
|
light: 'dark', |
||||
|
dark: 'system', |
||||
|
} as const; |
||||
|
|
||||
|
theme.preference = themeCycle[theme.preference]; |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,27 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
v-if="globalStore.updateAvailable && globalStore.latestRelease" |
||||
|
class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600" |
||||
|
:title="`v${globalStore.currentRelease} → v${globalStore.latestRelease.version}`" |
||||
|
> |
||||
|
<div class="container mx-auto flex flex-auto flex-row 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="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') }} → |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const globalStore = useGlobalStore(); |
||||
|
globalStore.fetchRelease(); |
||||
|
</script> |
@ -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="m11.25 9-3 3m0 0 3 3m-3-3h7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</template> |
@ -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="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</template> |
@ -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> |
@ -0,0 +1,5 @@ |
|||||
|
<template> |
||||
|
<div class="m-4"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</template> |
@ -0,0 +1,7 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
class="container mx-auto max-w-3xl overflow-hidden rounded-lg bg-white px-3 text-gray-700 shadow-md md:px-0 dark:bg-neutral-700 dark:text-neutral-200" |
||||
|
> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</template> |
@ -0,0 +1,5 @@ |
|||||
|
<template> |
||||
|
<div class="flex flex-shrink-0 space-x-1 md:block"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</template> |
@ -0,0 +1,7 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
class="flex flex-auto flex-grow flex-row items-center border-b-2 border-gray-100 p-3 px-5 dark:border-neutral-600" |
||||
|
> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</template> |
@ -0,0 +1,11 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
const { text } = defineProps<{ |
||||
|
text: string; |
||||
|
}>(); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<h2 class="flex-1 text-2xl font-medium"> |
||||
|
{{ text }} |
||||
|
</h2> |
||||
|
</template> |
@ -0,0 +1,8 @@ |
|||||
|
<template> |
||||
|
<h1 |
||||
|
class="my-16 text-center text-4xl font-medium text-gray-700 dark:text-neutral-200" |
||||
|
> |
||||
|
<img src="/logo.png" width="32" class="dark:bg inline align-middle" /> |
||||
|
<span class="align-middle">WireGuard</span> |
||||
|
</h1> |
||||
|
</template> |
@ -0,0 +1,46 @@ |
|||||
|
<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="Customise 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"> |
||||
|
import { LOCALES } from '#shared/locales'; |
||||
|
|
||||
|
const { locale } = useI18n(); |
||||
|
const emit = defineEmits(['update:lang']); |
||||
|
|
||||
|
const langProxy = ref(locale); |
||||
|
|
||||
|
watch(langProxy, (newVal) => { |
||||
|
emit('update:lang', newVal); |
||||
|
}); |
||||
|
|
||||
|
const langs = LOCALES.sort((a, b) => a.code.localeCompare(b.code)); |
||||
|
</script> |
@ -0,0 +1,23 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
v-for="n in totalSteps" |
||||
|
:key="n" |
||||
|
:class="[ |
||||
|
'step mx-3 h-[3px] grow', |
||||
|
step >= n ? 'bg-red-800 dark:bg-white' : 'bg-gray-500', |
||||
|
]" |
||||
|
></div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps({ |
||||
|
step: { |
||||
|
type: Number, |
||||
|
required: true, |
||||
|
}, |
||||
|
totalSteps: { |
||||
|
type: Number, |
||||
|
default: 5, |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
@ -0,0 +1,85 @@ |
|||||
|
<template> |
||||
|
<DropdownMenuRoot v-model:open="toggleState"> |
||||
|
<DropdownMenuTrigger> |
||||
|
<button |
||||
|
class="flex items-center rounded-full pe-1 text-sm font-medium text-gray-400 hover:text-red-800 focus:ring-4 focus:ring-gray-100 md:me-0 dark:text-neutral-400 dark:hover:text-red-800 dark:focus:ring-gray-700" |
||||
|
type="button" |
||||
|
> |
||||
|
<BaseAvatar class="h-8 w-8"> |
||||
|
{{ fallbackName }} |
||||
|
</BaseAvatar> |
||||
|
{{ authStore.userData?.name }} |
||||
|
</button> |
||||
|
</DropdownMenuTrigger> |
||||
|
|
||||
|
<DropdownMenuPortal> |
||||
|
<DropdownMenuContent |
||||
|
:side-offset="5" |
||||
|
class="z-10 w-44 divide-y divide-gray-100 rounded-lg bg-white text-gray-700 shadow dark:divide-neutral-800 dark:bg-neutral-700 dark:text-gray-200" |
||||
|
> |
||||
|
<DropdownMenuItem> |
||||
|
<div class="truncate">{{ authStore.userData?.name }}</div> |
||||
|
<div class="truncate">@{{ authStore.userData?.username }}</div> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem> |
||||
|
<NuxtLink |
||||
|
to="/" |
||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" |
||||
|
> |
||||
|
Clients |
||||
|
</NuxtLink> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem> |
||||
|
<NuxtLink |
||||
|
to="/me" |
||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" |
||||
|
> |
||||
|
Account |
||||
|
</NuxtLink> |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem v-if="authStore.userData?.role === 'ADMIN'"> |
||||
|
<NuxtLink |
||||
|
to="/admin" |
||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" |
||||
|
> |
||||
|
Admin Panel |
||||
|
</NuxtLink> |
||||
|
</DropdownMenuItem> |
||||
|
<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" |
||||
|
> |
||||
|
<IconsLogout class="h-5" /> |
||||
|
{{ $t('logout') }} |
||||
|
</button> |
||||
|
</DropdownMenuItem> |
||||
|
</DropdownMenuContent> |
||||
|
</DropdownMenuPortal> |
||||
|
</DropdownMenuRoot> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
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 fallbackName = computed(() => { |
||||
|
return authStore.userData?.name |
||||
|
.split(' ') |
||||
|
.map((word) => word.charAt(0).toUpperCase()) |
||||
|
.slice(0, 2) |
||||
|
.join(''); |
||||
|
}); |
||||
|
</script> |
@ -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> |
|
@ -1,124 +0,0 @@ |
|||||
<template> |
|
||||
<header class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6"> |
|
||||
<div |
|
||||
:class=" |
|
||||
isLoginPage |
|
||||
? 'flex justify-end' |
|
||||
: 'flex flex-col-reverse xxs:flex-row flex-auto items-center gap-3' |
|
||||
" |
|
||||
> |
|
||||
<h1 |
|
||||
v-if="isLoginPage" |
|
||||
class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4" |
|
||||
> |
|
||||
<img |
|
||||
src="/logo.png" |
|
||||
width="32" |
|
||||
class="inline align-middle dark:bg mr-2" |
|
||||
/><span class="align-middle">WireGuard</span> |
|
||||
</h1> |
|
||||
<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.features.trafficStats.type > 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> |
|
||||
<span |
|
||||
v-if="authStore.requiresPassword && !isLoginPage" |
|
||||
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline" |
|
||||
@click="logout" |
|
||||
> |
|
||||
{{ $t('logout') }} |
|
||||
<IconsLogout class="h-3 inline" /> |
|
||||
</span> |
|
||||
</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 authStore = useAuthStore(); |
|
||||
const globalStore = useGlobalStore(); |
|
||||
const route = useRoute(); |
|
||||
|
|
||||
const isLoginPage = computed(() => route.path == '/login'); |
|
||||
|
|
||||
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'); |
|
||||
} |
|
||||
|
|
||||
async function logout(e: Event) { |
|
||||
e.preventDefault(); |
|
||||
try { |
|
||||
await authStore.logout(); |
|
||||
navigateTo('/login'); |
|
||||
} catch (err) { |
|
||||
if (err instanceof Error) { |
|
||||
// TODO: better ui |
|
||||
alert(err.message || err.toString()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
@ -0,0 +1,68 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<header class="container mx-auto mt-4 max-w-3xl px-3 xs:mt-6 md:px-0"> |
||||
|
<div |
||||
|
class="mb-5" |
||||
|
:class=" |
||||
|
hasOwnLogo |
||||
|
? 'flex justify-end' |
||||
|
: 'flex flex-auto flex-col-reverse items-center gap-3 xxs:flex-row' |
||||
|
" |
||||
|
> |
||||
|
<HeaderLogo v-if="!hasOwnLogo" /> |
||||
|
<div class="flex grow-0 items-center gap-3 self-end xxs:self-center"> |
||||
|
<HeaderThemeSwitch /> |
||||
|
<HeaderChartToggle /> |
||||
|
<UiUserMenu v-if="loggedIn" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<HeaderUpdate class="mt-5" /> |
||||
|
</header> |
||||
|
<slot /> |
||||
|
<footer> |
||||
|
<p class="m-10 text-center text-xs text-gray-300 dark:text-neutral-600"> |
||||
|
<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://emile.nl/?ref=wg-easy" |
||||
|
>Emile Nijssen</a |
||||
|
> |
||||
|
is licensed under |
||||
|
<a |
||||
|
class="hover:underline" |
||||
|
target="_blank" |
||||
|
href="https://spdx.org/licenses/AGPL-3.0-only.html" |
||||
|
>AGPL-3.0-only</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' |
||||
|
); |
||||
|
</script> |
@ -0,0 +1,29 @@ |
|||||
|
<template> |
||||
|
<main class="container mx-auto px-4"> |
||||
|
<UiBanner /> |
||||
|
<Panel> |
||||
|
<PanelBody class="mx-auto mt-10 p-4 md:w-[70%] lg:w-[60%]"> |
||||
|
<h2 class="mb-16 mt-8 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> |
||||
|
|
||||
|
<BaseToast ref="toast" /> |
||||
|
</main> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const setupStore = useSetupStore(); |
||||
|
const savedRef = useTemplateRef('toast'); |
||||
|
setupStore.setErrorRef(savedRef); |
||||
|
</script> |
@ -0,0 +1,53 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<div class="container mx-auto p-4"> |
||||
|
<div class="flex"> |
||||
|
<div class="mr-4 w-64 rounded-lg bg-white p-4 dark:bg-neutral-700"> |
||||
|
<NuxtLink to="/admin"> |
||||
|
<h2 class="mb-4 text-xl font-bold dark:text-neutral-200"> |
||||
|
Admin Panel |
||||
|
</h2> |
||||
|
</NuxtLink> |
||||
|
<div class="flex flex-col space-y-2"> |
||||
|
<NuxtLink |
||||
|
v-for="(item, index) in menuItems" |
||||
|
:key="index" |
||||
|
:to="`/admin/${item.id}`" |
||||
|
> |
||||
|
<BaseButton |
||||
|
class="w-full cursor-pointer rounded p-2 font-medium transition-colors duration-200 hover:bg-red-800 dark:text-neutral-200" |
||||
|
> |
||||
|
{{ item.name }} |
||||
|
</BaseButton> |
||||
|
</NuxtLink> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="flex-1 rounded-lg bg-white p-6 dark:bg-neutral-700 dark:text-neutral-200" |
||||
|
> |
||||
|
<h1 class="mb-6 text-3xl font-bold">{{ activeMenuItem?.name }}</h1> |
||||
|
<NuxtPage /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const authStore = useAuthStore(); |
||||
|
authStore.update(); |
||||
|
|
||||
|
const route = useRoute(); |
||||
|
|
||||
|
const menuItems = [ |
||||
|
{ id: '', name: 'General' }, |
||||
|
{ id: 'defaults', name: 'Defaults' }, |
||||
|
{ id: 'interface', name: 'Interface' }, |
||||
|
{ id: 'metrics', name: 'Metrics' }, |
||||
|
]; |
||||
|
|
||||
|
const activeMenuItem = computed(() => { |
||||
|
return menuItems.find((item) => route.path === `/admin/${item.id}`); |
||||
|
}); |
||||
|
</script> |
@ -0,0 +1,24 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Connection</FormHeading> |
||||
|
<FormTextField id="host" label="Host" /> |
||||
|
<FormTextField id="port" label="Port" /> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Allowed IPs</FormHeading> |
||||
|
<FormArrayField /> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>DNS</FormHeading> |
||||
|
<FormArrayField /> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Advanced</FormHeading> |
||||
|
<FormNumberField id="mtu" label="MTU" /> |
||||
|
<FormNumberField id="keepalive" label="Persistent Keepalive" /> |
||||
|
</FormGroup> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"></script> |
@ -0,0 +1,9 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<FormGroup> |
||||
|
<FormNumberField id="session" label="Session Timeout" /> |
||||
|
</FormGroup> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"></script> |
@ -0,0 +1,19 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Interface Settings</FormHeading> |
||||
|
<FormNumberField id="mtu" label="MTU" /> |
||||
|
<FormNumberField id="port" label="Port" /> |
||||
|
<FormTextField id="device" label="Device" /> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Scripts</FormHeading> |
||||
|
<FormTextField id="mtu" label="PreUp" /> |
||||
|
<FormTextField id="port" label="PostUp" /> |
||||
|
<FormTextField id="device" label="PreDown" /> |
||||
|
<FormTextField id="device" label="PostDown" /> |
||||
|
</FormGroup> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"></script> |
@ -0,0 +1,3 @@ |
|||||
|
<template><div></div></template> |
||||
|
|
||||
|
<script lang="ts" setup></script> |
@ -0,0 +1,66 @@ |
|||||
|
<template> |
||||
|
<main v-if="data"> |
||||
|
<Panel> |
||||
|
<PanelHead> |
||||
|
<PanelHeadTitle :text="data.name" /> |
||||
|
</PanelHead> |
||||
|
<PanelBody> |
||||
|
<FormGroup> |
||||
|
<FormHeading> |
||||
|
{{ $t('me.sectionGeneral') }} |
||||
|
</FormHeading> |
||||
|
<FormTextField id="name" v-model.trim="data.name" label="Name" /> |
||||
|
<FormSwitchField |
||||
|
id="enabled" |
||||
|
v-model="data.enabled" |
||||
|
label="Enabled" |
||||
|
/> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Address</FormHeading> |
||||
|
<FormTextField id="ipv4" v-model.trim="data.address4" label="IPv4" /> |
||||
|
<FormTextField id="ipv6" v-model.trim="data.address6" label="IPv6" /> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Allowed IPs</FormHeading> |
||||
|
<FormArrayField v-model="data.allowedIPs" /> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Server Allowed IPs</FormHeading> |
||||
|
<FormArrayField v-model="data.serverAllowedIPs" /> |
||||
|
</FormGroup> |
||||
|
<FormGroup></FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Advanced</FormHeading> |
||||
|
<FormNumberField id="mtu" v-model="data.mtu" label="MTU" /> |
||||
|
<FormNumberField |
||||
|
id="keepalive" |
||||
|
v-model="data.persistentKeepalive" |
||||
|
label="Persistent Keepalive" |
||||
|
/> |
||||
|
</FormGroup> |
||||
|
<FormGroup> |
||||
|
<FormHeading>Actions</FormHeading> |
||||
|
<FormActionField label="Delete!" /> |
||||
|
<FormActionField label="Revert!" @click="revert" /> |
||||
|
</FormGroup> |
||||
|
</PanelBody> |
||||
|
</Panel> |
||||
|
</main> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const authStore = useAuthStore(); |
||||
|
authStore.update(); |
||||
|
const route = useRoute(); |
||||
|
const id = route.params.id as string; |
||||
|
const { data: _data, refresh } = await useFetch(`/api/client/${id}`, { |
||||
|
method: 'get', |
||||
|
}); |
||||
|
const data = toRef(_data.value); |
||||
|
|
||||
|
async function revert() { |
||||
|
await refresh(); |
||||
|
data.value = toRef(_data.value).value; |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,120 @@ |
|||||
|
<template> |
||||
|
<main> |
||||
|
<Panel> |
||||
|
<PanelHead> |
||||
|
<PanelHeadTitle :text="$t('pages.me')" /> |
||||
|
</PanelHead> |
||||
|
<PanelBody class="dark:text-neutral-200"> |
||||
|
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
||||
|
<h4 class="col-span-full py-6 text-2xl"> |
||||
|
{{ $t('me.sectionGeneral') }} |
||||
|
</h4> |
||||
|
<Label |
||||
|
for="username" |
||||
|
class="font-semibold md:align-middle md:leading-10" |
||||
|
> |
||||
|
{{ $t('username') }} |
||||
|
</Label> |
||||
|
<input |
||||
|
id="username" |
||||
|
v-model.trim="username" |
||||
|
type="text" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
<Label for="name" class="font-semibold md:align-middle md:leading-10"> |
||||
|
{{ $t('name') }} |
||||
|
</Label> |
||||
|
<input |
||||
|
id="name" |
||||
|
v-model.trim="name" |
||||
|
type="text" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
<Label |
||||
|
for="email" |
||||
|
class="font-semibold md:align-middle md:leading-10" |
||||
|
> |
||||
|
{{ $t('email') }} |
||||
|
</Label> |
||||
|
<input |
||||
|
id="email" |
||||
|
v-model.trim="email" |
||||
|
type="email" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
<div class="col-span-full"> |
||||
|
<BaseButton @click="submit">{{ $t('save') }}</BaseButton> |
||||
|
</div> |
||||
|
</section> |
||||
|
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
||||
|
<h4 class="col-span-full py-6 text-2xl"> |
||||
|
{{ $t('me.sectionPassword') }} |
||||
|
</h4> |
||||
|
<Label |
||||
|
for="current-password" |
||||
|
class="font-semibold md:align-middle md:leading-10" |
||||
|
> |
||||
|
{{ $t('currentPassword') }} |
||||
|
</Label> |
||||
|
<input |
||||
|
id="current-password" |
||||
|
v-model.trim="currentPassword" |
||||
|
type="password" |
||||
|
autocomplete="current-password" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
<Label |
||||
|
for="new-password" |
||||
|
class="font-semibold md:align-middle md:leading-10" |
||||
|
> |
||||
|
{{ $t('setup.newPassword') }} |
||||
|
</Label> |
||||
|
<input |
||||
|
id="new-password" |
||||
|
v-model.trim="newPassword" |
||||
|
type="password" |
||||
|
autocomplete="new-password" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
<Label |
||||
|
for="confirm-password" |
||||
|
class="font-semibold md:align-middle md:leading-10" |
||||
|
> |
||||
|
{{ $t('confirmPassword') }} |
||||
|
</Label> |
||||
|
<input |
||||
|
id="confirm-password" |
||||
|
v-model.trim="confirmPassword" |
||||
|
type="password" |
||||
|
autocomplete="new-password" |
||||
|
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
||||
|
/> |
||||
|
<div class="col-span-full"> |
||||
|
<BaseButton @click="updatePassword">{{ |
||||
|
$t('updatePassword') |
||||
|
}}</BaseButton> |
||||
|
</div> |
||||
|
</section> |
||||
|
</PanelBody> |
||||
|
</Panel> |
||||
|
</main> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const authStore = useAuthStore(); |
||||
|
authStore.update(); |
||||
|
|
||||
|
const username = ref(authStore.userData?.username); |
||||
|
const name = ref(authStore.userData?.name); |
||||
|
const email = ref(authStore.userData?.email); |
||||
|
|
||||
|
// TODO: handle update password |
||||
|
|
||||
|
const currentPassword = ref(authStore.userData?.email); |
||||
|
const newPassword = ref(authStore.userData?.email); |
||||
|
const confirmPassword = ref(authStore.userData?.email); |
||||
|
|
||||
|
function submit() {} |
||||
|
|
||||
|
function updatePassword() {} |
||||
|
</script> |
@ -1,106 +0,0 @@ |
|||||
<template> |
|
||||
<main class="container mx-auto px-4"> |
|
||||
<h1 |
|
||||
class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center" |
|
||||
> |
|
||||
<img src="/logo.png" width="32" class="inline align-middle dark:bg" /> |
|
||||
<span class="align-middle">WireGuard</span> |
|
||||
</h1> |
|
||||
<div |
|
||||
class="flex flex-col items-center lg:w-[60%] mx-auto shadow rounded-md bg-white dark:bg-neutral-700 p-5 overflow-hidden mt-10 text-gray-700 dark:text-neutral-200" |
|
||||
> |
|
||||
<h2 |
|
||||
class="mt-8 mb-16 text-3xl font-medium text-gray-700 dark:text-neutral-200" |
|
||||
> |
|
||||
{{ $t('setup.welcome') }} |
|
||||
</h2> |
|
||||
<p class="text-lg p-8">{{ $t('setup.msg') }}</p> |
|
||||
<form class="mb-8" @submit="newAccount"> |
|
||||
<div> |
|
||||
<label for="username" class="inline-block py-2">{{ |
|
||||
$t('username') |
|
||||
}}</label> |
|
||||
<input |
|
||||
id="username" |
|
||||
v-model="username" |
|
||||
type="text" |
|
||||
name="username" |
|
||||
autocomplete="username" |
|
||||
: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" |
|
||||
required="true" |
|
||||
/> |
|
||||
<small v-if="errorCU" class="text-danger">{{ |
|
||||
$t('setup.usernameCondition') |
|
||||
}}</small> |
|
||||
</div> |
|
||||
<div> |
|
||||
<label for="password" class="inline-block py-2">{{ |
|
||||
$t('setup.newPassword') |
|
||||
}}</label> |
|
||||
<input |
|
||||
id="password" |
|
||||
v-model="password" |
|
||||
type="password" |
|
||||
name="password" |
|
||||
autocomplete="new-password" |
|
||||
: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" |
|
||||
required="true" |
|
||||
/> |
|
||||
<small v-if="errorPWD" class="text-danger">{{ |
|
||||
$t('setup.passwordCondition') |
|
||||
}}</small> |
|
||||
</div> |
|
||||
<div> |
|
||||
<label for="accept" class="inline-block my-4 mr-4">{{ |
|
||||
$t('setup.accept') |
|
||||
}}</label> |
|
||||
<input id="accept" type="checkbox" name="accept" required="true" /> |
|
||||
</div> |
|
||||
<button |
|
||||
type="submit" |
|
||||
:class="[ |
|
||||
{ |
|
||||
'bg-red-800 dark:bg-red-800 hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer': |
|
||||
password && username, |
|
||||
'bg-gray-200 dark:bg-neutral-800 cursor-not-allowed': |
|
||||
!password && !username, |
|
||||
}, |
|
||||
'w-max px-4 rounded shadow py-2 text-sm text-white dark:text-white', |
|
||||
]" |
|
||||
> |
|
||||
{{ $t('setup.submitBtn') }} |
|
||||
</button> |
|
||||
</form> |
|
||||
</div> |
|
||||
</main> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
const username = ref<null | string>(null); |
|
||||
const password = ref<null | string>(null); |
|
||||
const authStore = useAuthStore(); |
|
||||
|
|
||||
const errorCU = ref(false); |
|
||||
const errorPWD = ref(false); |
|
||||
|
|
||||
async function newAccount(e: Event) { |
|
||||
e.preventDefault(); |
|
||||
|
|
||||
if (!username.value || !password.value) return; |
|
||||
|
|
||||
try { |
|
||||
const res = await authStore.signup(username.value, password.value); |
|
||||
if (res) { |
|
||||
navigateTo('/login'); |
|
||||
} |
|
||||
} catch (error) { |
|
||||
if (error instanceof Error) { |
|
||||
// TODO: replace alert with actual ui error message |
|
||||
// TODO: also use errorCU & errorPWD to show prompt error |
|
||||
alert(error.message || error.toString()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
@ -0,0 +1,42 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="p-8 text-center text-lg"> |
||||
|
{{ $t('setup.messageSetupLanguage') }} |
||||
|
</p> |
||||
|
<div class="mb-8 flex justify-center"> |
||||
|
<UiChooseLang @update:lang="handleEventUpdateLang" /> |
||||
|
</div> |
||||
|
<div><BaseButton @click="updateLang">Continue</BaseButton></div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { FetchError } from 'ofetch'; |
||||
|
|
||||
|
definePageMeta({ |
||||
|
layout: 'setup', |
||||
|
}); |
||||
|
|
||||
|
const { t, locale, setLocale } = useI18n(); |
||||
|
|
||||
|
function handleEventUpdateLang(value: string) { |
||||
|
setLocale(value); |
||||
|
} |
||||
|
|
||||
|
const setupStore = useSetupStore(); |
||||
|
setupStore.setStep(1); |
||||
|
const router = useRouter(); |
||||
|
async function updateLang() { |
||||
|
try { |
||||
|
await setupStore.step1(locale.value); |
||||
|
router.push('/setup/2'); |
||||
|
} catch (error) { |
||||
|
if (error instanceof FetchError) { |
||||
|
setupStore.handleError({ |
||||
|
title: t('setup.requirements'), |
||||
|
message: error.data.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,19 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="p-8 text-center text-lg"> |
||||
|
{{ 'Do you have a existing Setup?' }} |
||||
|
</p> |
||||
|
<div class="mb-8 flex justify-center"> |
||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="px-8 pt-8 text-center text-2xl"> |
||||
|
{{ $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> |
@ -0,0 +1,79 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="p-8 text-center text-lg"> |
||||
|
{{ $t('setup.messageSetupCreateAdminUser') }} |
||||
|
</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> |
||||
|
<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(4); |
||||
|
const router = useRouter(); |
||||
|
const username = ref<null | string>(null); |
||||
|
const password = ref<null | string>(null); |
||||
|
const accept = ref<boolean>(true); |
||||
|
|
||||
|
async function newAccount() { |
||||
|
try { |
||||
|
if (!username.value || !password.value) { |
||||
|
setupStore.handleError({ |
||||
|
title: t('setup.requirements'), |
||||
|
message: t('setup.emptyFields'), |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await setupStore.step4(username.value, password.value, accept.value); |
||||
|
await router.push('/setup/5'); |
||||
|
} catch (error) { |
||||
|
if (error instanceof FetchError) { |
||||
|
setupStore.handleError({ |
||||
|
title: t('setup.requirements'), |
||||
|
message: error.data.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,67 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="p-8 text-center text-lg"> |
||||
|
{{ $t('setup.messageSetupHostPort') }} |
||||
|
</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> |
||||
|
<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(5); |
||||
|
const router = useRouter(); |
||||
|
const host = ref<null | string>(null); |
||||
|
const port = ref<number>(51820); |
||||
|
|
||||
|
async function updateHostPort() { |
||||
|
if (!host.value || !port.value) { |
||||
|
setupStore.handleError({ |
||||
|
title: t('setup.requirements'), |
||||
|
message: t('setup.emptyFields'), |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await setupStore.step5(host.value, port.value); |
||||
|
await router.push('/setup/success'); |
||||
|
} catch (error) { |
||||
|
if (error instanceof FetchError) { |
||||
|
setupStore.handleError({ |
||||
|
title: t('setup.requirements'), |
||||
|
message: error.data.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,78 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<p class="p-8 text-center text-lg"> |
||||
|
{{ $t('setup.messageSetupMigration') }} |
||||
|
</p> |
||||
|
<div> |
||||
|
<Label for="migration">{{ $t('setup.migration') }}</Label> |
||||
|
<input id="migration" type="file" @change="onChangeFile" /> |
||||
|
</div> |
||||
|
<BaseButton @click="sendFile">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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
|
||||
|
async function sendFile() { |
||||
|
if (!backupFile.value) { |
||||
|
setupStore.handleError({ |
||||
|
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) { |
||||
|
setupStore.handleError({ |
||||
|
title: t('setup.requirements'), |
||||
|
message: error.data.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function readFileContent(file: File): Promise<string> { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const reader = new FileReader(); |
||||
|
|
||||
|
reader.onload = (event) => { |
||||
|
resolve(event.target?.result as string); |
||||
|
}; |
||||
|
reader.onerror = (error) => { |
||||
|
reject(error); |
||||
|
}; |
||||
|
reader.readAsText(file); |
||||
|
}); |
||||
|
} |
||||
|
</script> |
@ -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> |
@ -0,0 +1,84 @@ |
|||||
|
import { defineStore } from 'pinia'; |
||||
|
|
||||
|
export const useSetupStore = defineStore('Setup', () => { |
||||
|
/** |
||||
|
* @throws if unsuccessful |
||||
|
*/ |
||||
|
async function step1(lang: string) { |
||||
|
const response = await $fetch('/api/setup/1', { |
||||
|
method: 'post', |
||||
|
body: { lang }, |
||||
|
}); |
||||
|
return response.success; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @throws if unsuccessful |
||||
|
*/ |
||||
|
async function step4(username: string, password: string, accept: boolean) { |
||||
|
const response = await $fetch('/api/setup/4', { |
||||
|
method: 'post', |
||||
|
body: { username, password, accept }, |
||||
|
}); |
||||
|
return response.success; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @throws if unsuccessful |
||||
|
*/ |
||||
|
async function step5(host: string, port: number) { |
||||
|
const response = await $fetch('/api/setup/5', { |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
type SetupError = { |
||||
|
title: string; |
||||
|
message: string; |
||||
|
}; |
||||
|
|
||||
|
type ErrorRef = { |
||||
|
value: { publish: (e: SetupError) => void } | null; |
||||
|
}; |
||||
|
|
||||
|
const errorRef = ref<null | ErrorRef>(null); |
||||
|
|
||||
|
function setErrorRef(a: ErrorRef | null) { |
||||
|
errorRef.value = a; |
||||
|
} |
||||
|
|
||||
|
function handleError(e: SetupError) { |
||||
|
errorRef.value?.value?.publish(e); |
||||
|
} |
||||
|
|
||||
|
const step = ref(1); |
||||
|
const totalSteps = ref(6); |
||||
|
function setStep(i: number) { |
||||
|
step.value = i; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
step1, |
||||
|
step4, |
||||
|
step5, |
||||
|
runMigration, |
||||
|
setErrorRef, |
||||
|
handleError, |
||||
|
step, |
||||
|
totalSteps, |
||||
|
setStep, |
||||
|
}; |
||||
|
}); |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue