Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c7f64ebd5 | |||
| 589ec1fe9a | |||
| 6e0d758e36 | |||
| 940edb2b0c | |||
| d51f12a82f | |||
| 4a3747fa12 | |||
| 499fb096b6 | |||
| c5c3a65bbf | |||
| c133446f9c | |||
| e8b3e54228 | |||
| a9a51337da | |||
| bbee7e04ed | |||
| 198b240755 | |||
| 86bdbe4c3d | |||
| 4890bb28e5 | |||
| c3dbd3a815 | |||
| fc480df910 | |||
| b3bd2502af | |||
| eb5ad91022 | |||
| f2955a1278 | |||
| 1b76c066e0 | |||
| 5b68cc7311 | |||
| 0597470f4c | |||
| 159a51cff4 |
Vendored
+3
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 ./
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<SparklesIcon />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import SparklesIcon from '@heroicons/vue/24/outline/esm/SparklesIcon';
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1738
-2233
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 };
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
@@ -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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})`);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user