Compare commits

...

24 Commits

Author SHA1 Message Date
Bernd Storath 1c7f64ebd5 Bump version to 15.0.0-beta.11 2025-03-31 10:32:27 +02:00
Bernd Storath 589ec1fe9a Feat: Show insecure warning (#1779)
show insecure warning
2025-03-31 10:29:22 +02:00
Bernd Storath 6e0d758e36 Feat: Hash metrics password (#1778)
hash the metrics password

if it is not already hashed
2025-03-31 09:58:02 +02:00
Pokydko Oleksandr 940edb2b0c Improvements to username and password validations (#1745)
* Fix: Improve special character regex (#1744)
* update password special character regex to support
( `-` `_` `=` `+` `[` `]` `;` `'` `\` `/` )

* Fix: Allow usernames starting from 2 characters (#1744)
* update username validation to support short usernames starting from 2 characters

* remove char validation altogether

---------

Co-authored-by: Bernd Storath <999999bst@gmail.com>
2025-03-31 08:58:24 +02:00
Bernd Storath d51f12a82f update packages 2025-03-31 07:51:21 +02:00
Bernd Storath 4a3747fa12 update packages 2025-03-24 07:53:02 +01:00
Bernd Storath 499fb096b6 Fix: button triggering form (#1750)
prevent button from triggering form
2025-03-19 11:50:48 +01:00
Bernd Storath c5c3a65bbf Fix: slow suggest Host Address (#1746)
load on client instead of block
2025-03-17 14:28:03 +01:00
Bernd Storath c133446f9c Bump version to 15.0.0-beta.10 2025-03-14 14:28:12 +01:00
Bernd Storath e8b3e54228 fix libsql bundling issue 2025-03-14 14:26:08 +01:00
Bernd Storath a9a51337da Bump version to 15.0.0-beta.9 2025-03-14 12:22:43 +01:00
Bernd Storath bbee7e04ed Feat: add ability to restart interface (#1740)
add ability to restart interface
2025-03-14 12:19:26 +01:00
Bernd Storath 198b240755 Feat: Suggest IP or Hostname (#1739)
* get ip and hostnames

* use heroicons

* add host field

* get private info

* unstyled prototype

* styled select

* add to setup

* fix types
2025-03-14 10:33:02 +01:00
Bernd Storath 86bdbe4c3d Feat: Initial Setup through env vars (#1736)
* initial support for initial setup

* improve setup

* improve mobile view

* move base admin route

* admin panel mobile view

* set initial host and port

* add docs

* properly setup everything, use for dev env

* change userconfig and interface port on setup, note users afterwards
2025-03-13 11:28:05 +01:00
Bernd Storath 4890bb28e5 Bump version to 15.0.0-beta.8 2025-03-12 13:47:46 +01:00
Bernd Storath c3dbd3a815 Fix: Add ui port to template (#1735)
* add ui port to template

* update changelog
2025-03-12 13:44:45 +01:00
Bernd Storath fc480df910 Fix: Update ip outside of cidr (#1733)
* update packages

* check if ip is included on update

* update package manager
2025-03-12 12:47:12 +01:00
dependabot[bot] b3bd2502af build(deps): bump nuxt from 3.15.4 to 3.16.0 in /src (#1727)
Bumps [nuxt](https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt) from 3.15.4 to 3.16.0.
- [Release notes](https://github.com/nuxt/nuxt/releases)
- [Commits](https://github.com/nuxt/nuxt/commits/v3.16.0/packages/nuxt)

---
updated-dependencies:
- dependency-name: nuxt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 12:26:21 +01:00
dependabot[bot] eb5ad91022 build(deps-dev): bump eslint from 9.21.0 to 9.22.0 in /src (#1726)
Bumps [eslint](https://github.com/eslint/eslint) from 9.21.0 to 9.22.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.21.0...v9.22.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 08:11:31 +01:00
dependabot[bot] f2955a1278 build(deps): bump @nuxtjs/i18n from 9.2.1 to 9.3.1 in /src (#1728)
Bumps [@nuxtjs/i18n](https://github.com/nuxt-modules/i18n) from 9.2.1 to 9.3.1.
- [Release notes](https://github.com/nuxt-modules/i18n/releases)
- [Changelog](https://github.com/nuxt-modules/i18n/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nuxt-modules/i18n/compare/v9.2.1...v9.3.1)

---
updated-dependencies:
- dependency-name: "@nuxtjs/i18n"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 07:34:37 +01:00
dependabot[bot] 1b76c066e0 build(deps-dev): bump @nuxt/eslint from 1.1.0 to 1.2.0 in /src (#1729)
Bumps [@nuxt/eslint](https://github.com/nuxt/eslint/tree/HEAD/packages/module) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/nuxt/eslint/releases)
- [Commits](https://github.com/nuxt/eslint/commits/v1.2.0/packages/module)

---
updated-dependencies:
- dependency-name: "@nuxt/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 07:27:29 +01:00
Bernd Storath 5b68cc7311 Feat: Confirm setup password (#1722)
confirm setup password
2025-03-07 16:05:40 +01:00
Bernd Storath 0597470f4c Bump version to 15.0.0-beta.7 2025-03-07 15:00:12 +01:00
Bernd Storath 159a51cff4 Feat: Global config override (#1720)
* be able to change dns. implement global override

* link donate to readme

* implement global config for allowed ips

* change translations, fix generation

* improve docs
2025-03-07 14:59:06 +01:00
91 changed files with 2794 additions and 2754 deletions
+3
View File
@@ -6,6 +6,9 @@
"nuxtr.vueFiles.style.addStyleTag": false, "nuxtr.vueFiles.style.addStyleTag": false,
"nuxtr.piniaFiles.defaultTemplate": "setup", "nuxtr.piniaFiles.defaultTemplate": "setup",
"nuxtr.monorepoMode.DirectoryName": "src", "nuxtr.monorepoMode.DirectoryName": "src",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
+2
View File
@@ -24,6 +24,8 @@ This update is an entire rewrite to make it even easier to set up your own VPN.
- Deprecated Dockerless Installations - Deprecated Dockerless Installations
- Added Docker Volume Mount (`/lib/modules`) - Added Docker Volume Mount (`/lib/modules`)
- Removed ARMv6 and ARMv7 support - Removed ARMv6 and ARMv7 support
- Connections over HTTP require setting the `INSECURE` env var
- Changed license from CC BY-NC-SA 4.0 to AGPL-3.0-only
## [14.0.0] - 2024-09-04 ## [14.0.0] - 2024-09-04
+2 -1
View File
@@ -26,7 +26,7 @@ COPY --from=build /app/.output /app
# Copy migrations # Copy migrations
COPY --from=build /app/server/database/migrations /app/server/database/migrations COPY --from=build /app/server/database/migrations /app/server/database/migrations
# libsql # libsql
RUN npm install --no-save libsql RUN cd /app/server && npm install --no-save libsql
# Install Linux packages # Install Linux packages
RUN apk add --no-cache \ RUN apk add --no-cache \
@@ -47,6 +47,7 @@ ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821 ENV PORT=51821
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV INSECURE=false ENV INSECURE=false
ENV INIT_ENABLED=false
LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy
+2 -1
View File
@@ -26,7 +26,8 @@ RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tab
ENV DEBUG=Server,WireGuard,Database,CMD ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821 ENV PORT=51821
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV INSECURE=false ENV INSECURE=true
ENV INIT_ENABLED=false
# Install Dependencies # Install Dependencies
COPY src/package.json src/pnpm-lock.yaml ./ COPY src/package.json src/pnpm-lock.yaml ./
+1 -1
View File
@@ -102,7 +102,7 @@ Now setup a reverse proxy to be able to access the Web UI from the internet.
If you want to access the Web UI over HTTP, change the env var `INSECURE` to `true`. This is not recommended. Only use this for testing If you want to access the Web UI over HTTP, change the env var `INSECURE` to `true`. This is not recommended. Only use this for testing
### 3. Sponsor ### Donate
Are you enjoying this project? Consider donating. Are you enjoying this project? Consider donating.
+6
View File
@@ -15,6 +15,12 @@ services:
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- SYS_MODULE - SYS_MODULE
environment:
- INIT_ENABLED=true
- INIT_HOST=test
- INIT_PORT=51820
- INIT_USERNAME=testtest
- INIT_PASSWORD=Qweasdyxcv!2
# folders should be generated inside container # folders should be generated inside container
volumes: volumes:
@@ -0,0 +1,32 @@
---
title: Unattended Setup
---
If you want to run the setup without any user interaction, e.g. with a tool like Ansible, you can use these environment variables to configure the setup.
These will only be used during the first start of the container. After that, the setup will be disabled.
| Env | Example | Description | Group |
| ---------------- | ----------------- | --------------------------------------------------------- | ----- |
| `INIT_ENABLED` | `true` | Enables the below env vars | 0 |
| `INIT_USERNAME` | `admin` | Sets admin username | 1 |
| `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 |
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 |
| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 |
| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 |
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 |
| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 |
/// warning | Variables have to be used together
If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`.
If you want to skip the setup process, you have to configure group `1`
///
/// note | Security
The initial username and password is not checked for complexity. Make sure to set a long enough username and a secure password. Otherwise, the user won't be able to log in.
Its recommended to remove the variables after the setup is done to prevent the password from being exposed.
///
@@ -4,13 +4,13 @@ title: Auto Updates
## Docker Compose ## Docker Compose
With Docker Compose WireGuard Easy can be updated with a single command: With Docker Compose `wg-easy` can be updated with a single command:
Replace `$DIR` with the directory where your `docker-compose.yml` is located. Replace `$DIR` with the directory where your `docker-compose.yml` is located.
```shell ```shell
cd $DIR cd $DIR
sudo docker compose -f up -d --pull always sudo docker compose up -d --pull always
``` ```
## Docker Run ## Docker Run
@@ -27,7 +27,7 @@ And then run the `docker run -d \ ...` command from [Docker Run][docker-run] aga
## Podman ## Podman
To update `wg-easy` (and every container that has auto updates enabled), you can run the following commands: To update `wg-easy` (and every container that has auto updates enabled), you can run the following command:
```shell ```shell
sudo podman auto-update sudo podman auto-update
@@ -4,7 +4,7 @@ title: Docker Run
To setup the IPv6 Network, simply run once: To setup the IPv6 Network, simply run once:
```bash ```shell
docker network create \ docker network create \
-d bridge --ipv6 \ -d bridge --ipv6 \
-d default \ -d default \
@@ -14,9 +14,9 @@ To setup the IPv6 Network, simply run once:
<!-- ref: major version --> <!-- ref: major version -->
To automatically install & run wg-easy, simply run: To automatically install & run ``wg-easy, simply run:
```bash ```shell
docker run -d \ docker run -d \
--net wg \ --net wg \
-e INSECURE=true \ -e INSECURE=true \
+1 -1
View File
@@ -88,7 +88,7 @@ In the Admin Panel of your WireGuard server, go to the `Hooks` tab and add the f
1. PostUp 1. PostUp
```shell ```shell
apk add nftables; nft add table inet wg_table; nft add chain inet wg_table postrouting { type nat hook postrouting priority 100 \; }; nft add rule inet wg_table postrouting ip saddr {{ipv4Cidr}} oifname {{device}} masquerade; nft add rule inet wg_table postrouting ip6 saddr {{ipv6Cidr}} oifname {{device}} masquerade; nft add chain inet wg_table input { type filter hook input priority 0 \; policy drop \; }; nft add rule inet wg_table input udp dport {{port}} accept; nft add chain inet wg_table forward { type filter hook forward priority 0 \; policy drop \; }; nft add rule inet wg_table forward iifname "wg0" accept; nft add rule inet wg_table forward oifname "wg0" accept; apk add nftables; nft add table inet wg_table; nft add chain inet wg_table postrouting { type nat hook postrouting priority 100 \; }; nft add rule inet wg_table postrouting ip saddr {{ipv4Cidr}} oifname {{device}} masquerade; nft add rule inet wg_table postrouting ip6 saddr {{ipv6Cidr}} oifname {{device}} masquerade; nft add chain inet wg_table input { type filter hook input priority 0 \; policy drop \; }; nft add rule inet wg_table input udp dport {{port}} accept; nft add rule inet wg_table input tcp dport {{uiPort}} accept; nft add chain inet wg_table forward { type filter hook forward priority 0 \; policy drop \; }; nft add rule inet wg_table forward iifname "wg0" accept; nft add rule inet wg_table forward oifname "wg0" accept;
``` ```
2. PostDown 2. PostDown
+1 -1
View File
@@ -7,5 +7,5 @@
"docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs squidfunk/mkdocs-material serve -a 0.0.0.0:8080", "docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs squidfunk/mkdocs-material serve -a 0.0.0.0:8080",
"scripts:version": "bash scripts/version.sh" "scripts:version": "bash scripts/version.sh"
}, },
"packageManager": "pnpm@10.5.2" "packageManager": "pnpm@10.7.0"
} }
@@ -0,0 +1,24 @@
<template>
<BaseDialog :trigger-class="triggerClass">
<template #trigger><slot /></template>
<template #title>{{ $t('admin.interface.restart') }}</template>
<template #description>
{{ $t('admin.interface.restartWarn') }}
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('restart')">
{{ $t('admin.interface.restart') }}
</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>
defineEmits(['restart']);
defineProps<{ triggerClass?: string }>();
</script>
@@ -0,0 +1,39 @@
<template>
<BaseDialog :trigger-class="triggerClass">
<template #trigger><slot /></template>
<template #title>{{ $t('admin.config.suggest') }}</template>
<template #description>
<div class="flex flex-col items-start gap-2">
<p>{{ $t('admin.config.suggestDesc') }}</p>
<p v-if="!data">
{{ $t('general.loading') }}
</p>
<BaseSelect v-else v-model="selected" :options="data" />
</div>
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('change', selected)">
{{ $t('dialog.change') }}
</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>
defineEmits(['change']);
const props = defineProps<{
triggerClass?: string;
url: '/api/admin/ip-info' | '/api/setup/4';
}>();
const { data } = useFetch(props.url, {
method: 'get',
});
const selected = ref<string>();
</script>
+37
View File
@@ -0,0 +1,37 @@
<template>
<SelectRoot v-model="selected">
<SelectTrigger
class="inline-flex h-8 items-center justify-around gap-2 rounded bg-gray-200 px-3 text-sm leading-none dark:bg-neutral-500 dark:text-neutral-200"
aria-label="Choose option"
>
<SelectValue placeholder="Select..." />
<IconsArrowDown class="size-3" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="z-[100] min-w-28 rounded bg-gray-300 dark:bg-neutral-500"
>
<SelectViewport class="p-2">
<SelectItem
v-for="(option, index) in options"
:key="index"
:value="option.value"
class="relative flex h-6 items-center rounded px-3 text-sm leading-none outline-none hover:bg-red-800 hover:text-white dark:text-white"
>
<SelectItemText>
{{ option.value }} - {{ option.label }}
</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
<script lang="ts" setup>
defineProps<{
options: { label: string; value: string }[];
}>();
const selected = defineModel<string>();
</script>
+9 -4
View File
@@ -1,14 +1,17 @@
<template> <template>
<TooltipProvider> <TooltipProvider>
<TooltipRoot> <TooltipRoot :open="open" @update:open="open = $event">
<TooltipTrigger <TooltipTrigger
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-gray-400 outline-none focus:shadow-sm focus:shadow-black" class="mx-2 inline-flex h-4 w-4 items-center justify-center rounded-full text-gray-400 outline-none focus:shadow-sm focus:shadow-black"
as-child
> >
<slot /> <button type="button" @click="open = !open">
<slot />
</button>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent <TooltipContent
class="select-none rounded bg-gray-600 px-3 py-2 text-sm leading-none text-white shadow-lg will-change-[transform,opacity]" class="select-none whitespace-pre-line rounded bg-gray-600 px-3 py-2 text-sm leading-none text-white shadow-lg will-change-[transform,opacity]"
:side-offset="5" :side-offset="5"
> >
{{ text }} {{ text }}
@@ -21,4 +24,6 @@
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ text: string }>(); defineProps<{ text: string }>();
const open = ref(false);
</script> </script>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<section class="grid grid-cols-1 gap-4 md:grid-cols-2"> <section class="grid grid-cols-2 gap-4">
<slot /> <slot />
<Separator <Separator
decorative decorative
+50
View File
@@ -0,0 +1,50 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<div class="flex">
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="text"
class="w-full"
:placeholder="placeholder"
/>
<ClientOnly>
<AdminSuggestDialog :url="url" @change="data = $event">
<BaseButton as="span">
<div class="flex items-center gap-3">
<IconsSparkles class="w-4" />
<span>{{ $t('admin.config.suggest') }}</span>
</div>
</BaseButton>
</AdminSuggestDialog>
</ClientOnly>
</div>
</template>
<script lang="ts" setup>
defineProps<{
id: string;
label: string;
description?: string;
placeholder?: string;
url: '/api/admin/ip-info' | '/api/setup/4';
}>();
const data = defineModel<string | null>({
set(value) {
const temp = value?.trim() ?? null;
if (temp === '') {
return null;
}
return temp;
},
});
</script>
@@ -0,0 +1,68 @@
<template>
<div class="flex flex-col gap-2">
<div v-if="data === null">
{{ emptyText || $t('form.nullNoItems') }}
</div>
<div v-for="(item, i) in data" v-else :key="i">
<div class="mt-1 flex flex-row gap-1">
<input
:value="item"
:name="name"
type="text"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
@input="update($event, i)"
/>
<BaseButton
as="input"
type="button"
class="rounded-lg"
value="-"
@click="del(i)"
/>
</div>
</div>
<div class="mt-2">
<BaseButton
as="input"
type="button"
class="rounded-lg"
:value="$t('form.add')"
@click="add"
/>
</div>
</div>
</template>
<script lang="ts" setup>
const data = defineModel<string[] | null>();
defineProps<{ emptyText?: string[]; name: string }>();
function update(e: Event, i: number) {
const v = (e.target as HTMLInputElement).value;
if (!data.value) {
return;
}
data.value[i] = v;
}
function add() {
if (data.value === undefined) {
return;
}
if (data.value === null) {
data.value = [''];
} else {
data.value.push('');
}
}
function del(i: number) {
if (!data.value) {
return;
}
data.value.splice(i, 1);
if (data.value.length === 0) {
data.value = null;
}
}
</script>
+22
View File
@@ -0,0 +1,22 @@
<template>
<div
v-if="!globalStore.information?.insecure && !https"
class="container mx-auto w-fit rounded-md bg-red-800 p-4 text-white shadow-lg dark:bg-red-100 dark:text-red-600"
>
<p class="text-center">{{ $t('login.insecure') }}</p>
</div>
</template>
<script lang="ts" setup>
const globalStore = useGlobalStore();
const https = ref(false);
onMounted(() => {
if (window.location.protocol === 'https:') {
https.value = true;
} else {
https.value = false;
}
});
</script>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<NuxtLink to="/" class="mb-4 flex-grow self-start"> <NuxtLink to="/" class="mb-4">
<h1 class="text-4xl font-medium dark:text-neutral-200"> <h1 class="text-4xl font-medium dark:text-neutral-200">
<img <img
src="/logo.png" src="/logo.png"
+9 -6
View File
@@ -1,17 +1,21 @@
<template> <template>
<div <div
v-if="globalStore.release?.updateAvailable" v-if="
globalStore.information?.updateAvailable &&
authStore.userData &&
hasPermissions(authStore.userData, 'admin', 'any')
"
class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600" class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600"
:title="`v${globalStore.release.currentRelease} → v${globalStore.release.latestRelease.version}`" :title="`v${globalStore.information.currentRelease} → v${globalStore.information.latestRelease.version}`"
> >
<div class="container mx-auto flex flex-auto flex-row items-center"> <div class="container mx-auto flex flex-auto flex-row items-center">
<div class="flex-grow"> <div class="flex-grow">
<p class="font-bold">{{ $t('update.updateAvailable') }}</p> <p class="font-bold">{{ $t('update.updateAvailable') }}</p>
<p>{{ globalStore.release.latestRelease.changelog }}</p> <p>{{ globalStore.information.latestRelease.changelog }}</p>
</div> </div>
<a <a
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.release.latestRelease.version}`" :href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.information.latestRelease.version}`"
target="_blank" target="_blank"
class="font-sm float-right flex-shrink-0 rounded-md border-2 border-red-800 bg-white p-3 font-semibold text-red-800 transition-all hover:border-white hover:bg-red-800 hover:text-white dark:border-red-600 dark:bg-red-100 dark:text-red-600 dark:hover:border-red-600 dark:hover:bg-red-600 dark:hover:text-red-100" class="font-sm float-right flex-shrink-0 rounded-md border-2 border-red-800 bg-white p-3 font-semibold text-red-800 transition-all hover:border-white hover:bg-red-800 hover:text-white dark:border-red-600 dark:bg-red-100 dark:text-red-600 dark:hover:border-red-600 dark:hover:bg-red-600 dark:hover:text-red-100"
> >
@@ -23,6 +27,5 @@
<script lang="ts" setup> <script lang="ts" setup>
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const authStore = useAuthStore();
// TODO: only show this to admins
</script> </script>
+5 -11
View File
@@ -1,13 +1,7 @@
<template> <template>
<svg <ArrowDownIcon />
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> </template>
<script lang="ts" setup>
import ArrowDownIcon from '@heroicons/vue/24/outline/esm/ArrowDownIcon';
</script>
+5 -14
View File
@@ -1,16 +1,7 @@
<template> <template>
<svg <ArrowPathIcon />
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> </template>
<script lang="ts" setup>
import ArrowPathIcon from '@heroicons/vue/24/outline/esm/ArrowPathIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <ArrowLeftCircleIcon />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 9-3 3m0 0 3 3m-3-3h7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</template> </template>
<script lang="ts" setup>
import ArrowLeftCircleIcon from '@heroicons/vue/24/outline/esm/ArrowLeftCircleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <ArrowRightCircleIcon />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</template> </template>
<script lang="ts" setup>
import ArrowRightCircleIcon from '@heroicons/vue/24/outline/esm/ArrowLeftCircleIcon';
</script>
+5 -11
View File
@@ -1,13 +1,7 @@
<template> <template>
<svg <ArrowUpIcon />
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> </template>
<script lang="ts" setup>
import ArrowUpIcon from '@heroicons/vue/24/outline/esm/ArrowUpIcon';
</script>
+5 -10
View File
@@ -1,12 +1,7 @@
<template> <template>
<svg <ChartBarIcon />
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> </template>
<script lang="ts" setup>
import ChartBarIcon from '@heroicons/vue/24/outline/esm/ChartBarIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <CheckCircleIcon />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</template> </template>
<script lang="ts" setup>
import CheckCircleIcon from '@heroicons/vue/24/outline/esm/CheckCircleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <XMarkIcon />
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> </template>
<script lang="ts" setup>
import XMarkIcon from '@heroicons/vue/24/outline/esm/XMarkIcon';
</script>
+5 -11
View File
@@ -1,13 +1,7 @@
<template> <template>
<svg <TrashIcon />
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> </template>
<script lang="ts" setup>
import TrashIcon from '@heroicons/vue/24/outline/esm/TrashIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <ArrowDownTrayIcon />
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> </template>
<script lang="ts" setup>
import ArrowDownTrayIcon from '@heroicons/vue/24/outline/esm/ArrowDownTrayIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <PencilSquareIcon />
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> </template>
<script lang="ts" setup>
import PencilSquareIcon from '@heroicons/vue/24/outline/esm/PencilSquareIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <InformationCircleIcon />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</template> </template>
<script lang="ts" setup>
import InformationCircleIcon from '@heroicons/vue/24/outline/esm/InformationCircleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <LanguageIcon />
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="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"
/>
</svg>
</template> </template>
<script lang="ts" setup>
import LanguageIcon from '@heroicons/vue/24/outline/esm/LanguageIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <LinkIcon />
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>
</template> </template>
<script lang="ts" setup>
import LinkIcon from '@heroicons/vue/24/outline/esm/LinkIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <ArrowRightStartOnRectangleIcon />
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> </template>
<script lang="ts" setup>
import ArrowRightStartOnRectangleIcon from '@heroicons/vue/24/outline/esm/ArrowRightStartOnRectangleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <MoonIcon />
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> </template>
<script lang="ts" setup>
import MoonIcon from '@heroicons/vue/24/outline/esm/MoonIcon';
</script>
+5 -14
View File
@@ -1,16 +1,7 @@
<template> <template>
<svg <PlusIcon />
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> </template>
<script lang="ts" setup>
import PlusIcon from '@heroicons/vue/24/outline/esm/PlusIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <QrCodeIcon />
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> </template>
<script lang="ts" setup>
import QrCodeIcon from '@heroicons/vue/24/outline/esm/QrCodeIcon';
</script>
+7
View File
@@ -0,0 +1,7 @@
<template>
<SparklesIcon />
</template>
<script lang="ts" setup>
import SparklesIcon from '@heroicons/vue/24/outline/esm/SparklesIcon';
</script>
+5 -14
View File
@@ -1,16 +1,7 @@
<template> <template>
<svg <ServerStackIcon />
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> </template>
<script lang="ts" setup>
import ServerStackIcon from '@heroicons/vue/24/outline/esm/ServerStackIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template> <template>
<svg <SunIcon />
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> </template>
<script lang="ts" setup>
import SunIcon from '@heroicons/vue/24/outline/esm/SunIcon';
</script>
+5 -15
View File
@@ -1,17 +1,7 @@
<template> <template>
<!-- Heroicon name: outline/exclamation --> <ExclamationTriangleIcon />
<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> </template>
<script lang="ts" setup>
import ExclamationTriangleIcon from '@heroicons/vue/24/outline/esm/ExclamationTriangleIcon';
</script>
+2 -2
View File
@@ -7,7 +7,7 @@
href="https://github.com/wg-easy/wg-easy" href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a >WireGuard Easy</a
> >
({{ globalStore.release?.currentRelease }}) © 2021-2025 by ({{ globalStore.information?.currentRelease }}) © 2021-2025 by
<a <a
class="hover:underline" class="hover:underline"
target="_blank" target="_blank"
@@ -24,7 +24,7 @@
· ·
<a <a
class="hover:underline" class="hover:underline"
href="https://github.com/sponsors/WeeJeWel" href="https://github.com/wg-easy/wg-easy#donate"
target="_blank" target="_blank"
>{{ $t('layout.donate') }}</a >{{ $t('layout.donate') }}</a
> >
+5 -5
View File
@@ -1,23 +1,23 @@
<template> <template>
<div> <div>
<header class="container mx-auto mt-4 max-w-3xl px-3 xs:mt-6 md:px-0"> <header class="mx-auto mt-4 flex max-w-3xl flex-col justify-center">
<div <div
class="mb-5" class="mb-5 w-full"
:class=" :class="
loggedIn loggedIn
? 'flex flex-auto flex-col-reverse items-center gap-3 xxs:flex-row' ? 'flex flex-col items-center justify-between sm:flex-row'
: 'flex justify-end' : 'flex justify-end'
" "
> >
<HeaderLogo v-if="loggedIn" /> <HeaderLogo v-if="loggedIn" />
<div class="flex grow-0 items-center gap-3 self-end xxs:self-center"> <div class="flex flex-row gap-3">
<HeaderLangSelector /> <HeaderLangSelector />
<HeaderThemeSwitch /> <HeaderThemeSwitch />
<HeaderChartToggle v-if="loggedIn" /> <HeaderChartToggle v-if="loggedIn" />
<UiUserMenu v-if="loggedIn" /> <UiUserMenu v-if="loggedIn" />
</div> </div>
</div> </div>
<HeaderUpdate class="mt-5" /> <HeaderUpdate class="mt-4" />
</header> </header>
<slot /> <slot />
<UiFooter /> <UiFooter />
+2 -2
View File
@@ -11,8 +11,8 @@
</header> </header>
<main> <main>
<Panel> <Panel>
<PanelBody class="mx-auto mt-10 p-4 md:w-[70%] lg:w-[60%]"> <PanelBody class="m-4 mx-auto mt-10 md:w-[70%] lg:w-[60%]">
<h2 class="mb-16 mt-8 text-3xl font-medium"> <h2 class="mb-16 mt-8 text-center text-3xl font-medium">
{{ $t('setup.welcome') }} {{ $t('setup.welcome') }}
</h2> </h2>
+10 -5
View File
@@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<div class="flex"> <div class="flex flex-col gap-4 lg:flex-row">
<div class="mr-4 w-64 rounded-lg bg-white p-4 dark:bg-neutral-700"> <div class="rounded-lg bg-white p-4 lg:w-64 dark:bg-neutral-700">
<NuxtLink to="/admin"> <NuxtLink to="/admin">
<h2 class="mb-4 text-xl font-bold dark:text-neutral-200"> <h2 class="mb-4 text-xl font-bold dark:text-neutral-200">
{{ t('pages.admin.panel') }} {{ t('pages.admin.panel') }}
@@ -13,6 +13,7 @@
v-for="(item, index) in menuItems" v-for="(item, index) in menuItems"
:key="index" :key="index"
:to="`/admin/${item.id}`" :to="`/admin/${item.id}`"
active-class="bg-red-800 rounded"
> >
<BaseButton <BaseButton
as="span" as="span"
@@ -27,7 +28,7 @@
<div <div
class="flex-1 rounded-lg bg-white p-6 dark:bg-neutral-700 dark:text-neutral-200" class="flex-1 rounded-lg bg-white p-6 dark:bg-neutral-700 dark:text-neutral-200"
> >
<h1 class="mb-6 text-3xl font-bold">{{ activeMenuItem?.name }}</h1> <h1 class="mb-6 text-3xl font-bold">{{ activeMenuItem.name }}</h1>
<NuxtPage /> <NuxtPage />
</div> </div>
</div> </div>
@@ -44,13 +45,17 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const menuItems = [ const menuItems = [
{ id: '', name: t('pages.admin.general') }, { id: 'general', name: t('pages.admin.general') },
{ id: 'config', name: t('pages.admin.config') }, { id: 'config', name: t('pages.admin.config') },
{ id: 'interface', name: t('pages.admin.interface') }, { id: 'interface', name: t('pages.admin.interface') },
{ id: 'hooks', name: t('pages.admin.hooks') }, { id: 'hooks', name: t('pages.admin.hooks') },
]; ];
const defaultItem = { id: '', name: t('pages.admin.panel') };
const activeMenuItem = computed(() => { const activeMenuItem = computed(() => {
return menuItems.find((item) => route.path === `/admin/${item.id}`); return (
menuItems.find((item) => route.path === `/admin/${item.id}`) ?? defaultItem
);
}); });
</script> </script>
+8 -7
View File
@@ -3,11 +3,12 @@
<FormElement @submit.prevent="submit"> <FormElement @submit.prevent="submit">
<FormGroup> <FormGroup>
<FormHeading>{{ $t('admin.config.connection') }}</FormHeading> <FormHeading>{{ $t('admin.config.connection') }}</FormHeading>
<FormTextField <FormHostField
id="host" id="host"
v-model="data.host" v-model="data.host"
:label="$t('general.host')" :label="$t('general.host')"
:description="$t('admin.config.hostDesc')" :description="$t('admin.config.hostDesc')"
url="/api/admin/ip-info"
/> />
<FormNumberField <FormNumberField
id="port" id="port"
@@ -17,18 +18,18 @@
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('admin.config.allowedIpsDesc')">{{ <FormHeading :description="$t('admin.config.allowedIpsDesc')">
$t('general.allowedIps') {{ $t('general.allowedIps') }}
}}</FormHeading> </FormHeading>
<FormArrayField <FormArrayField
v-model="data.defaultAllowedIps" v-model="data.defaultAllowedIps"
name="defaultAllowedIps" name="defaultAllowedIps"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('admin.config.dnsDesc')">{{ <FormHeading :description="$t('admin.config.dnsDesc')">
$t('admin.config.dns') {{ $t('general.dns') }}
}}</FormHeading> </FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" /> <FormArrayField v-model="data.defaultDns" name="defaultDns" />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
+64
View File
@@ -0,0 +1,64 @@
<template>
<main v-if="data">
<FormElement @submit.prevent="submit">
<FormGroup>
<FormNumberField
id="session"
v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.general.metrics') }}</FormHeading>
<FormNullTextField
id="password"
v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
/>
<FormSwitchField
id="prometheus"
v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
/>
<FormSwitchField
id="json"
v-model="data.metricsJson"
:label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" />
<FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup>
</FormElement>
</main>
</template>
<script setup lang="ts">
const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get',
});
const data = toRef(_data.value);
const _submit = useSubmit(
`/api/admin/general`,
{
method: 'post',
},
{ revert }
);
function submit() {
return _submit(data.value);
}
async function revert() {
await refresh();
data.value = toRef(_data.value).value;
}
</script>
+2 -61
View File
@@ -1,64 +1,5 @@
<template> <template>
<main v-if="data"> <main class="flex flex-col gap-3">
<FormElement @submit.prevent="submit"> <p class="whitespace-pre-line">{{ $t('admin.introText') }}</p>
<FormGroup>
<FormNumberField
id="session"
v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.general.metrics') }}</FormHeading>
<FormNullTextField
id="password"
v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
/>
<FormSwitchField
id="prometheus"
v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
/>
<FormSwitchField
id="json"
v-model="data.metricsJson"
:label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" />
<FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup>
</FormElement>
</main> </main>
</template> </template>
<script setup lang="ts">
const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get',
});
const data = toRef(_data.value);
const _submit = useSubmit(
`/api/admin/general`,
{
method: 'post',
},
{ revert }
);
function submit() {
return _submit(data.value);
}
async function revert() {
await refresh();
data.value = toRef(_data.value).value;
}
</script>
+27
View File
@@ -34,8 +34,19 @@
<FormActionField <FormActionField
:label="$t('admin.interface.changeCidr')" :label="$t('admin.interface.changeCidr')"
class="w-full" class="w-full"
tabindex="-1"
/> />
</AdminCidrDialog> </AdminCidrDialog>
<AdminRestartInterfaceDialog
trigger-class="col-span-2"
@restart="restartInterface"
>
<FormActionField
:label="$t('admin.interface.restart')"
class="w-full"
tabindex="-1"
/>
</AdminRestartInterfaceDialog>
</FormGroup> </FormGroup>
</FormElement> </FormElement>
</main> </main>
@@ -82,4 +93,20 @@ const _changeCidr = useSubmit(
async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) { async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) {
await _changeCidr({ ipv4Cidr, ipv6Cidr }); await _changeCidr({ ipv4Cidr, ipv6Cidr });
} }
const _restartInterface = useSubmit(
`/api/admin/interface/restart`,
{
method: 'post',
},
{
revert,
successMsg: t('admin.interface.restartSuccess'),
errorMsg: t('admin.interface.restartError'),
}
);
async function restartInterface() {
await _restartInterface(undefined);
}
</script> </script>
+19 -10
View File
@@ -41,21 +41,26 @@
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('client.allowedIpsDesc')">{{ <FormHeading :description="$t('client.allowedIpsDesc')">
$t('general.allowedIps') {{ $t('general.allowedIps') }}
}}</FormHeading> </FormHeading>
<FormArrayField v-model="data.allowedIps" name="allowedIps" /> <FormNullArrayField v-model="data.allowedIps" name="allowedIps" />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('client.serverAllowedIpsDesc')">{{ <FormHeading :description="$t('client.serverAllowedIpsDesc')">
$t('client.serverAllowedIps') {{ $t('client.serverAllowedIps') }}
}}</FormHeading> </FormHeading>
<FormArrayField <FormArrayField
v-model="data.serverAllowedIps" v-model="data.serverAllowedIps"
name="serverAllowedIps" name="serverAllowedIps"
/> />
</FormGroup> </FormGroup>
<FormGroup></FormGroup> <FormGroup>
<FormHeading :description="$t('client.dnsDesc')">
{{ $t('general.dns') }}
</FormHeading>
<FormNullArrayField v-model="data.dns" name="dns" />
</FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading> <FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading>
<FormNumberField <FormNumberField
@@ -142,8 +147,12 @@ const _submit = useSubmit(
method: 'post', method: 'post',
}, },
{ {
revert: async () => { revert: async (success) => {
await navigateTo('/'); if (success) {
await navigateTo('/');
} else {
await revert();
}
}, },
} }
); );
+4
View File
@@ -1,6 +1,7 @@
<template> <template>
<main> <main>
<UiBanner /> <UiBanner />
<HeaderInsecure />
<form <form
class="mx-auto mt-10 flex w-64 flex-col gap-5 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200" class="mx-auto mt-10 flex w-64 flex-col gap-5 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200"
@submit.prevent="submit" @submit.prevent="submit"
@@ -54,6 +55,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const authStore = useAuthStore();
authStore.update();
const authenticating = ref(false); const authenticating = ref(false);
const remember = ref(false); const remember = ref(false);
const username = ref<null | string>(null); const username = ref<null | string>(null);
+1 -1
View File
@@ -40,7 +40,7 @@
id="confirm-password" id="confirm-password"
v-model="confirmPassword" v-model="confirmPassword"
autocomplete="new-password" autocomplete="new-password"
:label="$t('me.confirmPassword')" :label="$t('general.confirmPassword')"
/> />
<FormActionField <FormActionField
type="submit" type="submit"
+3 -3
View File
@@ -1,9 +1,9 @@
<template> <template>
<div> <div class="flex flex-col items-center">
<p class="px-8 pt-8 text-center text-2xl"> <p class="px-8 text-center text-2xl">
{{ $t('setup.welcomeDesc') }} {{ $t('setup.welcomeDesc') }}
</p> </p>
<NuxtLink to="/setup/2"> <NuxtLink to="/setup/2" class="mt-8">
<BaseButton as="span">{{ $t('general.continue') }}</BaseButton> <BaseButton as="span">{{ $t('general.continue') }}</BaseButton>
</NuxtLink> </NuxtLink>
</div> </div>
+17 -4
View File
@@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.createAdminDesc') }} {{ $t('setup.createAdminDesc') }}
</p> </p>
<div class="flex flex-col gap-3"> <div class="mt-8 flex flex-col gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
<FormNullTextField <FormNullTextField
id="username" id="username"
@@ -20,7 +20,15 @@
:label="$t('general.password')" :label="$t('general.password')"
/> />
</div> </div>
<div> <div class="flex flex-col">
<FormPasswordField
id="confirmPassword"
v-model="confirmPassword"
autocomplete="new-password"
:label="$t('general.confirmPassword')"
/>
</div>
<div class="mt-4 flex justify-center">
<BaseButton @click="submit">{{ $t('setup.createAccount') }}</BaseButton> <BaseButton @click="submit">{{ $t('setup.createAccount') }}</BaseButton>
</div> </div>
</div> </div>
@@ -37,6 +45,7 @@ setupStore.setStep(2);
const username = ref<null | string>(null); const username = ref<null | string>(null);
const password = ref<string>(''); const password = ref<string>('');
const confirmPassword = ref<string>('');
const _submit = useSubmit( const _submit = useSubmit(
'/api/setup/2', '/api/setup/2',
@@ -54,6 +63,10 @@ const _submit = useSubmit(
); );
function submit() { function submit() {
return _submit({ username: username.value, password: password.value }); return _submit({
username: username.value,
password: password.value,
confirmPassword: confirmPassword.value,
});
} }
</script> </script>
+10 -6
View File
@@ -1,14 +1,18 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.existingSetup') }} {{ $t('setup.existingSetup') }}
</p> </p>
<div class="mb-8 flex justify-center"> <div class="mt-4 flex justify-center gap-3">
<NuxtLink to="/setup/4"> <NuxtLink to="/setup/4" class="w-20">
<BaseButton as="span">{{ $t('general.no') }}</BaseButton> <BaseButton as="span" class="w-full justify-center">
{{ $t('general.no') }}
</BaseButton>
</NuxtLink> </NuxtLink>
<NuxtLink to="/setup/migrate"> <NuxtLink to="/setup/migrate" class="w-20">
<BaseButton as="span">{{ $t('general.yes') }}</BaseButton> <BaseButton as="span" class="w-full justify-center">
{{ $t('general.yes') }}
</BaseButton>
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
+12 -5
View File
@@ -1,21 +1,28 @@
<template> <template>
<div> <div>
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.setupConfigDesc') }} {{ $t('setup.setupConfigDesc') }}
</p> </p>
<div class="flex flex-col gap-3"> <div class="mt-8 flex flex-col gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
<FormNullTextField <FormHostField
id="host" id="host"
v-model="host" v-model="host"
:label="$t('general.host')" :label="$t('general.host')"
placeholder="vpn.example.com" placeholder="vpn.example.com"
:description="$t('setup.hostDesc')"
url="/api/setup/4"
/> />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<FormNumberField id="port" v-model="port" :label="$t('general.port')" /> <FormNumberField
id="port"
v-model="port"
:label="$t('general.port')"
:description="$t('setup.portDesc')"
/>
</div> </div>
<div> <div class="mt-4 flex justify-center">
<BaseButton @click="submit">{{ $t('general.continue') }}</BaseButton> <BaseButton @click="submit">{{ $t('general.continue') }}</BaseButton>
</div> </div>
</div> </div>
+6 -4
View File
@@ -1,13 +1,15 @@
<template> <template>
<div> <div class="flex flex-col items-center">
<p class="p-8 text-center text-lg"> <p class="text-center text-lg">
{{ $t('setup.setupMigrationDesc') }} {{ $t('setup.setupMigrationDesc') }}
</p> </p>
<div> <div class="mt-8 flex gap-3">
<Label for="migration">{{ $t('setup.migration') }}</Label> <Label for="migration">{{ $t('setup.migration') }}</Label>
<input id="migration" type="file" @change="onChangeFile" /> <input id="migration" type="file" @change="onChangeFile" />
</div> </div>
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton> <div class="mt-4">
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton>
</div>
</div> </div>
</template> </template>
+2 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div> <div class="flex flex-col items-center">
<p>{{ $t('setup.successful') }}</p> <p>{{ $t('setup.successful') }}</p>
<NuxtLink to="/login"> <NuxtLink to="/login" class="mt-4">
<BaseButton as="span">{{ $t('login.signIn') }}</BaseButton> <BaseButton as="span">{{ $t('login.signIn') }}</BaseButton>
</NuxtLink> </NuxtLink>
</div> </div>
+2 -2
View File
@@ -1,5 +1,5 @@
export const useGlobalStore = defineStore('Global', () => { export const useGlobalStore = defineStore('Global', () => {
const { data: release } = useFetch('/api/release', { const { data: information } = useFetch('/api/information', {
method: 'get', method: 'get',
}); });
@@ -21,7 +21,7 @@ export const useGlobalStore = defineStore('Global', () => {
return { return {
sortClient, sortClient,
release, information,
uiShowCharts, uiShowCharts,
toggleCharts, toggleCharts,
uiChartType, uiChartType,
+34 -24
View File
@@ -14,8 +14,7 @@
"email": "E-Mail" "email": "E-Mail"
}, },
"me": { "me": {
"currentPassword": "Current Password", "currentPassword": "Current Password"
"confirmPassword": "Confirm Password"
}, },
"general": { "general": {
"name": "Name", "name": "Name",
@@ -25,25 +24,30 @@
"updatePassword": "Update Password", "updatePassword": "Update Password",
"mtu": "MTU", "mtu": "MTU",
"allowedIps": "Allowed IPs", "allowedIps": "Allowed IPs",
"dns": "DNS",
"persistentKeepalive": "Persistent Keepalive", "persistentKeepalive": "Persistent Keepalive",
"logout": "Logout", "logout": "Logout",
"continue": "Continue", "continue": "Continue",
"host": "Host", "host": "Host",
"port": "Port", "port": "Port",
"yes": "Yes", "yes": "Yes",
"no": "No" "no": "No",
"confirmPassword": "Confirm Password",
"loading": "Loading..."
}, },
"setup": { "setup": {
"welcome": "Welcome to your first setup of wg-easy !", "welcome": "Welcome to your first setup of wg-easy",
"welcomeDesc": "You have found the easiest way to install and manage WireGuard on any Linux host!", "welcomeDesc": "You have found the easiest way to install and manage WireGuard on any Linux host",
"existingSetup": "Do you have an existing setup?", "existingSetup": "Do you have an existing setup?",
"createAdminDesc": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.", "createAdminDesc": "Please first enter an admin username and a strong secure password. This information will be used to log in to your administration panel.",
"setupConfigDesc": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.", "setupConfigDesc": "Please enter the host and port information. This will be used for the client configuration when setting up WireGuard on their devices.",
"setupMigrationDesc": "Please provide the backup file if you want to migrate your data from your previous wg-easy version to your new setup.", "setupMigrationDesc": "Please provide the backup file if you want to migrate your data from your previous wg-easy version to your new setup.",
"upload": "Upload", "upload": "Upload",
"migration": "Restore the backup", "migration": "Restore the backup:",
"createAccount": "Create Account", "createAccount": "Create Account",
"successful": "Setup successful" "successful": "Setup successful",
"hostDesc": "Public hostname clients will connect to",
"portDesc": "Public UDP port clients will connect to and WireGuard will listen on"
}, },
"update": { "update": {
"updateAvailable": "There is an update available!", "updateAvailable": "There is an update available!",
@@ -61,7 +65,8 @@
"login": { "login": {
"signIn": "Sign In", "signIn": "Sign In",
"rememberMe": "Remember me", "rememberMe": "Remember me",
"rememberMeDesc": "Stay logged after closing the browser" "rememberMeDesc": "Stay logged after closing the browser",
"insecure": "You can't log in with an insecure connection. Use HTTPS."
}, },
"error": { "error": {
"clear": "Clear", "clear": "Clear",
@@ -95,13 +100,14 @@
"noPrivKey": "This client has no known private key. Cannot create Configuration.", "noPrivKey": "This client has no known private key. Cannot create Configuration.",
"showQR": "Show QR Code", "showQR": "Show QR Code",
"downloadConfig": "Download Configuration", "downloadConfig": "Download Configuration",
"allowedIpsDesc": "Which IPs will be routed through the VPN", "allowedIpsDesc": "Which IPs will be routed through the VPN (overrides global config)",
"serverAllowedIpsDesc": "Which IPs the server will route to the client", "serverAllowedIpsDesc": "Which IPs the server will route to the client",
"mtuDesc": "Sets the maximum transmission unit (packet size) for the VPN tunnel", "mtuDesc": "Sets the maximum transmission unit (packet size) for the VPN tunnel",
"persistentKeepaliveDesc": "Sets the interval (in seconds) for keep-alive packets. 0 disables it", "persistentKeepaliveDesc": "Sets the interval (in seconds) for keep-alive packets. 0 disables it",
"hooks": "Hooks", "hooks": "Hooks",
"hooksDescription": "Hooks only work with wg-quick", "hooksDescription": "Hooks only work with wg-quick",
"hooksLeaveEmpty": "Only for wg-quick. Otherwise, leave it empty" "hooksLeaveEmpty": "Only for wg-quick. Otherwise, leave it empty",
"dnsDesc": "DNS server clients will use (overrides global config)"
}, },
"dialog": { "dialog": {
"change": "Change", "change": "Change",
@@ -121,6 +127,7 @@
"sectionGeneral": "General", "sectionGeneral": "General",
"sectionAdvanced": "Advanced", "sectionAdvanced": "Advanced",
"noItems": "No items", "noItems": "No items",
"nullNoItems": "No items. Using global config",
"add": "Add" "add": "Add"
}, },
"admin": { "admin": {
@@ -129,7 +136,7 @@
"sessionTimeoutDesc": "Session duration for Remember Me (seconds)", "sessionTimeoutDesc": "Session duration for Remember Me (seconds)",
"metrics": "Metrics", "metrics": "Metrics",
"metricsPassword": "Password", "metricsPassword": "Password",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (argon2 hash)", "metricsPasswordDesc": "Bearer Password for the metrics endpoint (password or argon2 hash)",
"json": "JSON", "json": "JSON",
"jsonDesc": "Route for metrics in JSON format", "jsonDesc": "Route for metrics in JSON format",
"prometheus": "Prometheus", "prometheus": "Prometheus",
@@ -138,12 +145,13 @@
"config": { "config": {
"connection": "Connection", "connection": "Connection",
"hostDesc": "Public hostname clients will connect to (invalidates config)", "hostDesc": "Public hostname clients will connect to (invalidates config)",
"portDesc": "Public UDP port clients will connect to (invalidates config)", "portDesc": "Public UDP port clients will connect to (invalidates config, you probably want to change Interface Port too)",
"allowedIpsDesc": "Allowed IPs clients will use (invalidates config)", "allowedIpsDesc": "Allowed IPs clients will use (global config)",
"dns": "DNS", "dnsDesc": "DNS server clients will use (global config)",
"dnsDesc": "DNS server clients will use (invalidates config)", "mtuDesc": "MTU clients will use (only for new clients)",
"mtuDesc": "MTU clients will use (invalidates config)", "persistentKeepaliveDesc": "Interval in seconds to send keepalives to the server. 0 = disabled (only for new clients)",
"persistentKeepaliveDesc": "Interval in seconds to send keepalives to the server. 0 = disabled (invalidates config)" "suggest": "Suggest",
"suggestDesc": "Choose a IP-Address or Hostname for the Host field"
}, },
"interface": { "interface": {
"cidrSuccess": "Changed CIDR", "cidrSuccess": "Changed CIDR",
@@ -151,9 +159,15 @@
"device": "Device", "device": "Device",
"deviceDesc": "Ethernet device the wireguard traffic should be forwarded through", "deviceDesc": "Ethernet device the wireguard traffic should be forwarded through",
"mtuDesc": "MTU WireGuard will use", "mtuDesc": "MTU WireGuard will use",
"portDesc": "UDP Port WireGuard will listen on (could invalidate config)", "portDesc": "UDP Port WireGuard will listen on (you probably want to change Config Port too)",
"changeCidr": "Change CIDR" "changeCidr": "Change CIDR",
} "restart": "Restart Interface",
"restartDesc": "Restart the WireGuard interface",
"restartWarn": "Are you sure to restart the interface? This will disconnect all clients.",
"restartSuccess": "Interface restarted",
"restartError": "Failed to restart interface"
},
"introText": "Welcome to the admin panel.\n\nHere you can manage the general settings, the configuration, the interface settings and the hooks.\n\nStart by choosing one of the sections in the sidebar."
}, },
"zod": { "zod": {
"generic": { "generic": {
@@ -176,10 +190,6 @@
"user": { "user": {
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"passwordUppercase": "Password must have at least 1 uppercase letter",
"passwordLowercase": "Password must have at least 1 lowercase letter",
"passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character",
"remember": "Remember", "remember": "Remember",
"name": "Name", "name": "Name",
"email": "Email", "email": "Email",
+2
View File
@@ -28,7 +28,9 @@ export default defineNuxtConfig({
}, },
locales: [ locales: [
{ {
// same as i18n.config.ts
code: 'en', code: 'en',
// BCP 47 language tag
language: 'en-US', language: 'en-US',
name: 'English', name: 'English',
}, },
+15 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "wg-easy", "name": "wg-easy",
"version": "15.0.0-beta.6", "version": "15.0.0-beta.11",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.", "description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -19,9 +19,11 @@
}, },
"dependencies": { "dependencies": {
"@eschricht/nuxt-color-mode": "^1.1.5", "@eschricht/nuxt-color-mode": "^1.1.5",
"@libsql/client": "^0.14.0", "@heroicons/vue": "^2.2.0",
"@nuxtjs/i18n": "^9.2.1", "@libsql/client": "^0.15.1",
"@nuxtjs/tailwindcss": "^6.13.1", "@nuxtjs/i18n": "^9.4.0",
"@nuxtjs/tailwindcss": "^6.13.2",
"@phc/format": "^1.0.0",
"@pinia/nuxt": "^0.10.1", "@pinia/nuxt": "^0.10.1",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"apexcharts": "^4.5.0", "apexcharts": "^4.5.0",
@@ -30,13 +32,13 @@
"cidr-tools": "^11.0.3", "cidr-tools": "^11.0.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"debug": "^4.4.0", "debug": "^4.4.0",
"drizzle-orm": "^0.40.0", "drizzle-orm": "^0.41.0",
"ip-bigint": "^8.2.1", "ip-bigint": "^8.2.1",
"is-cidr": "^5.1.1", "is-cidr": "^5.1.1",
"is-ip": "^5.0.1", "is-ip": "^5.0.1",
"js-sha256": "^0.11.0", "js-sha256": "^0.11.0",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
"nuxt": "^3.15.4", "nuxt": "^3.16.1",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-vue": "^1.9.17", "radix-vue": "^1.9.17",
@@ -48,17 +50,18 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "1.1.0", "@nuxt/eslint": "1.3.0",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/phc__format": "^1.0.1",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/semver": "^7.5.8", "@types/semver": "^7.7.0",
"drizzle-kit": "^0.30.5", "drizzle-kit": "^0.30.6",
"eslint": "^9.21.0", "eslint": "^9.23.0",
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.1.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
}, },
"packageManager": "pnpm@10.5.2" "packageManager": "pnpm@10.7.0"
} }
+1738 -2233
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
export default definePermissionEventHandler('admin', 'any', async () => {
await WireGuard.Restart();
return { success: true };
});
+4
View File
@@ -0,0 +1,4 @@
export default definePermissionEventHandler('admin', 'any', async () => {
const result = await cachedGetIpInformation();
return result;
});
@@ -3,9 +3,11 @@ import { gt } from 'semver';
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
const latestRelease = await cachedFetchLatestRelease(); const latestRelease = await cachedFetchLatestRelease();
const updateAvailable = gt(latestRelease.version, RELEASE); const updateAvailable = gt(latestRelease.version, RELEASE);
const insecure = WG_ENV.INSECURE;
return { return {
currentRelease: RELEASE, currentRelease: RELEASE,
latestRelease: latestRelease, latestRelease: latestRelease,
updateAvailable, updateAvailable,
insecure,
}; };
}); });
+3 -4
View File
@@ -2,11 +2,10 @@ export default defineEventHandler(async (event) => {
const session = await useWGSession(event); const session = await useWGSession(event);
if (!session.data.userId) { if (!session.data.userId) {
throw createError({ // not logged in
statusCode: 401, return null;
statusMessage: 'Not logged in',
});
} }
const user = await Database.users.get(session.data.userId); const user = await Database.users.get(session.data.userId);
if (!user) { if (!user) {
throw createError({ throw createError({
+4
View File
@@ -0,0 +1,4 @@
export default defineSetupEventHandler(4, async () => {
const result = await cachedGetIpInformation();
return result;
});
@@ -12,11 +12,11 @@ CREATE TABLE `clients_table` (
`public_key` text NOT NULL, `public_key` text NOT NULL,
`pre_shared_key` text NOT NULL, `pre_shared_key` text NOT NULL,
`expires_at` text, `expires_at` text,
`allowed_ips` text NOT NULL, `allowed_ips` text,
`server_allowed_ips` text NOT NULL, `server_allowed_ips` text NOT NULL,
`persistent_keepalive` integer NOT NULL, `persistent_keepalive` integer NOT NULL,
`mtu` integer NOT NULL, `mtu` integer NOT NULL,
`dns` text NOT NULL, `dns` text,
`enabled` integer NOT NULL, `enabled` integer NOT NULL,
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
@@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "383501e4-f8de-4413-847f-a9082f6dc398", "id": "8c2af02b-c4bd-4880-a9ad-b38805636208",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"clients_table": { "clients_table": {
@@ -106,7 +106,7 @@
"name": "allowed_ips", "name": "allowed_ips",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"server_allowed_ips": { "server_allowed_ips": {
@@ -134,7 +134,7 @@
"name": "dns", "name": "dns",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"enabled": { "enabled": {
@@ -1,6 +1,6 @@
{ {
"id": "bf316694-e2ce-4e29-bd66-ce6c0a9d3c90", "id": "a61263b1-9af1-4d2e-99e9-80d08127b545",
"prevId": "383501e4-f8de-4413-847f-a9082f6dc398", "prevId": "8c2af02b-c4bd-4880-a9ad-b38805636208",
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"tables": { "tables": {
@@ -106,7 +106,7 @@
"name": "allowed_ips", "name": "allowed_ips",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"server_allowed_ips": { "server_allowed_ips": {
@@ -134,7 +134,7 @@
"name": "dns", "name": "dns",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"enabled": { "enabled": {
@@ -5,14 +5,14 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1741335144499, "when": 1741355094140,
"tag": "0000_short_skin", "tag": "0000_short_skin",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "6", "version": "6",
"when": 1741335153054, "when": 1741355098159,
"tag": "0001_classy_the_stranger", "tag": "0001_classy_the_stranger",
"breakpoints": true "breakpoints": true
} }
@@ -3,6 +3,8 @@ import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { oneTimeLink, user } from '../../schema'; import { oneTimeLink, user } from '../../schema';
/** null means use value from userConfig */
export const client = sqliteTable('clients_table', { export const client = sqliteTable('clients_table', {
id: int().primaryKey({ autoIncrement: true }), id: int().primaryKey({ autoIncrement: true }),
userId: int('user_id') userId: int('user_id')
@@ -22,13 +24,13 @@ export const client = sqliteTable('clients_table', {
publicKey: text('public_key').notNull(), publicKey: text('public_key').notNull(),
preSharedKey: text('pre_shared_key').notNull(), preSharedKey: text('pre_shared_key').notNull(),
expiresAt: text('expires_at'), expiresAt: text('expires_at'),
allowedIps: text('allowed_ips', { mode: 'json' }).$type<string[]>().notNull(), allowedIps: text('allowed_ips', { mode: 'json' }).$type<string[]>(),
serverAllowedIps: text('server_allowed_ips', { mode: 'json' }) serverAllowedIps: text('server_allowed_ips', { mode: 'json' })
.$type<string[]>() .$type<string[]>()
.notNull(), .notNull(),
persistentKeepalive: int('persistent_keepalive').notNull(), persistentKeepalive: int('persistent_keepalive').notNull(),
mtu: int().notNull(), mtu: int().notNull(),
dns: text({ mode: 'json' }).$type<string[]>().notNull(), dns: text({ mode: 'json' }).$type<string[]>(),
enabled: int({ mode: 'boolean' }).notNull(), enabled: int({ mode: 'boolean' }).notNull(),
createdAt: text('created_at') createdAt: text('created_at')
.notNull() .notNull()
@@ -1,5 +1,5 @@
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { parseCidr } from 'cidr-tools'; import { containsCidr, parseCidr } from 'cidr-tools';
import { client } from './schema'; import { client } from './schema';
import type { import type {
ClientCreateFromExistingType, ClientCreateFromExistingType,
@@ -115,8 +115,6 @@ export class ClientService {
ipv4Address, ipv4Address,
ipv6Address, ipv6Address,
mtu: clientConfig.defaultMtu, mtu: clientConfig.defaultMtu,
allowedIps: clientConfig.defaultAllowedIps,
dns: clientConfig.defaultDns,
persistentKeepalive: clientConfig.defaultPersistentKeepalive, persistentKeepalive: clientConfig.defaultPersistentKeepalive,
serverAllowedIps: [], serverAllowedIps: [],
enabled: true, enabled: true,
@@ -134,7 +132,27 @@ export class ClientService {
} }
update(id: ID, data: UpdateClientType) { update(id: ID, data: UpdateClientType) {
return this.#db.update(client).set(data).where(eq(client.id, id)).execute(); return this.#db.transaction(async (tx) => {
const clientInterface = await tx.query.wgInterface
.findFirst({
where: eq(wgInterface.name, 'wg0'),
})
.execute();
if (!clientInterface) {
throw new Error('WireGuard interface not found');
}
if (!containsCidr(clientInterface.ipv4Cidr, data.ipv4Address)) {
throw new Error('IPv4 address is not within the CIDR range');
}
if (!containsCidr(clientInterface.ipv6Cidr, data.ipv6Address)) {
throw new Error('IPv6 address is not within the CIDR range');
}
await tx.update(client).set(data).where(eq(client.id, id)).execute();
});
} }
async createFromExisting({ async createFromExisting({
@@ -61,11 +61,11 @@ export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
postUp: HookSchema, postUp: HookSchema,
preDown: HookSchema, preDown: HookSchema,
postDown: HookSchema, postDown: HookSchema,
allowedIps: AllowedIpsSchema, allowedIps: AllowedIpsSchema.nullable(),
serverAllowedIps: serverAllowedIps, serverAllowedIps: serverAllowedIps,
mtu: MtuSchema, mtu: MtuSchema,
persistentKeepalive: PersistentKeepaliveSchema, persistentKeepalive: PersistentKeepaliveSchema,
dns: DnsSchema, dns: DnsSchema.nullable(),
}) })
); );
@@ -107,7 +107,15 @@ export class GeneralService {
}; };
} }
update(data: GeneralUpdateType) { async update(data: GeneralUpdateType) {
// only hash the password if it is not already hashed
if (
data.metricsPassword !== null &&
!isValidPasswordHash(data.metricsPassword)
) {
data.metricsPassword = await hashPassword(data.metricsPassword);
}
return this.#db.update(general).set(data).execute(); return this.#db.update(general).set(data).execute();
} }
@@ -11,7 +11,6 @@ const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') });
const metricsPassword = z const metricsPassword = z
.string({ message: t('zod.general.metricsPassword') }) .string({ message: t('zod.general.metricsPassword') })
.min(1, { message: t('zod.general.metricsPassword') }) .min(1, { message: t('zod.general.metricsPassword') })
// TODO?: validate argon2 regex
.nullable(); .nullable();
export const GeneralUpdateSchema = z.object({ export const GeneralUpdateSchema = z.object({
+10 -9
View File
@@ -6,16 +6,12 @@ export type UserType = InferSelectModel<typeof user>;
const username = z const username = z
.string({ message: t('zod.user.username') }) .string({ message: t('zod.user.username') })
.min(8, t('zod.user.username')) .min(2, t('zod.user.username'))
.pipe(safeStringRefine); .pipe(safeStringRefine);
const password = z const password = z
.string({ message: t('zod.user.password') }) .string({ message: t('zod.user.password') })
.min(12, t('zod.user.password')) .min(12, t('zod.user.password'))
.regex(/[A-Z]/, t('zod.user.passwordUppercase'))
.regex(/[a-z]/, t('zod.user.passwordLowercase'))
.regex(/\d/, t('zod.user.passwordNumber'))
.regex(/[!@#$%^&*(),.?":{}|<>]/, t('zod.user.passwordSpecial'))
.pipe(safeStringRefine); .pipe(safeStringRefine);
const remember = z.boolean({ message: t('zod.user.remember') }); const remember = z.boolean({ message: t('zod.user.remember') });
@@ -26,10 +22,15 @@ export const UserLoginSchema = z.object({
remember: remember, remember: remember,
}); });
export const UserSetupSchema = z.object({ export const UserSetupSchema = z
username: username, .object({
password: password, username: username,
}); password: password,
confirmPassword: password,
})
.refine((val) => val.password === val.confirmPassword, {
message: t('zod.user.passwordMatch'),
});
const name = z const name = z
.string({ message: t('zod.user.name') }) .string({ message: t('zod.user.name') })
@@ -1,6 +1,7 @@
import { eq, sql } from 'drizzle-orm'; import { eq, sql } from 'drizzle-orm';
import { userConfig } from './schema'; import { userConfig } from './schema';
import type { UserConfigUpdateType } from './types'; import type { UserConfigUpdateType } from './types';
import { wgInterface } from '#db/schema';
import type { DBType } from '#db/sqlite'; import type { DBType } from '#db/sqlite';
function createPreparedStatement(db: DBType) { function createPreparedStatement(db: DBType) {
@@ -8,14 +9,6 @@ function createPreparedStatement(db: DBType) {
get: db.query.userConfig get: db.query.userConfig
.findFirst({ where: eq(userConfig.id, sql.placeholder('interface')) }) .findFirst({ where: eq(userConfig.id, sql.placeholder('interface')) })
.prepare(), .prepare(),
updateHostPort: db
.update(userConfig)
.set({
host: sql.placeholder('host') as never as string,
port: sql.placeholder('port') as never as number,
})
.where(eq(userConfig.id, sql.placeholder('interface')))
.prepare(),
}; };
} }
@@ -38,11 +31,26 @@ export class UserConfigService {
return userConfig; return userConfig;
} }
// TODO: wrap ipv6 host in square brackets
/**
* sets host of user config
*
* sets port of user config and interface
*/
updateHostPort(host: string, port: number) { updateHostPort(host: string, port: number) {
return this.#statements.updateHostPort.execute({ return this.#db.transaction(async (tx) => {
interface: 'wg0', await tx
host, .update(userConfig)
port, .set({ host, port })
.where(eq(userConfig.id, 'wg0'))
.execute();
await tx
.update(wgInterface)
.set({ port })
.where(eq(wgInterface.name, 'wg0'))
.execute();
}); });
} }
+51 -1
View File
@@ -19,7 +19,13 @@ const db = drizzle({ client, schema });
export async function connect() { export async function connect() {
await migrate(); await migrate();
return new DBService(db); const dbService = new DBService(db);
if (WG_INITIAL_ENV.ENABLED) {
await initialSetup(dbService);
}
return dbService;
} }
class DBService { class DBService {
@@ -58,3 +64,47 @@ async function migrate() {
} }
} }
} }
async function initialSetup(db: DBServiceType) {
const setup = await db.general.getSetupStep();
if (setup.done) {
DB_DEBUG('Setup already done. Skiping initial setup.');
return;
}
if (WG_INITIAL_ENV.IPV4_CIDR && WG_INITIAL_ENV.IPV6_CIDR) {
DB_DEBUG('Setting initial CIDR...');
await db.interfaces.updateCidr({
ipv4Cidr: WG_INITIAL_ENV.IPV4_CIDR,
ipv6Cidr: WG_INITIAL_ENV.IPV6_CIDR,
});
}
if (WG_INITIAL_ENV.DNS) {
DB_DEBUG('Setting initial DNS...');
const userConfig = await db.userConfigs.get();
await db.userConfigs.update({
...userConfig,
defaultDns: WG_INITIAL_ENV.DNS,
});
}
if (
WG_INITIAL_ENV.USERNAME &&
WG_INITIAL_ENV.PASSWORD &&
WG_INITIAL_ENV.HOST &&
WG_INITIAL_ENV.PORT
) {
DB_DEBUG('Creating initial user...');
await db.users.create(WG_INITIAL_ENV.USERNAME, WG_INITIAL_ENV.PASSWORD);
DB_DEBUG('Setting initial host and port...');
await db.userConfigs.updateHostPort(
WG_INITIAL_ENV.HOST,
WG_INITIAL_ENV.PORT
);
await db.general.setSetupStep(0);
}
}
+5
View File
@@ -193,6 +193,11 @@ class WireGuard {
await wg.down(wgInterface.name).catch(() => {}); await wg.down(wgInterface.name).catch(() => {});
} }
async Restart() {
const wgInterface = await Database.interfaces.get();
await wg.restart(wgInterface.name);
}
async cronJob() { async cronJob() {
const clients = await Database.clients.getAll(); const clients = await Database.clients.getAll();
// Expires Feature // Expires Feature
+29
View File
@@ -0,0 +1,29 @@
type Opts = {
/**
* Expiry time in milliseconds
*/
expiry: number;
};
/**
* Cache function for 1 hour
*/
export function cacheFunction<T>(fn: () => T, { expiry }: Opts): () => T {
let cache: { value: T; expiry: number } | null = null;
return (): T => {
const now = Date.now();
if (cache && cache.expiry > now) {
return cache.value;
}
const result = fn();
cache = {
value: result,
expiry: now + expiry,
};
return result;
};
}
+25
View File
@@ -15,4 +15,29 @@ export const OLD_ENV = {
export const WG_ENV = { export const WG_ENV = {
/** UI is hosted on HTTP instead of HTTPS */ /** UI is hosted on HTTP instead of HTTPS */
INSECURE: process.env.INSECURE === 'true', INSECURE: process.env.INSECURE === 'true',
/** Port the UI is listening on */
PORT: assertEnv('PORT'),
}; };
export const WG_INITIAL_ENV = {
ENABLED: process.env.INIT_ENABLED === 'true',
USERNAME: process.env.INIT_USERNAME,
PASSWORD: process.env.INIT_PASSWORD,
DNS: process.env.INIT_DNS?.split(',').map((x) => x.trim()),
IPV4_CIDR: process.env.INIT_IPV4_CIDR,
IPV6_CIDR: process.env.INIT_IPV6_CIDR,
HOST: process.env.INIT_HOST,
PORT: process.env.INIT_PORT
? Number.parseInt(process.env.INIT_PORT, 10)
: undefined,
};
function assertEnv<T extends string>(env: T) {
const val = process.env[env];
if (!val) {
throw new Error(`Missing environment variable: ${env}`);
}
return val;
}
+140 -1
View File
@@ -1,5 +1,7 @@
import type { parseCidr } from 'cidr-tools'; import { Resolver } from 'node:dns/promises';
import { networkInterfaces } from 'node:os';
import { stringifyIp } from 'ip-bigint'; import { stringifyIp } from 'ip-bigint';
import type { parseCidr } from 'cidr-tools';
import type { ClientNextIpType } from '#db/repositories/client/types'; import type { ClientNextIpType } from '#db/repositories/client/types';
@@ -31,3 +33,140 @@ export function nextIP(
return address; return address;
} }
// use opendns to get public ip
const dnsServers = {
ip4: ['208.67.222.222'],
ip6: ['2620:119:35::35'],
ip: 'myip.opendns.com',
};
async function getPublicInformation() {
const ipv4 = await getPublicIpv4();
const ipv6 = await getPublicIpv6();
const ptr4 = ipv4 ? await getReverseDns(ipv4) : [];
const ptr6 = ipv6 ? await getReverseDns(ipv6) : [];
const hostnames = [...new Set([...ptr4, ...ptr6])];
return { ipv4, ipv6, hostnames };
}
async function getPublicIpv4() {
try {
const resolver = new Resolver();
resolver.setServers(dnsServers.ip4);
const ipv4 = await resolver.resolve4(dnsServers.ip);
return ipv4[0];
} catch {
return null;
}
}
async function getPublicIpv6() {
try {
const resolver = new Resolver();
resolver.setServers(dnsServers.ip6);
const ipv6 = await resolver.resolve6(dnsServers.ip);
return ipv6[0];
} catch {
return null;
}
}
async function getReverseDns(ip: string) {
try {
const resolver = new Resolver();
resolver.setServers([...dnsServers.ip4, ...dnsServers.ip6]);
const ptr = await resolver.reverse(ip);
return ptr;
} catch {
return [];
}
}
function getPrivateInformation() {
const interfaces = networkInterfaces();
const interfaceNames = Object.keys(interfaces);
const obj: Record<string, { ipv4: string[]; ipv6: string[] }> = {};
for (const name of interfaceNames) {
if (name === 'wg0') {
continue;
}
const iface = interfaces[name];
if (!iface) continue;
for (const { family, internal, address } of iface) {
if (internal) {
continue;
}
if (!obj[name]) {
obj[name] = {
ipv4: [],
ipv6: [],
};
}
if (family === 'IPv4') {
obj[name].ipv4.push(address);
} else if (family === 'IPv6') {
obj[name].ipv6.push(address);
}
}
}
return obj;
}
async function getIpInformation() {
const results = [];
const publicInfo = await getPublicInformation();
if (publicInfo.ipv4) {
results.push({
value: publicInfo.ipv4,
label: 'IPv4 - Public',
});
}
if (publicInfo.ipv6) {
results.push({
value: `[${publicInfo.ipv6}]`,
label: 'IPv6 - Public',
});
}
for (const hostname of publicInfo.hostnames) {
results.push({
value: hostname,
label: 'Hostname - Public',
});
}
const privateInfo = getPrivateInformation();
for (const [name, { ipv4, ipv6 }] of Object.entries(privateInfo)) {
for (const ip of ipv4) {
results.push({
value: ip,
label: `IPv4 - ${name}`,
});
}
for (const ip of ipv6) {
results.push({
value: `[${ip}]`,
label: `IPv6 - ${name}`,
});
}
}
return results;
}
/**
* Fetch IP Information
* @cache Response is cached for 15 min
*/
export const cachedGetIpInformation = cacheFunction(getIpInformation, {
expiry: 15 * 60 * 1000,
});
+19
View File
@@ -1,4 +1,5 @@
import argon2 from 'argon2'; import argon2 from 'argon2';
import { deserialize } from '@phc/format';
/** /**
* Checks if `password` matches the hash. * Checks if `password` matches the hash.
@@ -16,3 +17,21 @@ export function isPasswordValid(
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string): Promise<string> {
return argon2.hash(password); return argon2.hash(password);
} }
/**
* Checks if the password hash is valid.
* This only checks if the hash is a valid PHC formatted string using argon2.
*/
export function isValidPasswordHash(hash: string): boolean {
try {
const obj = deserialize(hash);
if (obj.id !== 'argon2i' && obj.id !== 'argon2d' && obj.id !== 'argon2id') {
return false;
}
return true;
} catch {
return false;
}
}
+3 -24
View File
@@ -3,29 +3,6 @@ type GithubRelease = {
body: string; body: string;
}; };
/**
* Cache function for 1 hour
*/
function cacheFunction<T>(fn: () => T): () => T {
let cache: { value: T; expiry: number } | null = null;
return (): T => {
const now = Date.now();
if (cache && cache.expiry > now) {
return cache.value;
}
const result = fn();
cache = {
value: result,
expiry: now + 3600000,
};
return result;
};
}
async function fetchLatestRelease() { async function fetchLatestRelease() {
try { try {
const response = await $fetch<GithubRelease>( const response = await $fetch<GithubRelease>(
@@ -53,4 +30,6 @@ async function fetchLatestRelease() {
* Fetch latest release from GitHub * Fetch latest release from GitHub
* @cache Response is cached for 1 hour * @cache Response is cached for 1 hour
*/ */
export const cachedFetchLatestRelease = cacheFunction(fetchLatestRelease); export const cachedFetchLatestRelease = cacheFunction(fetchLatestRelease, {
expiry: 60 * 60 * 1000,
});
+5
View File
@@ -91,6 +91,11 @@ export async function getCurrentUser(event: H3Event) {
}); });
} }
user = foundUser; user = foundUser;
} else {
throw createError({
statusCode: 401,
statusMessage: 'Session failed. No Authorization',
});
} }
if (!user) { if (!user) {
+2
View File
@@ -15,6 +15,7 @@ export function template(templ: string, values: Record<string, string>) {
* - ipv6Cidr: IPv6 CIDR * - ipv6Cidr: IPv6 CIDR
* - device: Network device * - device: Network device
* - port: Port number * - port: Port number
* - uiPort: UI port number
*/ */
export function iptablesTemplate(templ: string, wgInterface: InterfaceType) { export function iptablesTemplate(templ: string, wgInterface: InterfaceType) {
return template(templ, { return template(templ, {
@@ -22,5 +23,6 @@ export function iptablesTemplate(templ: string, wgInterface: InterfaceType) {
ipv6Cidr: wgInterface.ipv6Cidr, ipv6Cidr: wgInterface.ipv6Cidr,
device: wgInterface.device, device: wgInterface.device,
port: wgInterface.port.toString(), port: wgInterface.port.toString(),
uiPort: WG_ENV.PORT,
}); });
} }
+6 -2
View File
@@ -59,13 +59,13 @@ PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
return `[Interface] return `[Interface]
PrivateKey = ${client.privateKey} PrivateKey = ${client.privateKey}
Address = ${client.ipv4Address}/${cidr4Block}, ${client.ipv6Address}/${cidr6Block} Address = ${client.ipv4Address}/${cidr4Block}, ${client.ipv6Address}/${cidr6Block}
DNS = ${client.dns.join(', ')} DNS = ${(client.dns ?? userConfig.defaultDns).join(', ')}
MTU = ${client.mtu} MTU = ${client.mtu}
${hookLines.length ? `${hookLines.join('\n')}\n` : ''} ${hookLines.length ? `${hookLines.join('\n')}\n` : ''}
[Peer] [Peer]
PublicKey = ${wgInterface.publicKey} PublicKey = ${wgInterface.publicKey}
PresharedKey = ${client.preSharedKey} PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.allowedIps.join(', ')} AllowedIPs = ${(client.allowedIps ?? userConfig.defaultAllowedIps).join(', ')}
PersistentKeepalive = ${client.persistentKeepalive} PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${userConfig.host}:${userConfig.port}`; Endpoint = ${userConfig.host}:${userConfig.port}`;
}, },
@@ -92,6 +92,10 @@ Endpoint = ${userConfig.host}:${userConfig.port}`;
return exec(`wg-quick down ${infName}`); return exec(`wg-quick down ${infName}`);
}, },
restart: (infName: string) => {
return exec(`wg-quick down ${infName}; wg-quick up ${infName}`);
},
sync: (infName: string) => { sync: (infName: string) => {
return exec(`wg syncconf ${infName} <(wg-quick strip ${infName})`); return exec(`wg syncconf ${infName} <(wg-quick strip ${infName})`);
}, },