mirror of https://github.com/wg-easy/wg-easy
Browse Source
fixup shutdown wireguard properly fix styling, fix store split even more clear interval split even more split even more handle auth middleware on server avoid flicker of login pagepull/1244/head
36 changed files with 1414 additions and 1305 deletions
@ -1,6 +0,0 @@ |
|||
[v-cloak] { |
|||
display: none; |
|||
} |
|||
.line-chart .apexcharts-svg { |
|||
transform: translateY(3px); |
|||
} |
@ -0,0 +1,71 @@ |
|||
<template> |
|||
<span class="group"> |
|||
<!-- Show --> |
|||
<input |
|||
v-show="clientEditAddressId === client.id" |
|||
ref="clientAddressInput" |
|||
v-model="clientEditAddress" |
|||
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=" |
|||
updateClientAddress(client, clientEditAddress); |
|||
clientEditAddress = null; |
|||
clientEditAddressId = null; |
|||
" |
|||
@keyup.escape=" |
|||
clientEditAddress = null; |
|||
clientEditAddressId = null; |
|||
" |
|||
/> |
|||
<span v-show="clientEditAddressId !== client.id" class="inline-block">{{ |
|||
client.address |
|||
}}</span> |
|||
|
|||
<!-- Edit --> |
|||
<span |
|||
v-show="clientEditAddressId !== client.id" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|||
@click=" |
|||
clientEditAddress = client.address; |
|||
clientEditAddressId = client.id; |
|||
nextTick(() => clientAddressInput?.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> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const clientsStore = useClientsStore(); |
|||
|
|||
const clientAddressInput = ref<HTMLInputElement | null>(null); |
|||
const clientEditAddress = ref<null | string>(null); |
|||
const clientEditAddressId = ref<null | string>(null); |
|||
|
|||
function updateClientAddress(client: WGClient, address: string | null) { |
|||
if (address === null) { |
|||
return; |
|||
} |
|||
api |
|||
.updateClientAddress({ clientId: client.id, address }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
} |
|||
</script> |
@ -0,0 +1,42 @@ |
|||
<template> |
|||
<div class="h-10 w-10 mt-2 self-start rounded-full bg-gray-50 relative"> |
|||
<svg |
|||
class="w-6 m-2 text-gray-300" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
<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> |
@ -0,0 +1,140 @@ |
|||
<template> |
|||
<div |
|||
v-if="globalStore.uiChartType" |
|||
:class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${globalStore.uiChartType === 1 && 'line-chart'}`" |
|||
> |
|||
<ClientOnly> |
|||
<Chart :options="chartOptionsTX" :series="client.transferTxSeries" /> |
|||
</ClientOnly> |
|||
</div> |
|||
<div |
|||
v-if="globalStore.uiChartType" |
|||
:class="`absolute z-0 top-0 left-0 right-0 h-6 ${globalStore.uiChartType === 1 && 'line-chart'}`" |
|||
> |
|||
<ClientOnly> |
|||
<Chart |
|||
:options="chartOptionsRX" |
|||
:series="client.transferRxSeries" |
|||
style="transform: scaleY(-1)" |
|||
/> |
|||
</ClientOnly> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const globalStore = useGlobalStore(); |
|||
const theme = useTheme(); |
|||
|
|||
const chartOptionsTX = computed(() => { |
|||
const opts = { |
|||
...chartOptions, |
|||
colors: [CHART_COLORS.tx[theme.value]], |
|||
}; |
|||
opts.chart.type = UI_CHART_TYPES[globalStore.uiChartType].type || false; |
|||
opts.stroke.width = UI_CHART_TYPES[globalStore.uiChartType].strokeWidth; |
|||
return opts; |
|||
}); |
|||
|
|||
const chartOptionsRX = computed(() => { |
|||
const opts = { |
|||
...chartOptions, |
|||
colors: [CHART_COLORS.rx[theme.value]], |
|||
}; |
|||
opts.chart.type = UI_CHART_TYPES[globalStore.uiChartType].type || false; |
|||
opts.stroke.width = UI_CHART_TYPES[globalStore.uiChartType].strokeWidth; |
|||
return opts; |
|||
}); |
|||
|
|||
const chartOptions = { |
|||
chart: { |
|||
type: false as string | boolean, |
|||
background: 'transparent', |
|||
stacked: false, |
|||
toolbar: { |
|||
show: false, |
|||
}, |
|||
animations: { |
|||
enabled: false, |
|||
}, |
|||
parentHeightOffset: 0, |
|||
sparkline: { |
|||
enabled: true, |
|||
}, |
|||
}, |
|||
colors: [], |
|||
stroke: { |
|||
curve: 'smooth', |
|||
width: 0, |
|||
}, |
|||
fill: { |
|||
type: 'gradient', |
|||
gradient: { |
|||
shade: 'dark', |
|||
type: 'vertical', |
|||
shadeIntensity: 0, |
|||
gradientToColors: CHART_COLORS.gradient[theme.value], |
|||
inverseColors: false, |
|||
opacityTo: 0, |
|||
stops: [0, 100], |
|||
}, |
|||
}, |
|||
dataLabels: { |
|||
enabled: false, |
|||
}, |
|||
plotOptions: { |
|||
bar: { |
|||
horizontal: false, |
|||
}, |
|||
}, |
|||
xaxis: { |
|||
labels: { |
|||
show: false, |
|||
}, |
|||
axisTicks: { |
|||
show: false, |
|||
}, |
|||
axisBorder: { |
|||
show: false, |
|||
}, |
|||
}, |
|||
yaxis: { |
|||
labels: { |
|||
show: false, |
|||
}, |
|||
min: 0, |
|||
}, |
|||
tooltip: { |
|||
enabled: false, |
|||
}, |
|||
legend: { |
|||
show: false, |
|||
}, |
|||
grid: { |
|||
show: false, |
|||
padding: { |
|||
left: -10, |
|||
right: 0, |
|||
bottom: -15, |
|||
top: -15, |
|||
}, |
|||
column: { |
|||
opacity: 0, |
|||
}, |
|||
xaxis: { |
|||
lines: { |
|||
show: false, |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="css"> |
|||
.line-chart .apexcharts-svg { |
|||
transform: translateY(3px); |
|||
} |
|||
</style> |
@ -0,0 +1,55 @@ |
|||
<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" |
|||
> |
|||
<ClientAddress :client="client" /> |
|||
<ClientInlineTransfer |
|||
v-if="!globalStore.uiTrafficStats" |
|||
:client="client" |
|||
/> |
|||
<ClientLastSeen :client="client" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Info --> |
|||
<div |
|||
v-if="globalStore.uiTrafficStats" |
|||
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" /> |
|||
<ClientDelete :client="client" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const globalStore = useGlobalStore(); |
|||
</script> |
@ -0,0 +1,40 @@ |
|||
<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(); |
|||
} |
|||
" |
|||
> |
|||
<svg |
|||
class="w-5" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" |
|||
/> |
|||
</svg> |
|||
</a> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
</script> |
@ -0,0 +1,28 @@ |
|||
<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" |
|||
> |
|||
<svg |
|||
class="w-5" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,51 @@ |
|||
<template> |
|||
<!-- Inline Transfer TX --> |
|||
<span |
|||
v-if="client.transferTx" |
|||
class="whitespace-nowrap" |
|||
:title="$t('totalDownload') + bytes(client.transferTx)" |
|||
> |
|||
· |
|||
<svg |
|||
class="align-middle h-3 inline" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
{{ bytes(client.transferTxCurrent) }}/s |
|||
</span> |
|||
|
|||
<!-- Inline Transfer RX --> |
|||
<span |
|||
v-if="client.transferRx" |
|||
class="whitespace-nowrap" |
|||
:title="$t('totalUpload') + bytes(client.transferRx)" |
|||
> |
|||
· |
|||
<svg |
|||
class="align-middle h-3 inline" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
{{ bytes(client.transferRxCurrent) }}/s |
|||
</span> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
</script> |
@ -0,0 +1,20 @@ |
|||
<template> |
|||
<span |
|||
v-if="client.latestHandshakeAt" |
|||
class="text-gray-400 dark:text-neutral-500 whitespace-nowrap" |
|||
:title="$t('lastSeen') + dateTime(new Date(client.latestHandshakeAt))" |
|||
> |
|||
{{ !globalStore.uiTrafficStats ? ' · ' : '' |
|||
}}{{ timeago(new Date(client.latestHandshakeAt)) }} |
|||
</span> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { format as timeago } from 'timeago.js'; |
|||
|
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const globalStore = useGlobalStore(); |
|||
</script> |
@ -0,0 +1,76 @@ |
|||
<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()); |
|||
" |
|||
> |
|||
<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> |
|||
</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: WGClient, 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> |
@ -0,0 +1,38 @@ |
|||
<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` |
|||
" |
|||
> |
|||
<svg |
|||
class="w-5" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,40 @@ |
|||
<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,67 @@ |
|||
<template> |
|||
<!-- Transfer TX --> |
|||
<div v-if="client.transferTx" class="min-w-20 md:min-w-24"> |
|||
<span |
|||
class="flex gap-1" |
|||
:title="$t('totalDownload') + bytes(client.transferTx)" |
|||
> |
|||
<svg |
|||
class="align-middle h-3 inline mt-0.5" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
<div> |
|||
<span class="text-gray-700 dark:text-neutral-200" |
|||
>{{ bytes(client.transferTxCurrent) }}/s</span |
|||
> |
|||
<!-- Total TX --> |
|||
<br /><span class="font-regular" style="font-size: 0.85em">{{ |
|||
bytes(client.transferTx) |
|||
}}</span> |
|||
</div> |
|||
</span> |
|||
</div> |
|||
|
|||
<!-- Transfer RX --> |
|||
<div v-if="client.transferRx" class="min-w-20 md:min-w-24"> |
|||
<span |
|||
class="flex gap-1" |
|||
:title="$t('totalUpload') + bytes(client.transferRx)" |
|||
> |
|||
<svg |
|||
class="align-middle h-3 inline mt-0.5" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 20 20" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
fill-rule="evenodd" |
|||
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z" |
|||
clip-rule="evenodd" |
|||
/> |
|||
</svg> |
|||
<div> |
|||
<span class="text-gray-700 dark:text-neutral-200" |
|||
>{{ bytes(client.transferRxCurrent) }}/s</span |
|||
> |
|||
<!-- Total RX --> |
|||
<br /><span class="font-regular" style="font-size: 0.85em">{{ |
|||
bytes(client.transferRx) |
|||
}}</span> |
|||
</div> |
|||
</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
</script> |
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<div |
|||
v-for="client in clientsStore.clients" |
|||
:key="client.id" |
|||
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid" |
|||
> |
|||
<Client :client="client" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const clientsStore = useClientsStore(); |
|||
</script> |
@ -0,0 +1,33 @@ |
|||
<template> |
|||
<p class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm"> |
|||
{{ $t('noClients') }}<br /><br /> |
|||
<button |
|||
class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition" |
|||
@click=" |
|||
modalStore.clientCreate = true; |
|||
modalStore.clientCreateName = ''; |
|||
" |
|||
> |
|||
<svg |
|||
class="w-4 mr-2" |
|||
inline |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" |
|||
/> |
|||
</svg> |
|||
<span class="text-sm">{{ $t('newClient') }}</span> |
|||
</button> |
|||
</p> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,30 @@ |
|||
<template> |
|||
<button |
|||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-r-full md:rounded inline-flex items-center transition" |
|||
@click=" |
|||
modalStore.clientCreate = true; |
|||
modalStore.clientCreateName = ''; |
|||
" |
|||
> |
|||
<svg |
|||
class="w-4 md:mr-2" |
|||
inline |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" |
|||
/> |
|||
</svg> |
|||
<span class="max-md:hidden text-sm">{{ $t('new') }}</span> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,17 @@ |
|||
<template> |
|||
<apexchart |
|||
width="100%" |
|||
height="100%" |
|||
:options="chartOptionsTX" |
|||
:series="client.transferTxSeries" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import type { VueApexChartsComponent } from 'vue3-apexcharts'; |
|||
|
|||
defineProps<{ |
|||
options: VueApexChartsComponent['options']; |
|||
series: VueApexChartsComponent['series']; |
|||
}>(); |
|||
</script> |
@ -1,4 +1,10 @@ |
|||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'; |
|||
import eslintConfigPrettier from 'eslint-config-prettier'; |
|||
|
|||
export default createConfigForNuxt().append(eslintConfigPrettier); |
|||
export default createConfigForNuxt() |
|||
.append({ |
|||
rules: { |
|||
'vue/no-multiple-template-root': 'off', |
|||
}, |
|||
}) |
|||
.append(eslintConfigPrettier); |
|||
|
File diff suppressed because it is too large
@ -0,0 +1,14 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const url = getRequestURL(event); |
|||
const session = await useWGSession(event); |
|||
if (url.pathname === '/login') { |
|||
if (!REQUIRES_PASSWORD || session.data.authenticated) { |
|||
return sendRedirect(event, '/', 302) |
|||
} |
|||
} |
|||
if (url.pathname === '/') { |
|||
if (!session.data.authenticated) { |
|||
return sendRedirect(event, '/login', 302) |
|||
} |
|||
} |
|||
}); |
@ -0,0 +1,6 @@ |
|||
export default defineNitroPlugin((nitroApp) => { |
|||
nitroApp.hooks.hook('close', () => { |
|||
console.log('Shutting down'); |
|||
WireGuard.Shutdown(); |
|||
}); |
|||
}); |
@ -0,0 +1,34 @@ |
|||
export const useAuthStore = defineStore('Auth', () => { |
|||
const authenticated = ref<boolean>(false); |
|||
const requiresPassword = ref<boolean>(true); |
|||
|
|||
/** |
|||
* @throws if unsuccessful |
|||
*/ |
|||
async function login(password: string) { |
|||
const response = await api.createSession({ password }); |
|||
authenticated.value = response.success; |
|||
requiresPassword.value = response.requiresPassword; |
|||
return true as const; |
|||
} |
|||
|
|||
/** |
|||
* @throws if unsuccessful |
|||
*/ |
|||
async function logout() { |
|||
const response = await api.deleteSession(); |
|||
authenticated.value = !response.success; |
|||
return response.success; |
|||
} |
|||
|
|||
/** |
|||
* @throws if unsuccessful |
|||
*/ |
|||
async function update() { |
|||
const response = await api.getSession(); |
|||
authenticated.value = response.authenticated; |
|||
requiresPassword.value = response.requiresPassword; |
|||
} |
|||
|
|||
return { requiresPassword, authenticated, login, logout, update }; |
|||
}); |
@ -0,0 +1,110 @@ |
|||
import { defineStore } from 'pinia'; |
|||
import { sha256 } from 'js-sha256'; |
|||
|
|||
export type LocalClient = WGClient & { |
|||
avatar?: string; |
|||
transferMax?: number; |
|||
} & Omit<ClientPersist, 'transferRxPrevious' | 'transferTxPrevious'>; |
|||
|
|||
export type ClientPersist = { |
|||
transferRxHistory: number[]; |
|||
transferRxPrevious: number; |
|||
transferRxCurrent: number; |
|||
transferRxSeries: { name: string; data: number[] }[]; |
|||
hoverRx?: unknown; |
|||
transferTxHistory: number[]; |
|||
transferTxPrevious: number; |
|||
transferTxCurrent: number; |
|||
transferTxSeries: { name: string; data: number[] }[]; |
|||
hoverTx?: unknown; |
|||
}; |
|||
|
|||
export const useClientsStore = defineStore('Clients', () => { |
|||
const clients = ref<null | LocalClient[]>(null); |
|||
const clientsPersist = ref<Record<string, ClientPersist>>({}); |
|||
|
|||
async function refresh({ updateCharts = false } = {}) { |
|||
const _clients = await api.getClients(); |
|||
clients.value = _clients.map((client) => { |
|||
let avatar = undefined; |
|||
if (client.name.includes('@') && client.name.includes('.')) { |
|||
avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`; |
|||
} |
|||
|
|||
if (!clientsPersist.value[client.id]) { |
|||
clientsPersist.value[client.id] = { |
|||
transferRxHistory: Array(50).fill(0), |
|||
transferRxPrevious: client.transferRx ?? 0, |
|||
transferTxHistory: Array(50).fill(0), |
|||
transferTxPrevious: client.transferTx ?? 0, |
|||
transferRxCurrent: 0, |
|||
transferTxCurrent: 0, |
|||
transferRxSeries: [], |
|||
transferTxSeries: [], |
|||
}; |
|||
} |
|||
|
|||
const clientPersist = clientsPersist.value[client.id]; |
|||
|
|||
// Debug
|
|||
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
|||
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
|||
// client.latestHandshakeAt = new Date();
|
|||
// this.requiresPassword = true;
|
|||
|
|||
clientPersist.transferRxCurrent = |
|||
(client.transferRx ?? 0) - clientPersist.transferRxPrevious; |
|||
|
|||
clientPersist.transferRxPrevious = client.transferRx ?? 0; |
|||
|
|||
clientPersist.transferTxCurrent = |
|||
(client.transferTx ?? 0) - clientPersist.transferTxPrevious; |
|||
|
|||
clientPersist.transferTxPrevious = client.transferTx ?? 0; |
|||
|
|||
let transferMax = undefined; |
|||
|
|||
if (updateCharts) { |
|||
clientPersist.transferRxHistory.push(clientPersist.transferRxCurrent); |
|||
clientPersist.transferRxHistory.shift(); |
|||
|
|||
clientPersist.transferTxHistory.push(clientPersist.transferTxCurrent); |
|||
clientPersist.transferTxHistory.shift(); |
|||
|
|||
clientPersist.transferTxSeries = [ |
|||
{ |
|||
name: 'Tx', |
|||
data: clientPersist.transferTxHistory, |
|||
}, |
|||
]; |
|||
|
|||
clientPersist.transferRxSeries = [ |
|||
{ |
|||
name: 'Rx', |
|||
data: clientPersist.transferRxHistory, |
|||
}, |
|||
]; |
|||
|
|||
transferMax = Math.max( |
|||
...clientPersist.transferTxHistory, |
|||
...clientPersist.transferRxHistory |
|||
); |
|||
} |
|||
|
|||
return { |
|||
...client, |
|||
avatar, |
|||
transferTxHistory: clientPersist.transferTxHistory, |
|||
transferRxHistory: clientPersist.transferRxHistory, |
|||
transferMax, |
|||
transferTxSeries: clientPersist.transferTxSeries, |
|||
transferRxSeries: clientPersist.transferRxSeries, |
|||
transferTxCurrent: clientPersist.transferTxCurrent, |
|||
transferRxCurrent: clientPersist.transferRxCurrent, |
|||
hoverTx: clientPersist.hoverTx, |
|||
hoverRx: clientPersist.hoverRx, |
|||
}; |
|||
}); |
|||
} |
|||
return { clients, clientsPersist, refresh }; |
|||
}); |
@ -0,0 +1,81 @@ |
|||
import { defineStore } from 'pinia'; |
|||
|
|||
export const useGlobalStore = defineStore('Global', () => { |
|||
const uiChartType = ref(0); |
|||
const uiShowCharts = ref(getItem('uiShowCharts') === '1'); |
|||
const currentRelease = ref<null | number>(null); |
|||
const latestRelease = ref<null | { version: number; changelog: string }>( |
|||
null |
|||
); |
|||
const uiTrafficStats = ref(false); |
|||
|
|||
const { availableLocales, locale } = useI18n(); |
|||
|
|||
async function fetchRelease() { |
|||
const lang = await api.getLang(); |
|||
if (lang !== getItem('lang') && availableLocales.includes(lang)) { |
|||
setItem('lang', lang); |
|||
locale.value = lang; |
|||
} |
|||
|
|||
const _currentRelease = await api.getRelease(); |
|||
const _latestRelease = await fetch( |
|||
'https://wg-easy.github.io/wg-easy/changelog.json' |
|||
) |
|||
.then((res) => res.json()) |
|||
.then((releases) => { |
|||
const releasesArray = Object.entries(releases).map( |
|||
([version, changelog]) => ({ |
|||
version: parseInt(version, 10), |
|||
changelog: changelog as string, |
|||
}) |
|||
); |
|||
releasesArray.sort((a, b) => { |
|||
return b.version - a.version; |
|||
}); |
|||
|
|||
return releasesArray[0]; |
|||
}); |
|||
|
|||
if (_currentRelease >= _latestRelease.version) return; |
|||
|
|||
currentRelease.value = _currentRelease; |
|||
latestRelease.value = _latestRelease; |
|||
} |
|||
|
|||
async function fetchChartType() { |
|||
api |
|||
.getChartType() |
|||
.then((res) => { |
|||
uiChartType.value = res; |
|||
}) |
|||
.catch(() => { |
|||
uiChartType.value = 0; |
|||
}); |
|||
} |
|||
|
|||
async function fetchUITrafficStats() { |
|||
api |
|||
.getUITrafficStats() |
|||
.then((res) => { |
|||
uiTrafficStats.value = res; |
|||
}) |
|||
.catch(() => { |
|||
uiTrafficStats.value = false; |
|||
}); |
|||
} |
|||
|
|||
const updateCharts = computed(() => { |
|||
return uiChartType.value > 0 && uiShowCharts.value; |
|||
}); |
|||
|
|||
return { |
|||
uiChartType, |
|||
uiShowCharts, |
|||
uiTrafficStats, |
|||
updateCharts, |
|||
fetchRelease, |
|||
fetchChartType, |
|||
fetchUITrafficStats, |
|||
}; |
|||
}); |
@ -0,0 +1,36 @@ |
|||
import { defineStore } from 'pinia'; |
|||
|
|||
export const useModalStore = defineStore('Modal', () => { |
|||
const clientsStore = useClientsStore(); |
|||
const clientDelete = ref<null | WGClient>(null); |
|||
const clientCreate = ref<null | boolean>(null); |
|||
const clientCreateName = ref<string>(''); |
|||
const qrcode = ref<null | string>(null); |
|||
|
|||
function createClient() { |
|||
const name = clientCreateName.value; |
|||
if (!name) return; |
|||
|
|||
api |
|||
.createClient({ name }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
} |
|||
function deleteClient(client: WGClient | null) { |
|||
if (client === null) { |
|||
return; |
|||
} |
|||
api |
|||
.deleteClient({ clientId: client.id }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
} |
|||
return { |
|||
clientDelete, |
|||
clientCreate, |
|||
clientCreateName, |
|||
qrcode, |
|||
createClient, |
|||
deleteClient, |
|||
}; |
|||
}); |
@ -0,0 +1,15 @@ |
|||
export const UI_CHART_TYPES = [ |
|||
{ type: false, strokeWidth: 0 }, |
|||
{ type: 'line', strokeWidth: 3 }, |
|||
{ type: 'area', strokeWidth: 0 }, |
|||
{ type: 'bar', strokeWidth: 0 }, |
|||
]; |
|||
|
|||
export const CHART_COLORS = { |
|||
rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' }, |
|||
tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' }, |
|||
gradient: { |
|||
light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], |
|||
dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'], |
|||
}, |
|||
}; |
@ -0,0 +1,31 @@ |
|||
export function bytes( |
|||
bytes: number, |
|||
decimals = 2, |
|||
kib = false, |
|||
maxunit?: string |
|||
) { |
|||
if (bytes === 0) return '0 B'; |
|||
if (Number.isNaN(bytes) && !Number.isFinite(bytes)) return 'NaN'; |
|||
const k = kib ? 1024 : 1000; |
|||
const dm = |
|||
decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2; |
|||
const sizes = kib |
|||
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB'] |
|||
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB']; |
|||
let i = Math.floor(Math.log(bytes) / Math.log(k)); |
|||
if (maxunit !== undefined) { |
|||
const index = sizes.indexOf(maxunit); |
|||
if (index !== -1) i = index; |
|||
} |
|||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; |
|||
} |
|||
|
|||
export function dateTime(value: Date) { |
|||
return new Intl.DateTimeFormat(undefined, { |
|||
year: 'numeric', |
|||
month: 'short', |
|||
day: 'numeric', |
|||
hour: 'numeric', |
|||
minute: 'numeric', |
|||
}).format(value); |
|||
} |
Loading…
Reference in new issue