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", |
|||
"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