mirror of https://github.com/wg-easy/wg-easy
Browse Source
* wip: add nuxt
* basic implementation
* add changes from c9ff248
* update workflow, add eslint
* add types, fix wrong error message
* install correct bcrypt, move eslint to dev modules
* add docker dev script
* fix styling
* add wireguard routes
* typescript, vendors
* fix lint workflow
* lint fixes
* add prettier, format
* fix lint, add vscode settings
* better typescript
* use auto imports
* add prettier eslint config
* cache config
* fix styling issue, fix formatting
* fix tailwind problems
* fix logout not showing
* fix lint action
* Fix session middleware
* split files into correct methods
* use type safe api, fix typescript errors
* better return types
not tested
* change default working directory
* update workflows
* fix error
* correct session middleware, type safe session
* convert undefined to boolean
* correct key for api errors
* use zod to validate input
* add more jobs to check for good code
* add pinia
Co-authored-by: Sergei Birukov <[email protected]>
Co-authored-by: Bernd Storath <[email protected]>
* use color mode plugin
* !! use better storage key name
Breaking as if old key exists it breaks as "auto" is not compatible with new "system"
* better local dev while dev container is running
use `docker compose -f docker-compose.dev.yml up`
or after changing dockerfile
`docker compose -f docker-compose.dev.yml up --build`
* update translation to match new theme mode
* improve dx
new devs get extensions recommended to catch errors, etc directly in vscode
* reduce errors, improve typing
* Split components (#1)
* update: introduce pages & components
fix lint
* update: starting split components
* use auto imports
* Improve workflows and docker
workflow fix step naming
simplify docker dev
simplify docker prod
revert to node 18
dockerfile naming scheme
* Split components (#2)
* update: starting split components
* upd: rebase & continue splitting components
- layouts: header & footer
- components: basic buttton
- pages: login page
* update: login page
* package.json: remove dev:pass script
* Split into Components, migrate to nuxt
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 page
* fix: buttons spaces & move layouts to components (#3)
* update: icons into components
- fix: header login page
* fix: tailwind handle btn class
* Split into icons
fix avatar
move class to view not icon itself
fix icon
format
* invalidate cache to make restoreConfig work
* fix apexchart
* use different color mode module
other one resulted in hydration mismatch
* fix dialog
* fix bad i18n merge
* use nuxt 4
* fix typing, fix redirect, latest release on server
* start wireguard on start
* wait for shutdown
* improve zod errors, consistent server errors
* migrate to useFetch
this makes sure that there is no double fetching
* fix hydration issues, remove unnecessary state, rename function
* fetch globalstore globally
otherwise this will load on login to homepage
* migrate to useFetch
no javascript support
TODO: not properly tested
* update backend
* wip: frontend
* update frontend
* update pnpm lock
---------
Co-authored-by: Sergei Birukov <[email protected]>
Co-authored-by: Bernd Storath <[email protected]>
Co-authored-by: tetuaoro <[email protected]>
pull/1648/head
committed by
Bernd Storath
144 changed files with 13509 additions and 5033 deletions
@ -0,0 +1,13 @@ |
|||
{ |
|||
"recommendations": [ |
|||
"aaron-bond.better-comments", |
|||
"dbaeumer.vscode-eslint", |
|||
"antfu.goto-alias", |
|||
"visualstudioexptteam.vscodeintellicode", |
|||
"Nuxtr.nuxtr-vscode", |
|||
"esbenp.prettier-vscode", |
|||
"yoavbls.pretty-ts-errors", |
|||
"bradlc.vscode-tailwindcss", |
|||
"vue.volar" |
|||
] |
|||
} |
@ -0,0 +1,16 @@ |
|||
{ |
|||
"editor.tabSize": 2, |
|||
"editor.useTabStops": false, |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode", |
|||
"editor.formatOnSave": true, |
|||
"nuxtr.vueFiles.style.addStyleTag": false, |
|||
"nuxtr.piniaFiles.defaultTemplate": "setup", |
|||
"nuxtr.monorepoMode.DirectoryName": "src", |
|||
"[vue]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[typescript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"typescript.tsdk": "./src/node_modules/typescript/lib" |
|||
} |
@ -0,0 +1,28 @@ |
|||
# As a workaround we have to build on nodejs 18 |
|||
# nodejs 20 hangs on build with armv6/armv7 |
|||
FROM docker.io/library/node:20-alpine |
|||
WORKDIR /app |
|||
|
|||
# Install pnpm |
|||
RUN corepack enable pnpm |
|||
|
|||
# Copy Web UI |
|||
COPY src ./ |
|||
RUN pnpm install |
|||
|
|||
HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3 |
|||
|
|||
# Install Linux packages |
|||
RUN apk add --no-cache \ |
|||
dpkg \ |
|||
dumb-init \ |
|||
iptables \ |
|||
iptables-legacy \ |
|||
wireguard-tools |
|||
|
|||
# Use iptables-legacy |
|||
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save |
|||
|
|||
# Set Environment |
|||
ENV DEBUG=Server,WireGuard |
|||
ENV PORT=51821 |
@ -1,11 +0,0 @@ |
|||
{ |
|||
"name": "wg-easy", |
|||
"version": "1.0.1", |
|||
"lockfileVersion": 3, |
|||
"requires": true, |
|||
"packages": { |
|||
"": { |
|||
"version": "1.0.1" |
|||
} |
|||
} |
|||
} |
@ -6,5 +6,6 @@ |
|||
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up", |
|||
"sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy", |
|||
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy" |
|||
} |
|||
} |
|||
}, |
|||
"packageManager": "[email protected]" |
|||
} |
|||
|
@ -0,0 +1,9 @@ |
|||
lockfileVersion: '9.0' |
|||
|
|||
settings: |
|||
autoInstallPeers: true |
|||
excludeLinksFromLockfile: false |
|||
|
|||
importers: |
|||
|
|||
.: {} |
@ -1,11 +0,0 @@ |
|||
{ |
|||
"extends": "athom", |
|||
"ignorePatterns": [ |
|||
"**/vendor/*.js" |
|||
], |
|||
"rules": { |
|||
"consistent-return": "off", |
|||
"no-shadow": "off", |
|||
"max-len": "off" |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
# Nuxt dev/build outputs |
|||
.output |
|||
.data |
|||
.nuxt |
|||
.nitro |
|||
.cache |
|||
dist |
|||
|
|||
# Node dependencies |
|||
node_modules |
|||
|
|||
# Logs |
|||
logs |
|||
*.log |
|||
|
|||
# Misc |
|||
.DS_Store |
|||
.fleet |
|||
.idea |
|||
|
|||
# Local env files |
|||
.env |
|||
.env.* |
|||
!.env.example |
@ -0,0 +1 @@ |
|||
pnpm-lock.yaml |
@ -0,0 +1,6 @@ |
|||
{ |
|||
"trailingComma": "es5", |
|||
"tabWidth": 2, |
|||
"semi": true, |
|||
"singleQuote": true |
|||
} |
@ -0,0 +1,49 @@ |
|||
<template> |
|||
<NuxtLayout> |
|||
<NuxtLayout name="header" /> |
|||
<NuxtPage /> |
|||
<NuxtLayout name="footer" /> |
|||
</NuxtLayout> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const globalStore = useGlobalStore(); |
|||
globalStore.fetchTrafficStats(); |
|||
globalStore.fetchChartType(); |
|||
globalStore.fetchRelease(); |
|||
globalStore.fetchOneTimeLinks(); |
|||
globalStore.fetchSortClients(); |
|||
globalStore.fetchExpireTime(); |
|||
globalStore.fetchRememberMe(); |
|||
useHead({ |
|||
bodyAttrs: { |
|||
class: 'bg-gray-50 dark:bg-neutral-800', |
|||
}, |
|||
link: [ |
|||
{ |
|||
rel: 'manifest', |
|||
href: '/manifest.json', |
|||
}, |
|||
{ |
|||
rel: 'icon', |
|||
type: 'image/png', |
|||
href: '/favicon.png', |
|||
}, |
|||
{ |
|||
rel: 'apple-touch-icon', |
|||
href: '/apple-touch-icon.png', |
|||
}, |
|||
], |
|||
meta: [ |
|||
{ |
|||
name: 'apple-mobile-web-app-capable', |
|||
content: 'yes', |
|||
}, |
|||
{ |
|||
name: 'apple-mobile-web-app-status-bar-style', |
|||
content: 'black-translucent', |
|||
}, |
|||
], |
|||
title: 'WireGuard', |
|||
}); |
|||
</script> |
@ -0,0 +1,60 @@ |
|||
<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()); |
|||
" |
|||
> |
|||
<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 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,31 @@ |
|||
<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> |
@ -0,0 +1,138 @@ |
|||
<template> |
|||
<div |
|||
v-if="globalStore.uiChartType" |
|||
:class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${globalStore.uiChartType === 1 && 'line-chart'}`" |
|||
> |
|||
<UiChart :options="chartOptionsTX" :series="client.transferTxSeries" /> |
|||
</div> |
|||
<div |
|||
v-if="globalStore.uiChartType" |
|||
:class="`absolute z-0 top-0 left-0 right-0 h-6 ${globalStore.uiChartType === 1 && 'line-chart'}`" |
|||
> |
|||
<UiChart |
|||
:options="chartOptionsRX" |
|||
:series="client.transferRxSeries" |
|||
style="transform: scaleY(-1)" |
|||
/> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import type { ApexOptions } from 'apexcharts'; |
|||
|
|||
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 || undefined; |
|||
opts.stroke.width = UI_CHART_TYPES[globalStore.uiChartType]?.strokeWidth ?? 0; |
|||
return opts; |
|||
}); |
|||
|
|||
const chartOptionsRX = computed(() => { |
|||
const opts = { |
|||
...chartOptions, |
|||
colors: [CHART_COLORS.rx[theme.value]], |
|||
}; |
|||
opts.chart.type = UI_CHART_TYPES[globalStore.uiChartType]?.type || undefined; |
|||
opts.stroke.width = UI_CHART_TYPES[globalStore.uiChartType]?.strokeWidth ?? 0; |
|||
return opts; |
|||
}); |
|||
|
|||
const chartOptions = { |
|||
chart: { |
|||
type: undefined as ApexChart['type'], |
|||
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, |
|||
}, |
|||
}, |
|||
}, |
|||
} satisfies ApexOptions; |
|||
</script> |
|||
|
|||
<style scoped lang="css"> |
|||
.line-chart .apexcharts-svg { |
|||
transform: translateY(3px); |
|||
} |
|||
</style> |
@ -0,0 +1,58 @@ |
|||
<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> |
|||
<ClientOneTimeLink :client="client" /> |
|||
<ClientExpireDate :client="client" /> |
|||
</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" /> |
|||
<ClientOneTimeLinkBtn :client="client" /> |
|||
<ClientDelete :client="client" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ |
|||
client: LocalClient; |
|||
}>(); |
|||
|
|||
const globalStore = useGlobalStore(); |
|||
</script> |
@ -0,0 +1,27 @@ |
|||
<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> |
@ -0,0 +1,17 @@ |
|||
<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> |
@ -0,0 +1,91 @@ |
|||
<template> |
|||
<div |
|||
v-show="globalStore.enableExpireTime" |
|||
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.expireAt) }}</span |
|||
> |
|||
|
|||
<!-- Edit --> |
|||
<span |
|||
v-show="clientEditExpireDateId !== client.id" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" |
|||
@click=" |
|||
clientEditExpireDate = client.expireAt |
|||
? client.expireAt.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> |
@ -0,0 +1,29 @@ |
|||
<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> |
@ -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,65 @@ |
|||
<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> |
@ -0,0 +1,26 @@ |
|||
<template> |
|||
<div |
|||
v-if=" |
|||
globalStore.enableOneTimeLinks && |
|||
client.oneTimeLink !== null && |
|||
client.oneTimeLink !== '' |
|||
" |
|||
: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> |
@ -0,0 +1,47 @@ |
|||
<template> |
|||
<button |
|||
v-if="globalStore.enableOneTimeLinks" |
|||
:disabled="!client.downloadableConfig" |
|||
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('OneTimeLink')" |
|||
@click=" |
|||
if (client.downloadableConfig) { |
|||
showOneTimeLink(client); |
|||
} |
|||
" |
|||
> |
|||
<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="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
defineProps<{ client: LocalClient }>(); |
|||
|
|||
const clientsStore = useClientsStore(); |
|||
const globalStore = useGlobalStore(); |
|||
|
|||
function showOneTimeLink(client: LocalClient) { |
|||
api |
|||
.showOneTimeLink({ clientId: client.id }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
} |
|||
</script> |
@ -0,0 +1,25 @@ |
|||
<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> |
@ -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,45 @@ |
|||
<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)" |
|||
> |
|||
<IconsArrowDown class="align-middle h-3 inline mt-0.5" /> |
|||
<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)" |
|||
> |
|||
<IconsArrowUp class="align-middle h-3 inline mt-0.5" /> |
|||
<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,10 @@ |
|||
<template> |
|||
<BaseButton |
|||
as="a" |
|||
href="./api/wireguard/backup" |
|||
:title="$t('titleBackupConfig')" |
|||
> |
|||
<IconsStack class="w-4 md:mr-2" /> |
|||
<span class="max-md:hidden text-sm">{{ $t('backup') }}</span> |
|||
</BaseButton> |
|||
</template> |
@ -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,113 @@ |
|||
<template> |
|||
<!-- Create Dialog --> |
|||
<div |
|||
v-if="modalStore.clientCreate" |
|||
class="fixed z-10 inset-0 overflow-y-auto" |
|||
> |
|||
<div |
|||
class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" |
|||
> |
|||
<!-- |
|||
Background overlay, show/hide based on modal state. |
|||
|
|||
Entering: "ease-out duration-300" |
|||
From: "opacity-0" |
|||
To: "opacity-100" |
|||
Leaving: "ease-in duration-200" |
|||
From: "opacity-100" |
|||
To: "opacity-0" |
|||
--> |
|||
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> |
|||
<div |
|||
class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50" |
|||
/> |
|||
</div> |
|||
|
|||
<!-- This element is to trick the browser into centering the modal contents. --> |
|||
<span |
|||
class="hidden sm:inline-block sm:align-middle sm:h-screen" |
|||
aria-hidden="true" |
|||
>​</span |
|||
> |
|||
<!-- |
|||
Modal panel, show/hide based on modal state. |
|||
|
|||
Entering: "ease-out duration-300" |
|||
From: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
To: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
Leaving: "ease-in duration-200" |
|||
From: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
--> |
|||
<div |
|||
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full" |
|||
role="dialog" |
|||
aria-modal="true" |
|||
aria-labelledby="modal-headline" |
|||
> |
|||
<div class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> |
|||
<div class="sm:flex sm:items-start"> |
|||
<div |
|||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10" |
|||
> |
|||
<IconsPlus class="h-6 w-6 text-white" /> |
|||
</div> |
|||
<div |
|||
class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left" |
|||
> |
|||
<h3 |
|||
id="modal-headline" |
|||
class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200" |
|||
> |
|||
{{ $t('newClient') }} |
|||
</h3> |
|||
<div class="mt-2"> |
|||
<p class="text-sm text-gray-500"> |
|||
<input |
|||
v-model.trim="modalStore.clientCreateName" |
|||
class="rounded p-2 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none w-full" |
|||
type="text" |
|||
:placeholder="$t('name')" |
|||
/> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="bg-gray-50 dark:bg-neutral-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse" |
|||
> |
|||
<button |
|||
v-if="modalStore.clientCreateName.length" |
|||
type="button" |
|||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-800 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm" |
|||
@click=" |
|||
modalStore.createClient(); |
|||
modalStore.clientCreate = null; |
|||
" |
|||
> |
|||
{{ $t('create') }} |
|||
</button> |
|||
<button |
|||
v-else |
|||
type="button" |
|||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-200 dark:bg-neutral-400 text-base font-medium text-white dark:text-neutral-300 sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed" |
|||
> |
|||
{{ $t('create') }} |
|||
</button> |
|||
<button |
|||
type="button" |
|||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" |
|||
@click="modalStore.clientCreate = null" |
|||
> |
|||
{{ $t('cancel') }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,99 @@ |
|||
<template> |
|||
<div |
|||
v-if="modalStore.clientDelete" |
|||
class="fixed z-10 inset-0 overflow-y-auto" |
|||
> |
|||
<div |
|||
class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" |
|||
> |
|||
<!-- |
|||
Background overlay, show/hide based on modal state. |
|||
|
|||
Entering: "ease-out duration-300" |
|||
From: "opacity-0" |
|||
To: "opacity-100" |
|||
Leaving: "ease-in duration-200" |
|||
From: "opacity-100" |
|||
To: "opacity-0" |
|||
--> |
|||
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> |
|||
<div |
|||
class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50" |
|||
/> |
|||
</div> |
|||
|
|||
<!-- This element is to trick the browser into centering the modal contents. --> |
|||
<span |
|||
class="hidden sm:inline-block sm:align-middle sm:h-screen" |
|||
aria-hidden="true" |
|||
>​</span |
|||
> |
|||
<!-- |
|||
Modal panel, show/hide based on modal state. |
|||
|
|||
Entering: "ease-out duration-300" |
|||
From: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
To: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
Leaving: "ease-in duration-200" |
|||
From: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
--> |
|||
<div |
|||
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full" |
|||
role="dialog" |
|||
aria-modal="true" |
|||
aria-labelledby="modal-headline" |
|||
> |
|||
<div class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> |
|||
<div class="sm:flex sm:items-start"> |
|||
<div |
|||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10" |
|||
> |
|||
<IconsWarning class="h-6 w-6 text-red-600" /> |
|||
</div> |
|||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> |
|||
<h3 |
|||
id="modal-headline" |
|||
class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200" |
|||
> |
|||
{{ $t('deleteClient') }} |
|||
</h3> |
|||
<div class="mt-2"> |
|||
<p class="text-sm text-gray-500 dark:text-neutral-300"> |
|||
{{ $t('deleteDialog1') }} |
|||
<strong>{{ modalStore.clientDelete.name }}</strong |
|||
>? {{ $t('deleteDialog2') }} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="bg-gray-50 dark:bg-neutral-600 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse" |
|||
> |
|||
<button |
|||
type="button" |
|||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 dark:bg-red-600 text-base font-medium text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm" |
|||
@click=" |
|||
modalStore.deleteClient(modalStore.clientDelete); |
|||
modalStore.clientDelete = null; |
|||
" |
|||
> |
|||
{{ $t('deleteClient') }} |
|||
</button> |
|||
<button |
|||
type="button" |
|||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" |
|||
@click="modalStore.clientDelete = null" |
|||
> |
|||
{{ $t('cancel') }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,20 @@ |
|||
<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 = ''; |
|||
modalStore.clientExpireDate = ''; |
|||
" |
|||
> |
|||
<IconsPlus class="w-4 mr-2" /> |
|||
<span class="text-sm">{{ $t('newClient') }}</span> |
|||
</button> |
|||
</p> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<BaseButton |
|||
@click=" |
|||
modalStore.clientCreate = true; |
|||
modalStore.clientCreateName = ''; |
|||
modalStore.clientExpireDate = ''; |
|||
" |
|||
> |
|||
<IconsPlus class="w-4 md:mr-2" /> |
|||
<span class="max-md:hidden text-sm">{{ $t('new') }}</span> |
|||
</BaseButton> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,21 @@ |
|||
<template> |
|||
<div v-if="modalStore.qrcode"> |
|||
<div |
|||
class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center z-20" |
|||
> |
|||
<div class="bg-white rounded-md shadow-lg relative p-8"> |
|||
<button |
|||
class="absolute right-4 top-4 text-gray-600 dark:text-neutral-500 hover:text-gray-800 dark:hover:text-neutral-700" |
|||
@click="modalStore.qrcode = null" |
|||
> |
|||
<IconsClose class="w-8" /> |
|||
</button> |
|||
<img :src="modalStore.qrcode" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const modalStore = useModalStore(); |
|||
</script> |
@ -0,0 +1,34 @@ |
|||
<template> |
|||
<BaseButton as="label" for="inputRC" :title="$t('titleRestoreConfig')"> |
|||
<IconsArrowInf class="w-4 md:mr-2" /> |
|||
<span class="max-md:hidden text-sm">{{ $t('restore') }}</span> |
|||
<input |
|||
id="inputRC" |
|||
type="file" |
|||
name="configurationfile" |
|||
accept="text/*,.json" |
|||
class="hidden" |
|||
@change="restoreConfig" |
|||
/> |
|||
</BaseButton> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
function restoreConfig(e: Event) { |
|||
e.preventDefault(); |
|||
const file = (e.currentTarget as HTMLInputElement).files?.item(0); |
|||
if (file) { |
|||
file |
|||
.text() |
|||
.then((content) => { |
|||
api |
|||
.restoreConfiguration(content) |
|||
.then(() => alert('The configuration was updated.')) |
|||
.catch((err) => alert(err.message || err.toString())); |
|||
}) |
|||
.catch((err) => alert(err.message || err.toString())); |
|||
} else { |
|||
alert('Failed to load your file!'); |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,51 @@ |
|||
<template> |
|||
<button |
|||
v-if="globalStore.enableSortClient" |
|||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition" |
|||
@click="globalStore.sortClient = !globalStore.sortClient" |
|||
> |
|||
<svg |
|||
v-if="globalStore.sortClient === true" |
|||
inline |
|||
class="w-4 md:mr-2" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
d="M12 19.75C11.9015 19.7504 11.8038 19.7312 11.7128 19.6934C11.6218 19.6557 11.5392 19.6001 11.47 19.53L5.47 13.53C5.33752 13.3878 5.2654 13.1997 5.26882 13.0054C5.27225 12.8111 5.35096 12.6258 5.48838 12.4883C5.62579 12.3509 5.81118 12.2722 6.00548 12.2688C6.19978 12.2654 6.38782 12.3375 6.53 12.47L12 17.94L17.47 12.47C17.6122 12.3375 17.8002 12.2654 17.9945 12.2688C18.1888 12.2722 18.3742 12.3509 18.5116 12.4883C18.649 12.6258 18.7277 12.8111 18.7312 13.0054C18.7346 13.1997 18.6625 13.3878 18.53 13.53L12.53 19.53C12.4608 19.6001 12.3782 19.6557 12.2872 19.6934C12.1962 19.7312 12.0985 19.7504 12 19.75Z" |
|||
fill="#000000" |
|||
/> |
|||
<path |
|||
d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z" |
|||
fill="#000000" |
|||
/> |
|||
</svg> |
|||
<svg |
|||
v-if="globalStore.sortClient === false" |
|||
inline |
|||
class="w-4 md:mr-2" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
d="M18 11.75C17.9015 11.7505 17.8038 11.7313 17.7128 11.6935C17.6218 11.6557 17.5392 11.6001 17.47 11.53L12 6.06001L6.53 11.53C6.38782 11.6625 6.19978 11.7346 6.00548 11.7312C5.81118 11.7278 5.62579 11.649 5.48838 11.5116C5.35096 11.3742 5.27225 11.1888 5.26882 10.9945C5.2654 10.8002 5.33752 10.6122 5.47 10.47L11.47 4.47001C11.6106 4.32956 11.8012 4.25067 12 4.25067C12.1987 4.25067 12.3894 4.32956 12.53 4.47001L18.53 10.47C18.6705 10.6106 18.7493 10.8013 18.7493 11C18.7493 11.1988 18.6705 11.3894 18.53 11.53C18.4608 11.6001 18.3782 11.6557 18.2872 11.6935C18.1962 11.7313 18.0985 11.7505 18 11.75Z" |
|||
fill="#000000" |
|||
/> |
|||
<path |
|||
d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z" |
|||
fill="#000000" |
|||
/> |
|||
</svg> |
|||
<span class="max-md:hidden text-sm">{{ $t('sort') }}</span> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const globalStore = useGlobalStore(); |
|||
</script> |
@ -0,0 +1,26 @@ |
|||
<template> |
|||
<component |
|||
:is="elementType" |
|||
role="button" |
|||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 md:px-4 rounded max-md:rounded-full inline-flex items-center transition" |
|||
v-bind="attrs" |
|||
> |
|||
<slot /> |
|||
</component> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const props = defineProps({ |
|||
as: { |
|||
type: String, |
|||
default: 'button', |
|||
}, |
|||
}); |
|||
|
|||
const elementType = computed(() => props.as); |
|||
|
|||
const attrs = computed(() => { |
|||
const { as, ...attrs } = props; |
|||
return attrs; |
|||
}); |
|||
</script> |
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<svg |
|||
inline |
|||
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="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,12 @@ |
|||
<template> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
fill="currentColor" |
|||
> |
|||
<path |
|||
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
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="M6 18L18 6M6 6l12 12" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,13 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
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="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> |
|||
</template> |
@ -0,0 +1,11 @@ |
|||
<template> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="currentColor" |
|||
viewBox="0 0 24 24" |
|||
> |
|||
<path |
|||
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M3.8,12c0-4.5,3.7-8.2,8.2-8.2v16.5C7.5,20.2,3.8,16.5,3.8,12z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,21 @@ |
|||
<template> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
viewBox="0 0 24 24" |
|||
fill="currentColor" |
|||
> |
|||
<circle |
|||
class="opacity-25" |
|||
cx="12" |
|||
cy="12" |
|||
r="10" |
|||
stroke="currentColor" |
|||
stroke-width="4" |
|||
/> |
|||
<path |
|||
class="opacity-75" |
|||
fill="currentColor" |
|||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" |
|||
/> |
|||
</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="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
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> |
|||
</template> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<svg |
|||
inline |
|||
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="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z" |
|||
/> |
|||
</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 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,17 @@ |
|||
<template> |
|||
<!-- Heroicon name: outline/exclamation --> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
aria-hidden="true" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,14 @@ |
|||
<template> |
|||
<ClientOnly> |
|||
<apexchart width="100%" height="100%" :options="options" :series="series" /> |
|||
</ClientOnly> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import type { VueApexChartsComponent } from 'vue3-apexcharts'; |
|||
|
|||
defineProps<{ |
|||
options: VueApexChartsComponent['options']; |
|||
series: VueApexChartsComponent['series']; |
|||
}>(); |
|||
</script> |
@ -0,0 +1,6 @@ |
|||
export const useTheme = useColorMode as () => ThemeInstance; |
|||
|
|||
type ThemeInstance = ReturnType<typeof useColorMode> & { |
|||
preference: 'system' | 'dark' | 'light'; |
|||
value: 'dark' | 'light'; |
|||
}; |
@ -1,47 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const { release: { version } } = require('./package.json'); |
|||
|
|||
module.exports.RELEASE = version; |
|||
module.exports.PORT = process.env.PORT || '51821'; |
|||
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; |
|||
/** This is only kept for migration purpose. DO NOT USE! */ |
|||
module.exports.PASSWORD = process.env.PASSWORD; |
|||
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH; |
|||
module.exports.MAX_AGE = parseInt(process.env.MAX_AGE, 10) * 1000 * 60 || 0; |
|||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/'; |
|||
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0'; |
|||
module.exports.WG_HOST = process.env.WG_HOST; |
|||
module.exports.WG_PORT = process.env.WG_PORT || '51820'; |
|||
module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820'; |
|||
module.exports.WG_MTU = process.env.WG_MTU || null; |
|||
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0'; |
|||
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x'; |
|||
module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string' |
|||
? process.env.WG_DEFAULT_DNS |
|||
: '1.1.1.1'; |
|||
module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; |
|||
|
|||
module.exports.WG_PRE_UP = process.env.WG_PRE_UP || ''; |
|||
module.exports.WG_POST_UP = process.env.WG_POST_UP || ` |
|||
iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; |
|||
iptables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; |
|||
iptables -A FORWARD -i wg0 -j ACCEPT; |
|||
iptables -A FORWARD -o wg0 -j ACCEPT; |
|||
`.split('\n').join(' ');
|
|||
|
|||
module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; |
|||
module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ` |
|||
iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; |
|||
iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; |
|||
iptables -D FORWARD -i wg0 -j ACCEPT; |
|||
iptables -D FORWARD -o wg0 -j ACCEPT; |
|||
`.split('\n').join(' ');
|
|||
module.exports.LANG = process.env.LANG || 'en'; |
|||
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false'; |
|||
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; |
|||
module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS || 'false'; |
|||
module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false'; |
|||
module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false'; |
|||
module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'false'; |
|||
module.exports.PROMETHEUS_METRICS_PASSWORD = process.env.PROMETHEUS_METRICS_PASSWORD; |
@ -0,0 +1,4 @@ |
|||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'; |
|||
import eslintConfigPrettier from 'eslint-config-prettier'; |
|||
|
|||
export default createConfigForNuxt().append(eslintConfigPrettier); |
@ -0,0 +1,679 @@ |
|||
export default defineI18nConfig(() => ({ |
|||
legacy: false, |
|||
locale: 'en', |
|||
messages: { |
|||
en: { |
|||
name: 'Name', |
|||
password: 'Password', |
|||
signIn: 'Sign In', |
|||
logout: 'Logout', |
|||
updateAvailable: 'There is an update available!', |
|||
update: 'Update', |
|||
clients: 'Clients', |
|||
new: 'New', |
|||
deleteClient: 'Delete Client', |
|||
deleteDialog1: 'Are you sure you want to delete', |
|||
deleteDialog2: 'This action cannot be undone.', |
|||
cancel: 'Cancel', |
|||
create: 'Create', |
|||
createdOn: 'Created on ', |
|||
lastSeen: 'Last seen on ', |
|||
totalDownload: 'Total Download: ', |
|||
totalUpload: 'Total Upload: ', |
|||
newClient: 'New Client', |
|||
disableClient: 'Disable Client', |
|||
enableClient: 'Enable Client', |
|||
noClients: 'There are no clients yet.', |
|||
noPrivKey: |
|||
'This client has no known private key. Cannot create Configuration.', |
|||
showQR: 'Show QR Code', |
|||
downloadConfig: 'Download Configuration', |
|||
madeBy: 'Made by', |
|||
donate: 'Donate', |
|||
toggleCharts: 'Show/hide Charts', |
|||
theme: { |
|||
dark: 'Dark theme', |
|||
light: 'Light theme', |
|||
system: 'System theme', |
|||
}, |
|||
restore: 'Restore', |
|||
backup: 'Backup', |
|||
titleRestoreConfig: 'Restore your configuration', |
|||
titleBackupConfig: 'Backup your configuration', |
|||
rememberMe: 'Remember me', |
|||
titleRememberMe: 'Stay logged after closing the browser', |
|||
sort: 'Sort', |
|||
ExpireDate: 'Expire Date', |
|||
Permanent: 'Permanent', |
|||
OneTimeLink: 'Generate short one time link', |
|||
}, |
|||
ua: { |
|||
name: 'Ім`я', |
|||
password: 'Пароль', |
|||
signIn: 'Увійти', |
|||
logout: 'Вихід', |
|||
updateAvailable: 'Доступне оновлення!', |
|||
update: 'Оновити', |
|||
clients: 'Клієнти', |
|||
new: 'Новий', |
|||
deleteClient: 'Видалити клієнта', |
|||
deleteDialog1: 'Ви впевнені, що бажаєте видалити', |
|||
deleteDialog2: 'Цю дію неможливо скасувати.', |
|||
cancel: 'Скасувати', |
|||
create: 'Створити', |
|||
createdOn: 'Створено ', |
|||
lastSeen: 'Останнє підключення в ', |
|||
totalDownload: 'Всього завантажено: ', |
|||
totalUpload: 'Всього відправлено: ', |
|||
newClient: 'Новий клієнт', |
|||
disableClient: 'Вимкнути клієнта', |
|||
enableClient: 'Увімкнути клієнта', |
|||
noClients: 'Ще немає клієнтів.', |
|||
noPrivKey: |
|||
'У цього клієнта немає відомого приватного ключа. Неможливо створити конфігурацію.', |
|||
showQR: 'Показати QR-код', |
|||
downloadConfig: 'Завантажити конфігурацію', |
|||
madeBy: 'Зроблено', |
|||
donate: 'Пожертвувати', |
|||
toggleCharts: 'Показати/сховати діаграми', |
|||
theme: { |
|||
dark: 'Темна тема', |
|||
light: 'Світла тема', |
|||
system: 'Автоматична тема', |
|||
}, |
|||
restore: 'Відновити', |
|||
backup: 'Резервна копія', |
|||
titleRestoreConfig: 'Відновити конфігурацію', |
|||
titleBackupConfig: 'Створити резервну копію конфігурації', |
|||
}, |
|||
ru: { |
|||
name: 'Имя', |
|||
password: 'Пароль', |
|||
signIn: 'Войти', |
|||
logout: 'Выйти', |
|||
updateAvailable: 'Доступно обновление!', |
|||
update: 'Обновить', |
|||
clients: 'Клиенты', |
|||
new: 'Создать', |
|||
deleteClient: 'Удалить клиента', |
|||
deleteDialog1: 'Вы уверены, что хотите удалить', |
|||
deleteDialog2: 'Это действие невозможно отменить.', |
|||
cancel: 'Закрыть', |
|||
create: 'Создать', |
|||
createdOn: 'Создано в ', |
|||
lastSeen: 'Последнее подключение в ', |
|||
totalDownload: 'Всего скачано: ', |
|||
totalUpload: 'Всего загружено: ', |
|||
newClient: 'Создать клиента', |
|||
disableClient: 'Выключить клиента', |
|||
enableClient: 'Включить клиента', |
|||
noClients: 'Пока нет клиентов.', |
|||
noPrivKey: |
|||
'Невозможно создать конфигурацию: у клиента нет известного приватного ключа.', |
|||
showQR: 'Показать QR-код', |
|||
downloadConfig: 'Скачать конфигурацию', |
|||
madeBy: 'Автор', |
|||
donate: 'Поблагодарить', |
|||
toggleCharts: 'Показать/скрыть графики', |
|||
theme: { |
|||
dark: 'Темная тема', |
|||
light: 'Светлая тема', |
|||
system: 'Как в системе', |
|||
}, |
|||
restore: 'Восстановить', |
|||
backup: 'Резервная копия', |
|||
titleRestoreConfig: 'Восстановить конфигурацию', |
|||
titleBackupConfig: 'Создать резервную копию конфигурации', |
|||
rememberMe: 'Запомнить меня', |
|||
titleRememberMe: 'Оставаться в системе после закрытия браузера', |
|||
sort: 'Сортировка', |
|||
ExpireDate: 'Дата истечения срока', |
|||
Permanent: 'Бессрочно', |
|||
OneTimeLink: 'Создать короткую одноразовую ссылку', |
|||
}, |
|||
tr: { |
|||
// Müslüm Barış Korkmazer @babico
|
|||
name: 'İsim', |
|||
password: 'Şifre', |
|||
signIn: 'Giriş Yap', |
|||
logout: 'Çıkış Yap', |
|||
updateAvailable: 'Mevcut bir güncelleme var!', |
|||
update: 'Güncelle', |
|||
clients: 'Kullanıcılar', |
|||
new: 'Yeni', |
|||
deleteClient: 'Kullanıcı Sil', |
|||
deleteDialog1: 'Silmek istediğine emin misin', |
|||
deleteDialog2: 'Bu işlem geri alınamaz.', |
|||
cancel: 'İptal', |
|||
create: 'Oluştur', |
|||
createdOn: 'Şu saatte oluşturuldu: ', |
|||
lastSeen: 'Son görülme tarihi: ', |
|||
totalDownload: 'Toplam İndirme: ', |
|||
totalUpload: 'Toplam Yükleme: ', |
|||
newClient: 'Yeni Kullanıcı', |
|||
disableClient: 'Kullanıcıyı Devre Dışı Bırak', |
|||
enableClient: 'Kullanıcıyı Etkinleştir', |
|||
noClients: 'Henüz kullanıcı yok.', |
|||
noPrivKey: |
|||
'Bu istemcinin bilinen bir özel anahtarı yok. Yapılandırma oluşturulamıyor.', |
|||
showQR: 'QR Kodunu Göster', |
|||
downloadConfig: 'Yapılandırmayı İndir', |
|||
madeBy: 'Yapan Kişi: ', |
|||
donate: 'Bağış Yap', |
|||
toggleCharts: 'Grafiği göster/gizle', |
|||
theme: { |
|||
dark: 'Karanlık tema', |
|||
light: 'Açık tema', |
|||
system: 'Otomatik tema', |
|||
}, |
|||
restore: 'Geri yükle', |
|||
backup: 'Yedekle', |
|||
titleRestoreConfig: 'Yapılandırmanızı geri yükleyin', |
|||
titleBackupConfig: 'Yapılandırmanızı yedekleyin', |
|||
}, |
|||
no: { |
|||
// github.com/digvalley
|
|||
name: 'Navn', |
|||
password: 'Passord', |
|||
signIn: 'Logg Inn', |
|||
logout: 'Logg Ut', |
|||
updateAvailable: 'En ny oppdatering er tilgjengelig!', |
|||
update: 'Oppdater', |
|||
clients: 'Klienter', |
|||
new: 'Ny', |
|||
deleteClient: 'Slett Klient', |
|||
deleteDialog1: 'Er du sikker på at du vil slette?', |
|||
deleteDialog2: 'Denne handlingen kan ikke angres', |
|||
cancel: 'Avbryt', |
|||
create: 'Opprett', |
|||
createdOn: 'Opprettet ', |
|||
lastSeen: 'Sist sett ', |
|||
totalDownload: 'Total Nedlasting: ', |
|||
totalUpload: 'Total Opplasting: ', |
|||
newClient: 'Ny Klient', |
|||
disableClient: 'Deaktiver Klient', |
|||
enableClient: 'Aktiver Klient', |
|||
noClients: 'Ingen klienter opprettet enda.', |
|||
showQR: 'Vis QR Kode', |
|||
downloadConfig: 'Last Ned Konfigurasjon', |
|||
madeBy: 'Laget av', |
|||
donate: 'Doner', |
|||
}, |
|||
pl: { |
|||
// github.com/archont94
|
|||
name: 'Nazwa', |
|||
password: 'Hasło', |
|||
signIn: 'Zaloguj się', |
|||
logout: 'Wyloguj się', |
|||
updateAvailable: 'Dostępna aktualizacja!', |
|||
update: 'Aktualizuj', |
|||
clients: 'Klienci', |
|||
new: 'Stwórz klienta', |
|||
deleteClient: 'Usuń klienta', |
|||
deleteDialog1: 'Jesteś pewny że chcesz usunąć', |
|||
deleteDialog2: 'Tej akcji nie da się cofnąć.', |
|||
cancel: 'Anuluj', |
|||
create: 'Stwórz', |
|||
createdOn: 'Utworzono ', |
|||
lastSeen: 'Ostatnio widziany ', |
|||
totalDownload: 'Całkowite pobieranie: ', |
|||
totalUpload: 'Całkowite wysyłanie: ', |
|||
newClient: 'Nowy klient', |
|||
disableClient: 'Wyłączenie klienta', |
|||
enableClient: 'Włączenie klienta', |
|||
noClients: 'Nie ma jeszcze klientów.', |
|||
showQR: 'Pokaż kod QR', |
|||
downloadConfig: 'Pobierz konfigurację', |
|||
madeBy: 'Stworzone przez', |
|||
donate: 'Wsparcie autora', |
|||
}, |
|||
fr: { |
|||
// github.com/clem3109
|
|||
name: 'Nom', |
|||
password: 'Mot de passe', |
|||
signIn: 'Se Connecter', |
|||
logout: 'Se déconnecter', |
|||
updateAvailable: 'Une mise à jour est disponible !', |
|||
update: 'Mise à jour', |
|||
clients: 'Clients', |
|||
new: 'Nouveau', |
|||
deleteClient: 'Supprimer ce client', |
|||
deleteDialog1: 'Êtes-vous que vous voulez supprimer', |
|||
deleteDialog2: 'Cette action ne peut pas être annulée.', |
|||
cancel: 'Annuler', |
|||
create: 'Créer', |
|||
createdOn: 'Créé le ', |
|||
lastSeen: 'Dernière connexion le ', |
|||
totalDownload: 'Téléchargement total : ', |
|||
totalUpload: 'Téléversement total : ', |
|||
newClient: 'Nouveau client', |
|||
disableClient: 'Désactiver ce client', |
|||
enableClient: 'Activer ce client', |
|||
noClients: 'Aucun client pour le moment.', |
|||
showQR: 'Afficher le code à réponse rapide (QR Code)', |
|||
downloadConfig: 'Télécharger la configuration', |
|||
madeBy: 'Développé par', |
|||
donate: 'Soutenir', |
|||
restore: 'Restaurer', |
|||
backup: 'Sauvegarder', |
|||
titleRestoreConfig: 'Restaurer votre configuration', |
|||
titleBackupConfig: 'Sauvegarder votre configuration', |
|||
}, |
|||
de: { |
|||
// github.com/florian-asche
|
|||
name: 'Name', |
|||
password: 'Passwort', |
|||
signIn: 'Anmelden', |
|||
logout: 'Abmelden', |
|||
updateAvailable: 'Eine Aktualisierung steht zur Verfügung!', |
|||
update: 'Aktualisieren', |
|||
clients: 'Clients', |
|||
new: 'Neu', |
|||
deleteClient: 'Client löschen', |
|||
deleteDialog1: 'Möchtest du wirklich löschen?', |
|||
deleteDialog2: 'Diese Aktion kann nicht rückgängig gemacht werden.', |
|||
cancel: 'Abbrechen', |
|||
create: 'Erstellen', |
|||
createdOn: 'Erstellt am ', |
|||
lastSeen: 'Zuletzt Online ', |
|||
totalDownload: 'Gesamt Download: ', |
|||
totalUpload: 'Gesamt Upload: ', |
|||
newClient: 'Neuer Client', |
|||
disableClient: 'Client deaktivieren', |
|||
enableClient: 'Client aktivieren', |
|||
noClients: 'Es wurden noch keine Clients konfiguriert.', |
|||
noPrivKey: |
|||
'Es ist kein Private Key für diesen Client bekannt. Eine Konfiguration kann nicht erstellt werden.', |
|||
showQR: 'Zeige den QR Code', |
|||
downloadConfig: 'Konfiguration herunterladen', |
|||
madeBy: 'Erstellt von', |
|||
donate: 'Spenden', |
|||
restore: 'Wiederherstellen', |
|||
backup: 'Sichern', |
|||
titleRestoreConfig: 'Stelle deine Konfiguration wieder her', |
|||
titleBackupConfig: 'Sichere deine Konfiguration', |
|||
}, |
|||
ca: { |
|||
// github.com/guillembonet
|
|||
name: 'Nom', |
|||
password: 'Contrasenya', |
|||
signIn: 'Iniciar sessió', |
|||
logout: 'Tanca sessió', |
|||
updateAvailable: 'Hi ha una actualització disponible!', |
|||
update: 'Actualitza', |
|||
clients: 'Clients', |
|||
new: 'Nou', |
|||
deleteClient: 'Esborra client', |
|||
deleteDialog1: 'Estàs segur que vols esborrar aquest client?', |
|||
deleteDialog2: 'Aquesta acció no es pot desfer.', |
|||
cancel: 'Cancel·la', |
|||
create: 'Crea', |
|||
createdOn: 'Creat el ', |
|||
lastSeen: 'Última connexió el ', |
|||
totalDownload: 'Baixada total: ', |
|||
totalUpload: 'Pujada total: ', |
|||
newClient: 'Nou client', |
|||
disableClient: 'Desactiva client', |
|||
enableClient: 'Activa client', |
|||
noClients: 'Encara no hi ha cap client.', |
|||
showQR: 'Mostra codi QR', |
|||
downloadConfig: 'Descarrega configuració', |
|||
madeBy: 'Fet per', |
|||
donate: 'Donatiu', |
|||
}, |
|||
es: { |
|||
// github.com/amarqz
|
|||
name: 'Nombre', |
|||
password: 'Contraseña', |
|||
signIn: 'Iniciar sesión', |
|||
logout: 'Cerrar sesión', |
|||
updateAvailable: '¡Hay una actualización disponible!', |
|||
update: 'Actualizar', |
|||
clients: 'Clientes', |
|||
new: 'Nuevo', |
|||
deleteClient: 'Eliminar cliente', |
|||
deleteDialog1: '¿Estás seguro de que quieres borrar este cliente?', |
|||
deleteDialog2: 'Esta acción no podrá ser revertida.', |
|||
cancel: 'Cancelar', |
|||
create: 'Crear', |
|||
createdOn: 'Creado el ', |
|||
lastSeen: 'Última conexión el ', |
|||
totalDownload: 'Total descargado: ', |
|||
totalUpload: 'Total subido: ', |
|||
newClient: 'Nuevo cliente', |
|||
disableClient: 'Desactivar cliente', |
|||
enableClient: 'Activar cliente', |
|||
noClients: 'Aún no hay ningún cliente.', |
|||
showQR: 'Mostrar código QR', |
|||
downloadConfig: 'Descargar configuración', |
|||
madeBy: 'Hecho por', |
|||
donate: 'Donar', |
|||
toggleCharts: 'Mostrar/Ocultar gráficos', |
|||
theme: { |
|||
dark: 'Modo oscuro', |
|||
light: 'Modo claro', |
|||
system: 'Modo automático', |
|||
}, |
|||
restore: 'Restaurar', |
|||
backup: 'Realizar copia de seguridad', |
|||
titleRestoreConfig: 'Restaurar su configuración', |
|||
titleBackupConfig: 'Realizar copia de seguridad de su configuración', |
|||
}, |
|||
ko: { |
|||
name: '이름', |
|||
password: '암호', |
|||
signIn: '로그인', |
|||
logout: '로그아웃', |
|||
updateAvailable: '업데이트가 있습니다!', |
|||
update: '업데이트', |
|||
clients: '클라이언트', |
|||
new: '추가', |
|||
deleteClient: '클라이언트 삭제', |
|||
deleteDialog1: '삭제 하시겠습니까?', |
|||
deleteDialog2: '이 작업은 취소할 수 없습니다.', |
|||
cancel: '취소', |
|||
create: '생성', |
|||
createdOn: '생성일: ', |
|||
lastSeen: '마지막 사용 날짜: ', |
|||
totalDownload: '총 다운로드: ', |
|||
totalUpload: '총 업로드: ', |
|||
newClient: '새로운 클라이언트', |
|||
disableClient: '클라이언트 비활성화', |
|||
enableClient: '클라이언트 활성화', |
|||
noClients: '아직 클라이언트가 없습니다.', |
|||
showQR: 'QR 코드 표시', |
|||
downloadConfig: '구성 다운로드', |
|||
madeBy: '만든 사람', |
|||
donate: '기부', |
|||
toggleCharts: '차트 표시/숨기기', |
|||
theme: { dark: '어두운 테마', light: '밝은 테마', auto: '자동 테마' }, |
|||
restore: '복원', |
|||
backup: '백업', |
|||
titleRestoreConfig: '구성 파일 복원', |
|||
titleBackupConfig: '구성 파일 백업', |
|||
}, |
|||
vi: { |
|||
// https://github.com/hoangneeee
|
|||
name: 'Tên', |
|||
password: 'Mật khẩu', |
|||
signIn: 'Đăng nhập', |
|||
logout: 'Đăng xuất', |
|||
updateAvailable: 'Có bản cập nhật mới!', |
|||
update: 'Cập nhật', |
|||
clients: 'Danh sách người dùng', |
|||
new: 'Mới', |
|||
deleteClient: 'Xóa người dùng', |
|||
deleteDialog1: 'Bạn có chắc chắn muốn xóa', |
|||
deleteDialog2: 'Thao tác này không thể hoàn tác.', |
|||
cancel: 'Huỷ', |
|||
create: 'Tạo', |
|||
createdOn: 'Được tạo lúc ', |
|||
lastSeen: 'Lần xem cuối vào ', |
|||
totalDownload: 'Tổng dung lượng tải xuống: ', |
|||
totalUpload: 'Tổng dung lượng tải lên: ', |
|||
newClient: 'Người dùng mới', |
|||
disableClient: 'Vô hiệu hóa người dùng', |
|||
enableClient: 'Kích hoạt người dùng', |
|||
noClients: 'Hiện chưa có người dùng nào.', |
|||
showQR: 'Hiển thị mã QR', |
|||
downloadConfig: 'Tải xuống cấu hình', |
|||
madeBy: 'Được tạo bởi', |
|||
donate: 'Ủng hộ', |
|||
toggleCharts: 'Mở/Ẩn Biểu đồ', |
|||
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' }, |
|||
restore: 'Khôi phục', |
|||
backup: 'Sao lưu', |
|||
titleRestoreConfig: 'Khôi phục cấu hình của bạn', |
|||
titleBackupConfig: 'Sao lưu cấu hình của bạn', |
|||
sort: 'Sắp xếp', |
|||
}, |
|||
nl: { |
|||
name: 'Naam', |
|||
password: 'Wachtwoord', |
|||
signIn: 'Inloggen', |
|||
logout: 'Uitloggen', |
|||
updateAvailable: 'Nieuw update beschikbaar!', |
|||
update: 'update', |
|||
clients: 'clients', |
|||
new: 'Nieuw', |
|||
deleteClient: 'client verwijderen', |
|||
deleteDialog1: 'Weet je zeker dat je wilt verwijderen', |
|||
deleteDialog2: 'Deze actie kan niet ongedaan worden gemaakt.', |
|||
cancel: 'Annuleren', |
|||
create: 'Creëren', |
|||
createdOn: 'Gemaakt op ', |
|||
lastSeen: 'Laatst gezien op ', |
|||
totalDownload: 'Totaal Gedownload: ', |
|||
totalUpload: 'Totaal Geupload: ', |
|||
newClient: 'Nieuwe client', |
|||
disableClient: 'client uitschakelen', |
|||
enableClient: 'client inschakelen', |
|||
noClients: 'Er zijn nog geen clients.', |
|||
showQR: 'QR-code weergeven', |
|||
downloadConfig: 'Configuratie downloaden', |
|||
madeBy: 'Gemaakt door', |
|||
donate: 'Doneren', |
|||
}, |
|||
is: { |
|||
name: 'Nafn', |
|||
password: 'Lykilorð', |
|||
signIn: 'Skrá inn', |
|||
logout: 'Útskráning', |
|||
updateAvailable: 'Það er uppfærsla í boði!', |
|||
update: 'Uppfæra', |
|||
clients: 'Viðskiptavinir', |
|||
new: 'Nýtt', |
|||
deleteClient: 'Eyða viðskiptavin', |
|||
deleteDialog1: 'Ertu viss um að þú viljir eyða', |
|||
deleteDialog2: 'Þessi aðgerð getur ekki verið afturkallað.', |
|||
cancel: 'Hætta við', |
|||
create: 'Búa til', |
|||
createdOn: 'Búið til á ', |
|||
lastSeen: 'Síðast séð á ', |
|||
totalDownload: 'Samtals Niðurhlaða: ', |
|||
totalUpload: 'Samtals Upphlaða: ', |
|||
newClient: 'Nýr Viðskiptavinur', |
|||
disableClient: 'Gera viðskiptavin óvirkan', |
|||
enableClient: 'Gera viðskiptavin virkan', |
|||
noClients: 'Engir viðskiptavinir ennþá.', |
|||
showQR: 'Sýna QR-kóða', |
|||
downloadConfig: 'Niðurhal Stillingar', |
|||
madeBy: 'Gert af', |
|||
donate: 'Gefa', |
|||
}, |
|||
pt: { |
|||
name: 'Nome', |
|||
password: 'Palavra Chave', |
|||
signIn: 'Entrar', |
|||
logout: 'Sair', |
|||
updateAvailable: 'Existe uma atualização disponível!', |
|||
update: 'Atualizar', |
|||
clients: 'Clientes', |
|||
new: 'Novo', |
|||
deleteClient: 'Apagar Clientes', |
|||
deleteDialog1: 'Tem certeza que pretende apagar', |
|||
deleteDialog2: 'Esta ação não pode ser revertida.', |
|||
cancel: 'Cancelar', |
|||
create: 'Criar', |
|||
createdOn: 'Criado em ', |
|||
lastSeen: 'Último acesso em ', |
|||
totalDownload: 'Total Download: ', |
|||
totalUpload: 'Total Upload: ', |
|||
newClient: 'Novo Cliente', |
|||
disableClient: 'Desativar Cliente', |
|||
enableClient: 'Ativar Cliente', |
|||
noClients: 'Não existem ainda clientes.', |
|||
showQR: 'Apresentar o código QR', |
|||
downloadConfig: 'Descarregar Configuração', |
|||
madeBy: 'Feito por', |
|||
donate: 'Doar', |
|||
}, |
|||
chs: { |
|||
name: '名称', |
|||
password: '密码', |
|||
signIn: '登录', |
|||
logout: '退出', |
|||
updateAvailable: '有新版本可用!', |
|||
update: '更新', |
|||
clients: '客户端', |
|||
new: '新建', |
|||
deleteClient: '删除客户端', |
|||
deleteDialog1: '您确定要删除', |
|||
deleteDialog2: '此操作无法撤销。', |
|||
cancel: '取消', |
|||
create: '创建', |
|||
createdOn: '创建于 ', |
|||
lastSeen: '最后访问于 ', |
|||
totalDownload: '总下载: ', |
|||
totalUpload: '总上传: ', |
|||
newClient: '新建客户端', |
|||
disableClient: '禁用客户端', |
|||
enableClient: '启用客户端', |
|||
noClients: '目前没有客户端。', |
|||
noPrivKey: '此客户端没有已知的私钥。无法创建配置。', |
|||
showQR: '显示二维码', |
|||
downloadConfig: '下载配置', |
|||
madeBy: '由', |
|||
donate: '捐赠', |
|||
toggleCharts: '显示/隐藏图表', |
|||
theme: { dark: '暗黑主题', light: '明亮主题', auto: '自动主题' }, |
|||
restore: '恢复', |
|||
backup: '备份', |
|||
titleRestoreConfig: '恢复您的配置', |
|||
titleBackupConfig: '备份您的配置', |
|||
rememberMe: '记住我', |
|||
titleRememberMe: '关闭浏览器后保持登录', |
|||
sort: '排序', |
|||
ExpireDate: '到期日期', |
|||
Permanent: '永久', |
|||
OneTimeLink: '生成一次性短链接', |
|||
}, |
|||
cht: { |
|||
name: '名字', |
|||
password: '密碼', |
|||
signIn: '登入', |
|||
logout: '登出', |
|||
updateAvailable: '有新版本可以使用!', |
|||
update: '更新', |
|||
clients: '使用者', |
|||
new: '建立', |
|||
deleteClient: '刪除使用者', |
|||
deleteDialog1: '您確定要刪除', |
|||
deleteDialog2: '此作業無法復原。', |
|||
cancel: '取消', |
|||
create: '建立', |
|||
createdOn: '建立於 ', |
|||
lastSeen: '最後存取於 ', |
|||
totalDownload: '總下載: ', |
|||
totalUpload: '總上傳: ', |
|||
newClient: '新用戶', |
|||
disableClient: '停用使用者', |
|||
enableClient: '啟用使用者', |
|||
noClients: '目前沒有使用者。', |
|||
noPrivKey: '此使用者沒有已知的私鑰。無法創建配置。', |
|||
showQR: '顯示 QR Code', |
|||
downloadConfig: '下載 Config 檔', |
|||
madeBy: '由', |
|||
donate: '抖內', |
|||
toggleCharts: '顯示/隱藏圖表', |
|||
theme: { dark: '暗黑主題', light: '明亮主題', auto: '自動主題' }, |
|||
restore: '恢復', |
|||
backup: '備份', |
|||
titleRestoreConfig: '恢復您的配置', |
|||
titleBackupConfig: '備份您的配置', |
|||
rememberMe: '記住我', |
|||
titleRememberMe: '關閉瀏覽器後保持登錄', |
|||
sort: '排序', |
|||
ExpireDate: '到期日期', |
|||
Permanent: '永久', |
|||
OneTimeLink: '生成一次性短鏈接', |
|||
}, |
|||
it: { |
|||
name: 'Nome', |
|||
password: 'Password', |
|||
signIn: 'Accedi', |
|||
logout: 'Esci', |
|||
updateAvailable: 'È disponibile un aggiornamento!', |
|||
update: 'Aggiorna', |
|||
clients: 'Client', |
|||
new: 'Nuovo', |
|||
deleteClient: 'Elimina Client', |
|||
deleteDialog1: 'Sei sicuro di voler eliminare', |
|||
deleteDialog2: 'Questa azione non può essere annullata.', |
|||
cancel: 'Annulla', |
|||
create: 'Crea', |
|||
createdOn: 'Creato il ', |
|||
lastSeen: "Visto l'ultima volta il ", |
|||
totalDownload: 'Totale Download: ', |
|||
totalUpload: 'Totale Upload: ', |
|||
newClient: 'Nuovo Client', |
|||
disableClient: 'Disabilita Client', |
|||
enableClient: 'Abilita Client', |
|||
noClients: 'Non ci sono ancora client.', |
|||
showQR: 'Mostra codice QR', |
|||
downloadConfig: 'Scarica configurazione', |
|||
madeBy: 'Realizzato da', |
|||
donate: 'Donazione', |
|||
restore: 'Ripristina', |
|||
backup: 'Backup', |
|||
titleRestoreConfig: 'Ripristina la tua configurazione', |
|||
titleBackupConfig: 'Esegui il backup della tua configurazione', |
|||
}, |
|||
th: { |
|||
name: 'ชื่อ', |
|||
password: 'รหัสผ่าน', |
|||
signIn: 'ลงชื่อเข้าใช้', |
|||
logout: 'ออกจากระบบ', |
|||
updateAvailable: 'มีอัปเดตพร้อมใช้งาน!', |
|||
update: 'อัปเดต', |
|||
clients: 'Clients', |
|||
new: 'ใหม่', |
|||
deleteClient: 'ลบ Client', |
|||
deleteDialog1: 'คุณแน่ใจหรือไม่ว่าต้องการลบ', |
|||
deleteDialog2: 'การกระทำนี้;ไม่สามารถยกเลิกได้', |
|||
cancel: 'ยกเลิก', |
|||
create: 'สร้าง', |
|||
createdOn: 'สร้างเมื่อ ', |
|||
lastSeen: 'เห็นครั้งสุดท้ายเมื่อ ', |
|||
totalDownload: 'ดาวน์โหลดทั้งหมด: ', |
|||
totalUpload: 'อัพโหลดทั้งหมด: ', |
|||
newClient: 'Client ใหม่', |
|||
disableClient: 'ปิดการใช้งาน Client', |
|||
enableClient: 'เปิดการใช้งาน Client', |
|||
noClients: 'ยังไม่มี Clients เลย', |
|||
showQR: 'แสดงรหัส QR', |
|||
downloadConfig: 'ดาวน์โหลดการตั้งค่า', |
|||
madeBy: 'สร้างโดย', |
|||
donate: 'บริจาค', |
|||
}, |
|||
hi: { |
|||
// github.com/rahilarious
|
|||
name: 'नाम', |
|||
password: 'पासवर्ड', |
|||
signIn: 'लॉगिन', |
|||
logout: 'लॉगआउट', |
|||
updateAvailable: 'अपडेट उपलब्ध है!', |
|||
update: 'अपडेट', |
|||
clients: 'उपयोगकर्ताये', |
|||
new: 'नया', |
|||
deleteClient: 'उपयोगकर्ता हटाएँ', |
|||
deleteDialog1: 'क्या आपको पक्का हटाना है', |
|||
deleteDialog2: 'यह निर्णय पलट नहीं सकता।', |
|||
cancel: 'कुछ ना करें', |
|||
create: 'बनाएं', |
|||
createdOn: 'सर्जन तारीख ', |
|||
lastSeen: 'पिछली बार देखे गए थे ', |
|||
totalDownload: 'कुल डाउनलोड: ', |
|||
totalUpload: 'कुल अपलोड: ', |
|||
newClient: 'नया उपयोगकर्ता', |
|||
disableClient: 'उपयोगकर्ता स्थगित कीजिये', |
|||
enableClient: 'उपयोगकर्ता शुरू कीजिये', |
|||
noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।', |
|||
noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।', |
|||
showQR: 'क्यू आर कोड देखिये', |
|||
downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन', |
|||
madeBy: 'सर्जक', |
|||
donate: 'दान करें', |
|||
}, |
|||
}, |
|||
})); |
@ -0,0 +1,35 @@ |
|||
<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 |
|||
> |
|||
© 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"></script> |
@ -0,0 +1,129 @@ |
|||
<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.uiChartType > 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="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${currentRelease} → v${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>{{ latestRelease.changelog }}</p> |
|||
</div> |
|||
|
|||
<a |
|||
href="https://github.com/wg-easy/wg-easy#updating" |
|||
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 currentRelease = ref<null | number>(null); |
|||
const latestRelease = ref<null | { version: number; changelog: string }>(null); |
|||
|
|||
const theme = useTheme(); |
|||
|
|||
globalStore.fetchChartType(); |
|||
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> |
@ -1,441 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const bcrypt = require('bcryptjs'); |
|||
const crypto = require('node:crypto'); |
|||
const basicAuth = require('basic-auth'); |
|||
const { createServer } = require('node:http'); |
|||
const { stat, readFile } = require('node:fs/promises'); |
|||
const { resolve, sep } = require('node:path'); |
|||
|
|||
const expressSession = require('express-session'); |
|||
const debug = require('debug')('Server'); |
|||
|
|||
const { |
|||
createApp, |
|||
createError, |
|||
createRouter, |
|||
defineEventHandler, |
|||
fromNodeMiddleware, |
|||
getRouterParam, |
|||
toNodeListener, |
|||
readBody, |
|||
setHeader, |
|||
serveStatic, |
|||
} = require('h3'); |
|||
|
|||
const WireGuard = require('../services/WireGuard'); |
|||
|
|||
const { |
|||
PORT, |
|||
WEBUI_HOST, |
|||
RELEASE, |
|||
PASSWORD, |
|||
PASSWORD_HASH, |
|||
MAX_AGE, |
|||
LANG, |
|||
UI_TRAFFIC_STATS, |
|||
UI_CHART_TYPE, |
|||
WG_ENABLE_ONE_TIME_LINKS, |
|||
UI_ENABLE_SORT_CLIENTS, |
|||
WG_ENABLE_EXPIRES_TIME, |
|||
ENABLE_PROMETHEUS_METRICS, |
|||
PROMETHEUS_METRICS_PASSWORD, |
|||
} = require('../config'); |
|||
|
|||
const requiresPassword = !!PASSWORD_HASH; |
|||
const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD; |
|||
|
|||
/** |
|||
* Checks if `password` matches the PASSWORD_HASH. |
|||
* |
|||
* If environment variable is not set, the password is always invalid. |
|||
* |
|||
* @param {string} password String to test |
|||
* @returns {boolean} true if matching environment, otherwise false |
|||
*/ |
|||
const isPasswordValid = (password, hash) => { |
|||
if (typeof password !== 'string') { |
|||
return false; |
|||
} |
|||
if (hash) { |
|||
return bcrypt.compareSync(password, hash); |
|||
} |
|||
|
|||
return false; |
|||
}; |
|||
|
|||
const cronJobEveryMinute = async () => { |
|||
await WireGuard.cronJobEveryMinute(); |
|||
setTimeout(cronJobEveryMinute, 60 * 1000); |
|||
}; |
|||
|
|||
module.exports = class Server { |
|||
|
|||
constructor() { |
|||
const app = createApp(); |
|||
this.app = app; |
|||
|
|||
app.use(fromNodeMiddleware(expressSession({ |
|||
secret: crypto.randomBytes(256).toString('hex'), |
|||
resave: true, |
|||
saveUninitialized: true, |
|||
}))); |
|||
|
|||
const router = createRouter(); |
|||
app.use(router); |
|||
|
|||
router |
|||
.get('/api/release', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return RELEASE; |
|||
})) |
|||
|
|||
.get('/api/lang', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${LANG}"`; |
|||
})) |
|||
|
|||
.get('/api/remember-me', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return MAX_AGE > 0; |
|||
})) |
|||
|
|||
.get('/api/ui-traffic-stats', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `${UI_TRAFFIC_STATS}`; |
|||
})) |
|||
|
|||
.get('/api/ui-chart-type', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${UI_CHART_TYPE}"`; |
|||
})) |
|||
|
|||
.get('/api/wg-enable-one-time-links', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `${WG_ENABLE_ONE_TIME_LINKS}`; |
|||
})) |
|||
|
|||
.get('/api/ui-sort-clients', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `${UI_ENABLE_SORT_CLIENTS}`; |
|||
})) |
|||
|
|||
.get('/api/wg-enable-expire-time', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `${WG_ENABLE_EXPIRES_TIME}`; |
|||
})) |
|||
|
|||
// Authentication
|
|||
.get('/api/session', defineEventHandler((event) => { |
|||
const authenticated = requiresPassword |
|||
? !!(event.node.req.session && event.node.req.session.authenticated) |
|||
: true; |
|||
|
|||
return { |
|||
requiresPassword, |
|||
authenticated, |
|||
}; |
|||
})) |
|||
.get('/cnf/:clientOneTimeLink', defineEventHandler(async (event) => { |
|||
if (WG_ENABLE_ONE_TIME_LINKS === 'false') { |
|||
throw createError({ |
|||
status: 404, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink'); |
|||
const clients = await WireGuard.getClients(); |
|||
const client = clients.find((client) => client.oneTimeLink === clientOneTimeLink); |
|||
if (!client) return; |
|||
const clientId = client.id; |
|||
const config = await WireGuard.getClientConfiguration({ clientId }); |
|||
await WireGuard.eraseOneTimeLink({ clientId }); |
|||
setHeader(event, 'Content-Disposition', `attachment; filename="${clientOneTimeLink}.conf"`); |
|||
setHeader(event, 'Content-Type', 'text/plain'); |
|||
return config; |
|||
})) |
|||
.post('/api/session', defineEventHandler(async (event) => { |
|||
const { password, remember } = await readBody(event); |
|||
|
|||
if (!requiresPassword) { |
|||
// if no password is required, the API should never be called.
|
|||
// Do not automatically authenticate the user.
|
|||
throw createError({ |
|||
status: 401, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
|
|||
if (!isPasswordValid(password, PASSWORD_HASH)) { |
|||
throw createError({ |
|||
status: 401, |
|||
message: 'Incorrect Password', |
|||
}); |
|||
} |
|||
|
|||
if (MAX_AGE && remember) { |
|||
event.node.req.session.cookie.maxAge = MAX_AGE; |
|||
} |
|||
event.node.req.session.authenticated = true; |
|||
event.node.req.session.save(); |
|||
|
|||
debug(`New Session: ${event.node.req.session.id}`); |
|||
|
|||
return { success: true }; |
|||
})); |
|||
|
|||
// WireGuard
|
|||
app.use( |
|||
fromNodeMiddleware((req, res, next) => { |
|||
if (!requiresPassword || !req.url.startsWith('/api/')) { |
|||
return next(); |
|||
} |
|||
|
|||
if (req.session && req.session.authenticated) { |
|||
return next(); |
|||
} |
|||
|
|||
if (req.url.startsWith('/api/') && req.headers['authorization']) { |
|||
if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) { |
|||
return next(); |
|||
} |
|||
return res.status(401).json({ |
|||
error: 'Incorrect Password', |
|||
}); |
|||
} |
|||
|
|||
return res.status(401).json({ |
|||
error: 'Not Logged In', |
|||
}); |
|||
}), |
|||
); |
|||
|
|||
const router2 = createRouter(); |
|||
app.use(router2); |
|||
|
|||
router2 |
|||
.delete('/api/session', defineEventHandler((event) => { |
|||
const sessionId = event.node.req.session.id; |
|||
|
|||
event.node.req.session.destroy(); |
|||
|
|||
debug(`Deleted Session: ${sessionId}`); |
|||
return { success: true }; |
|||
})) |
|||
.get('/api/wireguard/client', defineEventHandler(() => { |
|||
return WireGuard.getClients(); |
|||
})) |
|||
.get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
const svg = await WireGuard.getClientQRCodeSVG({ clientId }); |
|||
setHeader(event, 'Content-Type', 'image/svg+xml'); |
|||
return svg; |
|||
})) |
|||
.get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
const client = await WireGuard.getClient({ clientId }); |
|||
const config = await WireGuard.getClientConfiguration({ clientId }); |
|||
const configName = client.name |
|||
.replace(/[^a-zA-Z0-9_=+.-]/g, '-') |
|||
.replace(/(-{2,}|-$)/g, '-') |
|||
.replace(/-$/, '') |
|||
.substring(0, 32); |
|||
setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`); |
|||
setHeader(event, 'Content-Type', 'text/plain'); |
|||
return config; |
|||
})) |
|||
.post('/api/wireguard/client', defineEventHandler(async (event) => { |
|||
const { name } = await readBody(event); |
|||
const { expiredDate } = await readBody(event); |
|||
await WireGuard.createClient({ name, expiredDate }); |
|||
return { success: true }; |
|||
})) |
|||
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
await WireGuard.deleteClient({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
await WireGuard.enableClient({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.post('/api/wireguard/client/:clientId/generateOneTimeLink', defineEventHandler(async (event) => { |
|||
if (WG_ENABLE_ONE_TIME_LINKS === 'false') { |
|||
throw createError({ |
|||
status: 404, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
await WireGuard.generateOneTimeLink({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
await WireGuard.disableClient({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
const { name } = await readBody(event); |
|||
await WireGuard.updateClientName({ clientId, name }); |
|||
return { success: true }; |
|||
})) |
|||
.put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
const { address } = await readBody(event); |
|||
await WireGuard.updateClientAddress({ clientId, address }); |
|||
return { success: true }; |
|||
})) |
|||
.put('/api/wireguard/client/:clientId/expireDate', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
const { expireDate } = await readBody(event); |
|||
await WireGuard.updateClientExpireDate({ clientId, expireDate }); |
|||
return { success: true }; |
|||
})); |
|||
|
|||
const safePathJoin = (base, target) => { |
|||
// Manage web root (edge case)
|
|||
if (target === '/') { |
|||
return `${base}${sep}`; |
|||
} |
|||
|
|||
// Prepend './' to prevent absolute paths
|
|||
const targetPath = `.${sep}${target}`; |
|||
|
|||
// Resolve the absolute path
|
|||
const resolvedPath = resolve(base, targetPath); |
|||
|
|||
// Check if resolvedPath is a subpath of base
|
|||
if (resolvedPath.startsWith(`${base}${sep}`)) { |
|||
return resolvedPath; |
|||
} |
|||
|
|||
throw createError({ |
|||
status: 400, |
|||
message: 'Bad Request', |
|||
}); |
|||
}; |
|||
|
|||
// Check Prometheus credentials
|
|||
app.use( |
|||
fromNodeMiddleware((req, res, next) => { |
|||
if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) { |
|||
return next(); |
|||
} |
|||
const user = basicAuth(req); |
|||
if (!user) { |
|||
res.statusCode = 401; |
|||
return { error: 'Not Logged In' }; |
|||
} |
|||
if (user.pass) { |
|||
if (isPasswordValid(user.pass, PROMETHEUS_METRICS_PASSWORD)) { |
|||
return next(); |
|||
} |
|||
res.statusCode = 401; |
|||
return { error: 'Incorrect Password' }; |
|||
} |
|||
res.statusCode = 401; |
|||
return { error: 'Not Logged In' }; |
|||
}), |
|||
); |
|||
|
|||
// Prometheus Metrics API
|
|||
const routerPrometheusMetrics = createRouter(); |
|||
app.use(routerPrometheusMetrics); |
|||
|
|||
// Prometheus Routes
|
|||
routerPrometheusMetrics |
|||
.get('/metrics', defineEventHandler(async (event) => { |
|||
setHeader(event, 'Content-Type', 'text/plain'); |
|||
if (ENABLE_PROMETHEUS_METRICS === 'true') { |
|||
return WireGuard.getMetrics(); |
|||
} |
|||
return ''; |
|||
})) |
|||
.get('/metrics/json', defineEventHandler(async (event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
if (ENABLE_PROMETHEUS_METRICS === 'true') { |
|||
return WireGuard.getMetricsJSON(); |
|||
} |
|||
return ''; |
|||
})); |
|||
|
|||
// backup_restore
|
|||
const router3 = createRouter(); |
|||
app.use(router3); |
|||
|
|||
router3 |
|||
.get('/api/wireguard/backup', defineEventHandler(async (event) => { |
|||
const config = await WireGuard.backupConfiguration(); |
|||
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"'); |
|||
setHeader(event, 'Content-Type', 'text/json'); |
|||
return config; |
|||
})) |
|||
.put('/api/wireguard/restore', defineEventHandler(async (event) => { |
|||
const { file } = await readBody(event); |
|||
await WireGuard.restoreConfiguration(file); |
|||
return { success: true }; |
|||
})); |
|||
|
|||
// Static assets
|
|||
const publicDir = '/app/www'; |
|||
app.use( |
|||
defineEventHandler((event) => { |
|||
return serveStatic(event, { |
|||
getContents: (id) => { |
|||
return readFile(safePathJoin(publicDir, id)); |
|||
}, |
|||
getMeta: async (id) => { |
|||
const filePath = safePathJoin(publicDir, id); |
|||
|
|||
const stats = await stat(filePath).catch(() => {}); |
|||
if (!stats || !stats.isFile()) { |
|||
return; |
|||
} |
|||
|
|||
if (id.endsWith('.html')) setHeader(event, 'Content-Type', 'text/html'); |
|||
if (id.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript'); |
|||
if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json'); |
|||
if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css'); |
|||
if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png'); |
|||
|
|||
return { |
|||
size: stats.size, |
|||
mtime: stats.mtimeMs, |
|||
}; |
|||
}, |
|||
}); |
|||
}), |
|||
); |
|||
|
|||
if (PASSWORD) { |
|||
throw new Error('DO NOT USE PASSWORD ENVIRONMENT VARIABLE. USE PASSWORD_HASH INSTEAD.\nSee https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md'); |
|||
} |
|||
|
|||
createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST); |
|||
debug(`Listening on http://${WEBUI_HOST}:${PORT}`); |
|||
|
|||
cronJobEveryMinute(); |
|||
} |
|||
|
|||
}; |
@ -1,10 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
module.exports = class ServerError extends Error { |
|||
|
|||
constructor(message, statusCode = 500) { |
|||
super(message); |
|||
this.statusCode = statusCode; |
|||
} |
|||
|
|||
}; |
@ -1,80 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const childProcess = require('child_process'); |
|||
|
|||
module.exports = class Util { |
|||
|
|||
static isValidIPv4(str) { |
|||
const blocks = str.split('.'); |
|||
if (blocks.length !== 4) return false; |
|||
|
|||
for (let value of blocks) { |
|||
value = parseInt(value, 10); |
|||
if (Number.isNaN(value)) return false; |
|||
if (value < 0 || value > 255) return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static promisify(fn) { |
|||
// eslint-disable-next-line func-names
|
|||
return function(req, res) { |
|||
Promise.resolve().then(async () => fn(req, res)) |
|||
.then((result) => { |
|||
if (res.headersSent) return; |
|||
|
|||
if (typeof result === 'undefined') { |
|||
return res |
|||
.status(204) |
|||
.end(); |
|||
} |
|||
|
|||
return res |
|||
.status(200) |
|||
.json(result); |
|||
}) |
|||
.catch((error) => { |
|||
if (typeof error === 'string') { |
|||
error = new Error(error); |
|||
} |
|||
|
|||
// eslint-disable-next-line no-console
|
|||
console.error(error); |
|||
|
|||
return res |
|||
.status(error.statusCode || 500) |
|||
.json({ |
|||
error: error.message || error.toString(), |
|||
stack: error.stack, |
|||
}); |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
static async exec(cmd, { |
|||
log = true, |
|||
} = {}) { |
|||
if (typeof log === 'string') { |
|||
// eslint-disable-next-line no-console
|
|||
console.log(`$ ${log}`); |
|||
} else if (log === true) { |
|||
// eslint-disable-next-line no-console
|
|||
console.log(`$ ${cmd}`); |
|||
} |
|||
|
|||
if (process.platform !== 'linux') { |
|||
return ''; |
|||
} |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
childProcess.exec(cmd, { |
|||
shell: 'bash', |
|||
}, (err, stdout) => { |
|||
if (err) return reject(err); |
|||
return resolve(String(stdout).trim()); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
}; |
@ -0,0 +1,20 @@ |
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|||
export default defineNuxtConfig({ |
|||
future: { |
|||
compatibilityVersion: 4, |
|||
}, |
|||
compatibilityDate: '2024-04-03', |
|||
devtools: { enabled: true }, |
|||
modules: [ |
|||
'@nuxtjs/i18n', |
|||
'@nuxtjs/tailwindcss', |
|||
'@pinia/nuxt', |
|||
'@eschricht/nuxt-color-mode', |
|||
], |
|||
colorMode: { |
|||
preference: 'system', |
|||
fallback: 'light', |
|||
classSuffix: '', |
|||
cookieName: 'theme', |
|||
}, |
|||
}); |
@ -5,36 +5,50 @@ |
|||
"name": "wg-easy", |
|||
"version": "1.0.1", |
|||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.", |
|||
"main": "server.js", |
|||
"private": true, |
|||
"type": "module", |
|||
"scripts": { |
|||
"serve": "DEBUG=Server,WireGuard npx nodemon server.js", |
|||
"serve-with-password": "PASSWORD=wg npm run serve", |
|||
"build": "nuxt build", |
|||
"dev": "nuxt dev", |
|||
"generate": "nuxt generate", |
|||
"preview": "nuxt preview", |
|||
"postinstall": "nuxt prepare", |
|||
"lint": "eslint .", |
|||
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css" |
|||
"format": "prettier . --write", |
|||
"format:check": "prettier . --check", |
|||
"typecheck": "nuxt typecheck" |
|||
}, |
|||
"author": "Emile Nijssen", |
|||
"license": "CC BY-NC-SA 4.0", |
|||
"dependencies": { |
|||
"@eschricht/nuxt-color-mode": "^1.1.5", |
|||
"@nuxtjs/i18n": "^8.3.3", |
|||
"@nuxtjs/tailwindcss": "^6.12.1", |
|||
"@pinia/nuxt": "^0.5.3", |
|||
"@tailwindcss/forms": "^0.5.8", |
|||
"apexcharts": "^3.51.0", |
|||
"basic-auth": "^2.0.1", |
|||
"bcryptjs": "^2.4.3", |
|||
"crc-32": "^1.2.2", |
|||
"debug": "^4.3.7", |
|||
"express-session": "^1.18.1", |
|||
"h3": "^1.13.0", |
|||
"qrcode": "^1.5.4" |
|||
"js-sha256": "^0.11.0", |
|||
"nuxt": "^3.12.4", |
|||
"pinia": "^2.2.1", |
|||
"qrcode": "^1.5.4", |
|||
"tailwindcss": "^3.4.10", |
|||
"timeago.js": "^4.0.2", |
|||
"vue": "latest", |
|||
"vue3-apexcharts": "^1.5.3", |
|||
"zod": "^3.23.8" |
|||
}, |
|||
"devDependencies": { |
|||
"@tailwindcss/forms": "^0.5.9", |
|||
"eslint-config-athom": "^3.1.3", |
|||
"nodemon": "^3.1.7", |
|||
"tailwindcss": "^3.4.13" |
|||
"@nuxt/eslint-config": "^0.5.0", |
|||
"@types/bcryptjs": "^2.4.6", |
|||
"@types/debug": "^4.1.12", |
|||
"@types/qrcode": "^1.5.5", |
|||
"eslint": "^9.8.0", |
|||
"eslint-config-prettier": "^9.1.0", |
|||
"prettier": "^3.3.3", |
|||
"typescript": "^5.5.4", |
|||
"vue-tsc": "^2.0.29" |
|||
}, |
|||
"nodemonConfig": { |
|||
"ignore": [ |
|||
"www/*" |
|||
] |
|||
}, |
|||
"engines": { |
|||
"node": ">=18" |
|||
} |
|||
"packageManager": "[email protected]" |
|||
} |
|||
|
@ -0,0 +1,73 @@ |
|||
<template> |
|||
<main> |
|||
<div class="container mx-auto max-w-3xl px-3 md:px-0"> |
|||
<div |
|||
class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden" |
|||
> |
|||
<div |
|||
class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600" |
|||
> |
|||
<div class="flex-grow"> |
|||
<p class="text-2xl font-medium dark:text-neutral-200"> |
|||
{{ $t('clients') }} |
|||
</p> |
|||
</div> |
|||
<div class="flex md:block md:flex-shrink-0 space-x-1"> |
|||
<ClientsRestoreConfig /> |
|||
<ClientsBackupConfig /> |
|||
<ClientsSort /> |
|||
<ClientsNew /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div> |
|||
<Clients |
|||
v-if="clientsStore.clients && clientsStore.clients.length > 0" |
|||
/> |
|||
</div> |
|||
<ClientsEmpty |
|||
v-if="clientsStore.clients && clientsStore.clients.length === 0" |
|||
/> |
|||
<div |
|||
v-if="clientsStore.clients === null" |
|||
class="text-gray-200 dark:text-red-300 p-5" |
|||
> |
|||
<IconsLoading class="w-5 animate-spin mx-auto" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<ClientsQRCodeDialog /> |
|||
<ClientsCreateDialog /> |
|||
<ClientsDeleteDialog /> |
|||
</main> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const authStore = useAuthStore(); |
|||
authStore.update(); |
|||
const globalStore = useGlobalStore(); |
|||
const clientsStore = useClientsStore(); |
|||
|
|||
const intervalId = ref<NodeJS.Timeout | null>(null); |
|||
|
|||
clientsStore.refresh(); |
|||
|
|||
onMounted(() => { |
|||
// TODO?: replace with websocket or similar |
|||
intervalId.value = setInterval(() => { |
|||
clientsStore |
|||
.refresh({ |
|||
updateCharts: globalStore.updateCharts, |
|||
}) |
|||
.catch(console.error); |
|||
}, 1000); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
if (intervalId.value !== null) { |
|||
clearInterval(intervalId.value); |
|||
intervalId.value = null; |
|||
} |
|||
}); |
|||
</script> |
@ -0,0 +1,105 @@ |
|||
<template> |
|||
<section> |
|||
<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> |
|||
|
|||
<form |
|||
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10" |
|||
@submit="login" |
|||
> |
|||
<!-- Avatar --> |
|||
<div |
|||
class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 dark:bg-red-800 relative overflow-hidden" |
|||
> |
|||
<IconsAvatar class="w-10 h-10 m-5 text-white dark:text-white" /> |
|||
</div> |
|||
|
|||
<input |
|||
v-model="password" |
|||
type="password" |
|||
name="password" |
|||
:placeholder="$t('password')" |
|||
autocomplete="current-password" |
|||
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" |
|||
/> |
|||
|
|||
<label |
|||
v-if="globalStore.rememberMeEnabled" |
|||
class="inline-block mb-5 cursor-pointer whitespace-nowrap" |
|||
:title="$t('titleRememberMe')" |
|||
> |
|||
<input v-model="remember" type="checkbox" class="sr-only" /> |
|||
|
|||
<div |
|||
v-if="remember" |
|||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all" |
|||
> |
|||
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div> |
|||
</div> |
|||
|
|||
<div |
|||
v-if="!remember" |
|||
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" |
|||
> |
|||
<div class="rounded-full w-4 h-4 m-1 bg-white"></div> |
|||
</div> |
|||
|
|||
<span class="text-sm">{{ $t('rememberMe') }}</span> |
|||
</label> |
|||
|
|||
<button |
|||
v-if="authenticating" |
|||
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed" |
|||
> |
|||
<IconsLoading class="w-5 animate-spin mx-auto" /> |
|||
</button> |
|||
<input |
|||
v-else |
|||
type="submit" |
|||
:class="[ |
|||
{ |
|||
'bg-red-800 dark:bg-red-800 hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer': |
|||
password, |
|||
'bg-gray-200 dark:bg-neutral-800 cursor-not-allowed': !password, |
|||
}, |
|||
'w-full rounded shadow py-2 text-sm text-white dark:text-white', |
|||
]" |
|||
:value="$t('signIn')" |
|||
/> |
|||
</form> |
|||
</section> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const authenticating = ref(false); |
|||
const remember = ref(false); |
|||
const password = ref<null | string>(null); |
|||
const authStore = useAuthStore(); |
|||
const globalStore = useGlobalStore(); |
|||
|
|||
async function login(e: Event) { |
|||
e.preventDefault(); |
|||
|
|||
if (!password.value) return; |
|||
if (authenticating.value) return; |
|||
|
|||
authenticating.value = true; |
|||
try { |
|||
const res = await authStore.login(password.value, remember.value); |
|||
if (res) { |
|||
await navigateTo('/'); |
|||
} |
|||
} catch (err) { |
|||
if (err instanceof Error) { |
|||
// TODO: replace alert with actual ui error message |
|||
alert(err.message || err.toString()); |
|||
} |
|||
} |
|||
authenticating.value = false; |
|||
password.value = null; |
|||
} |
|||
</script> |
@ -0,0 +1,5 @@ |
|||
import VueApexCharts from 'vue3-apexcharts'; |
|||
|
|||
export default defineNuxtPlugin((nuxtApp) => { |
|||
nuxtApp.vueApp.use(VueApexCharts); |
|||
}); |
File diff suppressed because it is too large
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -1,29 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
require('./services/Server'); |
|||
|
|||
const WireGuard = require('./services/WireGuard'); |
|||
|
|||
WireGuard.getConfig() |
|||
.catch((err) => { |
|||
// eslint-disable-next-line no-console
|
|||
console.error(err); |
|||
|
|||
// eslint-disable-next-line no-process-exit
|
|||
process.exit(1); |
|||
}); |
|||
|
|||
// Handle terminate signal
|
|||
process.on('SIGTERM', async () => { |
|||
// eslint-disable-next-line no-console
|
|||
console.log('SIGTERM signal received.'); |
|||
await WireGuard.Shutdown(); |
|||
// eslint-disable-next-line no-process-exit
|
|||
process.exit(0); |
|||
}); |
|||
|
|||
// Handle interrupt signal
|
|||
process.on('SIGINT', () => { |
|||
// eslint-disable-next-line no-console
|
|||
console.log('SIGINT signal received.'); |
|||
}); |
@ -0,0 +1,24 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
if (WG_ENABLE_ONE_TIME_LINKS === 'false') { |
|||
throw createError({ |
|||
status: 404, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink'); |
|||
const clients = await WireGuard.getClients(); |
|||
const client = clients.find( |
|||
(client) => client.oneTimeLink === clientOneTimeLink |
|||
); |
|||
if (!client) return; |
|||
const clientId = client.id; |
|||
const config = await WireGuard.getClientConfiguration({ clientId }); |
|||
await WireGuard.eraseOneTimeLink({ clientId }); |
|||
setHeader( |
|||
event, |
|||
'Content-Disposition', |
|||
`attachment; filename="${clientOneTimeLink}.conf"` |
|||
); |
|||
setHeader(event, 'Content-Type', 'text/plain'); |
|||
return config; |
|||
}); |
@ -0,0 +1,4 @@ |
|||
export default defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return LANG; |
|||
}); |
@ -0,0 +1,8 @@ |
|||
export default defineEventHandler(async () => { |
|||
const release = Number.parseInt(RELEASE, 10); |
|||
const latestRelease = await fetchLatestRelease(); |
|||
return { |
|||
currentRelease: release, |
|||
latestRelease: latestRelease, |
|||
}; |
|||
}); |
@ -0,0 +1,4 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return MAX_AGE > 0; |
|||
}); |
@ -0,0 +1,16 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const session = await useWGSession(event); |
|||
const sessionId = session.id; |
|||
|
|||
if (sessionId === undefined) { |
|||
throw createError({ |
|||
statusCode: 401, |
|||
statusMessage: 'Not logged in', |
|||
}); |
|||
} |
|||
|
|||
await session.clear(); |
|||
|
|||
SERVER_DEBUG(`Deleted Session: ${sessionId}`); |
|||
return { success: true }; |
|||
}); |
@ -0,0 +1,11 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const session = await useWGSession(event); |
|||
const authenticated = REQUIRES_PASSWORD |
|||
? session.data.authenticated === true |
|||
: true; |
|||
|
|||
return { |
|||
requiresPassword: REQUIRES_PASSWORD, |
|||
authenticated, |
|||
}; |
|||
}); |
@ -0,0 +1,43 @@ |
|||
import type { SessionConfig } from 'h3'; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const { password, remember } = await readValidatedBody( |
|||
event, |
|||
validateZod(passwordType) |
|||
); |
|||
|
|||
if (!REQUIRES_PASSWORD) { |
|||
// if no password is required, the API should never be called.
|
|||
// Do not automatically authenticate the user.
|
|||
throw createError({ |
|||
statusCode: 401, |
|||
statusMessage: 'Invalid state', |
|||
}); |
|||
} |
|||
if (!isPasswordValid(password, PASSWORD_HASH)) { |
|||
throw createError({ |
|||
statusCode: 401, |
|||
statusMessage: 'Incorrect Password', |
|||
}); |
|||
} |
|||
|
|||
const conf: SessionConfig = SESSION_CONFIG; |
|||
if (MAX_AGE && remember) { |
|||
conf.cookie = { |
|||
...(SESSION_CONFIG.cookie ?? {}), |
|||
maxAge: MAX_AGE, |
|||
}; |
|||
} |
|||
|
|||
const session = await useSession(event, { |
|||
...SESSION_CONFIG, |
|||
}); |
|||
|
|||
const data = await session.update({ |
|||
authenticated: true, |
|||
}); |
|||
|
|||
SERVER_DEBUG(`New Session: ${data.id}`); |
|||
|
|||
return { success: true, requiresPassword: REQUIRES_PASSWORD }; |
|||
}); |
@ -0,0 +1,8 @@ |
|||
export default defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
const number = Number.parseInt(UI_CHART_TYPE, 10); |
|||
if (Number.isNaN(number)) { |
|||
return 0; |
|||
} |
|||
return number; |
|||
}); |
@ -0,0 +1,5 @@ |
|||
export default defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
const sort = UI_ENABLE_SORT_CLIENTS; |
|||
return sort === 'true' ? true : false; |
|||
}); |
@ -0,0 +1,6 @@ |
|||
export default defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
// Weird issue with auto import not working. This alias is needed
|
|||
const stats = UI_TRAFFIC_STATS; |
|||
return stats === 'true' ? true : false; |
|||
}); |
@ -0,0 +1,5 @@ |
|||
export default defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
const expires = WG_ENABLE_EXPIRES_TIME; |
|||
return expires === 'true' ? true : false; |
|||
}); |
@ -0,0 +1,5 @@ |
|||
export default defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
const otl = WG_ENABLE_ONE_TIME_LINKS; |
|||
return otl === 'true' ? true : false; |
|||
}); |
@ -0,0 +1,6 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const config = await WireGuard.backupConfiguration(); |
|||
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"'); |
|||
setHeader(event, 'Content-Type', 'text/json'); |
|||
return config; |
|||
}); |
@ -0,0 +1,9 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
const { address } = await readValidatedBody(event, validateZod(addressType)); |
|||
await WireGuard.updateClientAddress({ clientId, address }); |
|||
return { success: true }; |
|||
}); |
@ -0,0 +1,20 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
const client = await WireGuard.getClient({ clientId }); |
|||
const config = await WireGuard.getClientConfiguration({ clientId }); |
|||
const configName = client.name |
|||
.replace(/[^a-zA-Z0-9_=+.-]/g, '-') |
|||
.replace(/(-{2,}|-$)/g, '-') |
|||
.replace(/-$/, '') |
|||
.substring(0, 32); |
|||
setHeader( |
|||
event, |
|||
'Content-Disposition', |
|||
`attachment; filename="${configName || clientId}.conf"` |
|||
); |
|||
setHeader(event, 'Content-Type', 'text/plain'); |
|||
return config; |
|||
}); |
@ -0,0 +1,8 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
await WireGuard.disableClient({ clientId }); |
|||
return { success: true }; |
|||
}); |
@ -0,0 +1,8 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
await WireGuard.enableClient({ clientId }); |
|||
return { success: true }; |
|||
}); |
@ -0,0 +1,12 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
const { expireDate } = await readValidatedBody( |
|||
event, |
|||
validateZod(expireDateType) |
|||
); |
|||
await WireGuard.updateClientExpireDate({ clientId, expireDate }); |
|||
return { success: true }; |
|||
}); |
@ -0,0 +1,14 @@ |
|||
export default defineEventHandler(async (event) => { |
|||
if (WG_ENABLE_ONE_TIME_LINKS === 'false') { |
|||
throw createError({ |
|||
status: 404, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(clientIdType) |
|||
); |
|||
await WireGuard.generateOneTimeLink({ clientId }); |
|||
return { success: true }; |
|||
}); |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue