Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ef258a28a | |||
| ff783fd4d1 | |||
| 65aa067100 | |||
| 48e6949a4d | |||
| 9ebf2c1d33 | |||
| d0f85316a6 | |||
| ff9fd553c5 | |||
| e92ee0464e | |||
| 9df049d3f4 | |||
| 32b73b850a | |||
| 1c7f64ebd5 | |||
| 589ec1fe9a | |||
| 6e0d758e36 | |||
| 940edb2b0c | |||
| d51f12a82f | |||
| 4a3747fa12 | |||
| 499fb096b6 | |||
| c5c3a65bbf | |||
| c133446f9c | |||
| e8b3e54228 | |||
| a9a51337da | |||
| bbee7e04ed | |||
| 198b240755 | |||
| 86bdbe4c3d | |||
| 4890bb28e5 | |||
| c3dbd3a815 | |||
| fc480df910 | |||
| b3bd2502af | |||
| eb5ad91022 | |||
| f2955a1278 | |||
| 1b76c066e0 | |||
| 5b68cc7311 |
@@ -6,6 +6,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
|
# This workflow does not support fixing old versions
|
||||||
|
# as this will break the latest and major tags
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
name: Build & Deploy Docker
|
name: Build & Deploy Docker
|
||||||
@@ -31,6 +34,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/wg-easy/wg-easy
|
ghcr.io/wg-easy/wg-easy
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
@@ -87,8 +92,6 @@ jobs:
|
|||||||
cd docs
|
cd docs
|
||||||
git fetch origin gh-pages --depth=1 || true
|
git fetch origin gh-pages --depth=1 || true
|
||||||
|
|
||||||
# latest will point to old docs if old tag is pushed
|
|
||||||
|
|
||||||
# Extract version numbers
|
# Extract version numbers
|
||||||
DOCS_VERSION=${GITHUB_REF#refs/tags/} # e.g. v1.2.3 or v1.2.3-beta
|
DOCS_VERSION=${GITHUB_REF#refs/tags/} # e.g. v1.2.3 or v1.2.3-beta
|
||||||
MINOR_VERSION=$(echo $DOCS_VERSION | cut -d. -f1,2) # e.g. v1.2
|
MINOR_VERSION=$(echo $DOCS_VERSION | cut -d. -f1,2) # e.g. v1.2
|
||||||
|
|||||||
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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
We're super excited to announce v15!
|
We're super excited to announce v15!
|
||||||
This update is an entire rewrite to make it even easier to set up your own VPN.
|
This update is an entire rewrite to make it even easier to set up your own VPN.
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
As the whole setup has changed, we recommend to start from scratch. And import your existing configs.
|
||||||
|
|
||||||
## Major Changes
|
## Major Changes
|
||||||
|
|
||||||
- Almost all Environment variables removed
|
- Almost all Environment variables removed
|
||||||
@@ -24,6 +28,10 @@ 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
|
||||||
|
- Added 2FA using TOTP
|
||||||
|
- Improved mobile support
|
||||||
|
|
||||||
## [14.0.0] - 2024-09-04
|
## [14.0.0] - 2024-09-04
|
||||||
|
|
||||||
|
|||||||
+3
-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 \
|
||||||
@@ -34,6 +34,7 @@ RUN apk add --no-cache \
|
|||||||
dumb-init \
|
dumb-init \
|
||||||
iptables \
|
iptables \
|
||||||
ip6tables \
|
ip6tables \
|
||||||
|
nftables \
|
||||||
kmod \
|
kmod \
|
||||||
iptables-legacy \
|
iptables-legacy \
|
||||||
wireguard-tools
|
wireguard-tools
|
||||||
@@ -47,6 +48,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 ./
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
[](https://github.com/wg-easy/wg-easy/stargazers)
|
[](https://github.com/wg-easy/wg-easy/stargazers)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/wg-easy/wg-easy/releases/latest)
|
[](https://github.com/wg-easy/wg-easy/releases/latest)
|
||||||
[](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
|
[](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
|
||||||
|
|
||||||
<!-- TODO: remove after release -->
|
<!-- TODO: remove after release -->
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
|||||||
- Prometheus metrics support
|
- Prometheus metrics support
|
||||||
- IPv6 support
|
- IPv6 support
|
||||||
- CIDR support
|
- CIDR support
|
||||||
|
- 2FA support
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> To better manage documentation for this project, it has its own site here: [https://wg-easy.github.io/wg-easy/latest](https://wg-easy.github.io/wg-easy/latest)
|
> To better manage documentation for this project, it has its own site here: [https://wg-easy.github.io/wg-easy/latest](https://wg-easy.github.io/wg-easy/latest)
|
||||||
@@ -50,36 +51,19 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
|||||||
- [Getting Started](https://wg-easy.github.io/wg-easy/latest/getting-started/)
|
- [Getting Started](https://wg-easy.github.io/wg-easy/latest/getting-started/)
|
||||||
- [Basic Installation](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/basic-installation/)
|
- [Basic Installation](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/basic-installation/)
|
||||||
- [Caddy](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/caddy/)
|
- [Caddy](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/caddy/)
|
||||||
- [Nginx](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/nginx/)
|
|
||||||
- [Traefik](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/traefik/)
|
- [Traefik](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/traefik/)
|
||||||
- [Podman](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/podman/)
|
- [Podman](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/podman-nft/)
|
||||||
- [AdGuard Home](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/adguard/)
|
- [AdGuard Home](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/adguard/)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you want to migrate from the old version to the new version, you can find the migration guide here: [Migration Guide](https://wg-easy.github.io/wg-easy/latest/advanced/migrate/)
|
> If you want to migrate from the old version to the new version, you can find the migration guide here: [Migration Guide](https://wg-easy.github.io/wg-easy/latest/advanced/migrate/)
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- A host with a kernel that supports WireGuard (all modern kernels).
|
|
||||||
- A host with Docker installed.
|
|
||||||
|
|
||||||
## Versions
|
|
||||||
|
|
||||||
> 💡 We follow semantic versioning (semver)
|
|
||||||
|
|
||||||
We offer multiple Docker image tags to suit your needs. The table below is in a particular order, with the first tag being the most recommended:
|
|
||||||
|
|
||||||
| tag | Branch | Example | Description |
|
|
||||||
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| `15` | latest minor for that major tag | `ghcr.io/wg-easy/wg-easy:15` | latest features for specific major versions, no breaking changes |
|
|
||||||
| `latest` | latest tag | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possible get bug fixes quickly when needed, see Releases for more information. |
|
|
||||||
| `15.0` | latest patch for that minor tag | `ghcr.io/wg-easy/wg-easy:15.0` | latest patches for specific minor version |
|
|
||||||
| `15.0.0` | specific tag | `ghcr.io/wg-easy/wg-easy:15.0.0` | specific release, don't use this as this will not get updated |
|
|
||||||
| `nightly` | [`master`](https://github.com/wg-easy/wg-easy/tree/master) | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against [`master`](https://github.com/wg-easy/wg-easy/tree/master). |
|
|
||||||
| `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into [`master`](https://github.com/wg-easy/wg-easy/tree/master). |
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
This is a quick start guide to get you up and running with WireGuard Easy.
|
||||||
|
|
||||||
|
For a more detailed installation guide, please refer to the [Getting Started](https://wg-easy.github.io/wg-easy/latest/getting-started/) page.
|
||||||
|
|
||||||
### 1. Install Docker
|
### 1. Install Docker
|
||||||
|
|
||||||
If you haven't installed Docker yet, install it by running as root:
|
If you haven't installed Docker yet, install it by running as root:
|
||||||
@@ -95,14 +79,13 @@ And log in again.
|
|||||||
|
|
||||||
The easiest way to run WireGuard Easy is with Docker Compose.
|
The easiest way to run WireGuard Easy is with Docker Compose.
|
||||||
|
|
||||||
Just download [`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
|
Just download [`docker-compose.yml`](docker-compose.yml) and execute `sudo docker compose up -d`.
|
||||||
execute `sudo docker compose up -d`.
|
|
||||||
|
|
||||||
Now setup a reverse proxy to be able to access the Web UI from the internet.
|
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
|
||||||
|
|
||||||
### Donate
|
## 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:
|
||||||
|
|||||||
@@ -2,4 +2,37 @@
|
|||||||
title: API
|
title: API
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
You can use the API to interact with the application programmatically. The API is available at `/api` and supports both GET and POST requests. The API is designed to be simple and easy to use, with a focus on providing a consistent interface for all endpoints.
|
||||||
|
|
||||||
|
There is no documentation for the API yet, but this will be added as the underlying library supports it.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
To use the API, you need to authenticate using Basic Authentication. The username and password are the same as the ones you use to log in to the web application.
|
||||||
|
If you use 2FA, the API will not work. You need to disable 2FA in the web application to use the API.
|
||||||
|
|
||||||
|
### Authentication Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
|
url = "https://example.com:51821/api/client"
|
||||||
|
response = requests.get(url, auth=HTTPBasicAuth('username', 'password'))
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(data)
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.status_code}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
The Endpoints are not yet documented. But as file-based routing is used, you can find the endpoints in the `src/server/api` folder. The method is defined in the file name.
|
||||||
|
|
||||||
|
### Endpoints Example
|
||||||
|
|
||||||
|
| File Name | Endpoint | Method |
|
||||||
|
| -------------------------------- | -------------- | ------ |
|
||||||
|
| `src/server/api/client.get.ts` | `/api/client` | GET |
|
||||||
|
| `src/server/api/setup/2.post.ts` | `/api/setup/2` | POST |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Optional Configuration
|
title: Optional Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
You can set these environment variables to configure the container. They are not required, but can be useful in some cases.
|
||||||
|
|
||||||
| Env | Default | Example | Description |
|
| Env | Default | Example | Description |
|
||||||
| ---------- | --------- | ----------- | ------------------------------ |
|
| ---------- | --------- | ----------- | ------------------------------ |
|
||||||
|
|||||||
@@ -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 password. Otherwise, the user won't be able to log in.
|
||||||
|
|
||||||
|
It's recommended to remove the variables after the setup is done to prevent the password from being exposed.
|
||||||
|
///
|
||||||
@@ -2,6 +2,41 @@
|
|||||||
title: Prometheus
|
title: Prometheus
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
To monitor the WireGuard server, you can use [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/). The container exposes a `/metrics/prometheus` endpoint that can be scraped by Prometheus.
|
||||||
|
|
||||||
<!-- TOOD: add to docs: Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) -->
|
## Enable Prometheus
|
||||||
|
|
||||||
|
To enable Prometheus metrics, go to Admin Panel > General and enable Prometheus.
|
||||||
|
|
||||||
|
You can optionally set a Bearer Password for the metrics endpoints. This is useful if you want to expose the metrics endpoint to the internet.
|
||||||
|
|
||||||
|
## Configure Prometheus
|
||||||
|
|
||||||
|
You need to add a scrape config to your Prometheus configuration file. Here is an example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "wg-easy"
|
||||||
|
scrape_interval: 30s
|
||||||
|
metrics_path: /metrics/prometheus
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- "localhost:51821"
|
||||||
|
authorization:
|
||||||
|
type: Bearer
|
||||||
|
credentials: "SuperSecurePassword"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Grafana Dashboard
|
||||||
|
|
||||||
|
You can use the following Grafana dashboard to visualize the metrics:
|
||||||
|
|
||||||
|
[](https://grafana.com/grafana/dashboards/21733-wireguard/)
|
||||||
|
|
||||||
|
[21733](https://grafana.com/grafana/dashboards/21733-wireguard/)
|
||||||
|
|
||||||
|
/// note | Unofficial
|
||||||
|
|
||||||
|
The Grafana dashboard is not official and is not maintained by the `wg-easy` team. If you have any issues with the dashboard, please contact the author of the dashboard.
|
||||||
|
See [#1299](https://github.com/wg-easy/wg-easy/pull/1299) for more information.
|
||||||
|
///
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"1": "Initial version. Enjoy!",
|
|
||||||
"2": "You can now rename a client & update the address. Enjoy!",
|
|
||||||
"3": "Many improvements and small changes. Enjoy!",
|
|
||||||
"4": "Now with pretty charts for client's network speed. Enjoy!",
|
|
||||||
"5": "Many small improvements & feature requests. Enjoy!",
|
|
||||||
"6": "Many small performance improvements & bug fixes. Enjoy!",
|
|
||||||
"7": "Improved the look & performance of the upload/download chart.",
|
|
||||||
"8": "Updated to Node.js v18.",
|
|
||||||
"9": "Fixed issue running on devices with older kernels.",
|
|
||||||
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
|
||||||
"11": "Multilanguage Support & various bugfixes.",
|
|
||||||
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more.",
|
|
||||||
"13": "New framework (h3), UI_CHART_TYPE, some bugfixes & more.",
|
|
||||||
"14": "Home Assistent support, PASSWORD_HASH (inc. Helper), translation updates bugfixes & more."
|
|
||||||
}
|
|
||||||
@@ -24,9 +24,9 @@ Maintainers take the time to improve on this project and help by solving issues
|
|||||||
|
|
||||||
### Filing a Bug Report
|
### Filing a Bug Report
|
||||||
|
|
||||||
Thank you for participating in this project and reporting a bug. wg-easy is a community-driven project, and each contribution counts!
|
Thank you for participating in this project and reporting a bug. `wg-easy` is a community-driven project, and each contribution counts!
|
||||||
|
|
||||||
Maintainers and moderators are volunteers. We greatly appreciate reports that take the time to provide detailed information via the template, enabling us to help you in the best and quickest way. Ignoring the template provided may seem easier, but discourages receiving any support (_via assignment of the label `meta/no template - no support`_).
|
Maintainers and moderators are volunteers. We greatly appreciate reports that take the time to provide detailed information via the template, enabling us to help you in the best and quickest way. Ignoring the template provided may seem easier, but discourages receiving any support.
|
||||||
|
|
||||||
Markdown formatting can be used in almost all text fields (_unless stated otherwise in the description_).
|
Markdown formatting can be used in almost all text fields (_unless stated otherwise in the description_).
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,8 @@
|
|||||||
title: AdGuard Home
|
title: AdGuard Home
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
It seems like the Docs on how to setup AdGuard Home are not available yet.
|
||||||
|
|
||||||
|
Feel free to create a PR and add them here.
|
||||||
|
|
||||||
|
<!-- TODO -->
|
||||||
|
|||||||
@@ -6,11 +6,49 @@ title: Auto Updates
|
|||||||
|
|
||||||
With Docker Compose `wg-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.
|
```shell
|
||||||
|
cd /etc/docker/containers/wg-easy
|
||||||
|
sudo docker compose up -d --pull always
|
||||||
|
```
|
||||||
|
|
||||||
|
### Watchtower
|
||||||
|
|
||||||
|
If you want the updates to be fully automatic you can install Watchtower. This will check for updates every day at 4:00 AM and update the container if a new version is available.
|
||||||
|
|
||||||
|
File: `/etc/docker/containers/watchtower/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
env_file:
|
||||||
|
- watchtower.env
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
File: `/etc/docker/containers/watchtower/watchtower.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
WATCHTOWER_CLEANUP=true
|
||||||
|
WATCHTOWER_SCHEDULE=0 0 4 * * *
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
|
||||||
|
# Email
|
||||||
|
# WATCHTOWER_NOTIFICATIONS_LEVEL=info
|
||||||
|
# WATCHTOWER_NOTIFICATIONS=email
|
||||||
|
# WATCHTOWER_NOTIFICATION_EMAIL_FROM=mail@example.com
|
||||||
|
# WATCHTOWER_NOTIFICATION_EMAIL_TO=mail@example.com
|
||||||
|
# WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.example.com
|
||||||
|
# WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=mail@example.com
|
||||||
|
# WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD="SuperSecurePassword"
|
||||||
|
# WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
|
||||||
|
```
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd $DIR
|
cd /etc/docker/containers/watchtower
|
||||||
sudo docker compose up -d --pull always
|
sudo docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Run
|
## Docker Run
|
||||||
|
|||||||
@@ -20,20 +20,20 @@ Follow the Docs here: <https://docs.docker.com/engine/install/> and install Dock
|
|||||||
1. Create a directory for the configuration files (you can choose any directory you like):
|
1. Create a directory for the configuration files (you can choose any directory you like):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
DIR=/docker/wg-easy
|
sudo mkdir -p /etc/docker/containers/wg-easy
|
||||||
sudo mkdir -p $DIR
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Download docker compose file
|
2. Download docker compose file
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo curl -o $DIR/docker-compose.yml https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml
|
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start `wg-easy`
|
3. Start `wg-easy`
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo docker-compose -f $DIR/docker-compose.yml up -d
|
cd /etc/docker/containers/wg-easy
|
||||||
|
sudo docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Setup Firewall
|
## Setup Firewall
|
||||||
@@ -41,27 +41,22 @@ Follow the Docs here: <https://docs.docker.com/engine/install/> and install Dock
|
|||||||
If you are using a firewall, you need to open the following ports:
|
If you are using a firewall, you need to open the following ports:
|
||||||
|
|
||||||
- UDP 51820 (WireGuard)
|
- UDP 51820 (WireGuard)
|
||||||
- TCP 51821 (Web UI)
|
|
||||||
|
|
||||||
These ports can be changed, so if you change them you have to update your firewall rules accordingly.
|
These ports can be changed, so if you change them you have to update your firewall rules accordingly.
|
||||||
|
|
||||||
## Setup Reverse Proxy
|
## Setup Reverse Proxy
|
||||||
|
|
||||||
TODO
|
- To setup traefik follow the instructions here: [Traefik](./traefik.md)
|
||||||
|
- To setup caddy follow the instructions here: [Caddy](./caddy.md)
|
||||||
## Access the Web UI
|
|
||||||
|
|
||||||
Open your browser and navigate to `https://<your-domain>:51821` or `https://<your-ip>:51821`.
|
|
||||||
|
|
||||||
Follow the instructions to set up your WireGuard VPN.
|
|
||||||
|
|
||||||
## Update `wg-easy`
|
## Update `wg-easy`
|
||||||
|
|
||||||
To update `wg-easy` to the latest version, run:
|
To update `wg-easy` to the latest version, run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo docker-compose -f $DIR/docker-compose.yml pull
|
cd /etc/docker/containers/wg-easy
|
||||||
sudo docker-compose -f $DIR/docker-compose.yml up -d
|
sudo docker-compose pull
|
||||||
|
sudo docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Auto Update
|
## Auto Update
|
||||||
|
|||||||
@@ -2,4 +2,8 @@
|
|||||||
title: Caddy
|
title: Caddy
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
It seems like the Docs on how to setup Caddy are not available yet.
|
||||||
|
|
||||||
|
Feel free to create a PR and add them here.
|
||||||
|
|
||||||
|
<!-- TODO -->
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d \
|
docker run -d \
|
||||||
@@ -38,6 +38,4 @@ To automatically install & run ``wg-easy, simply run:
|
|||||||
ghcr.io/wg-easy/wg-easy:15
|
ghcr.io/wg-easy/wg-easy:15
|
||||||
```
|
```
|
||||||
|
|
||||||
The Web UI will now be available on `http://0.0.0.0:51821`.
|
The Web UI will now be available at <http://0.0.0.0:51821>.
|
||||||
|
|
||||||
> 💡 Your configuration files will be saved in `~/.wg-easy`
|
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
title: Without Docker
|
title: Without Docker
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
This is currently not yet supported.
|
||||||
|
|
||||||
|
<!-- TODO -->
|
||||||
|
|||||||
+2
-7
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Podman
|
title: Podman + nftables
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide will show you how to run `wg-easy` with rootful Podman and nftables.
|
This guide will show you how to run `wg-easy` with rootful Podman and nftables.
|
||||||
@@ -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;
|
nft add table inet wg_table; nft add chain inet wg_table prerouting { type nat hook prerouting priority 100 \; }; 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 accept \; }; 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 accept \; }; nft add rule inet wg_table forward iifname "wg0" accept; nft add rule inet wg_table forward oifname "wg0" accept;
|
||||||
```
|
```
|
||||||
|
|
||||||
2. PostDown
|
2. PostDown
|
||||||
@@ -106,8 +106,3 @@ Restart the container to apply the new hooks:
|
|||||||
```shell
|
```shell
|
||||||
sudo systemctl restart wg-easy
|
sudo systemctl restart wg-easy
|
||||||
```
|
```
|
||||||
|
|
||||||
<!--
|
|
||||||
TODO: improve docs after better nftables support
|
|
||||||
TODO: fix accept web ui port
|
|
||||||
-->
|
|
||||||
@@ -2,4 +2,183 @@
|
|||||||
title: Traefik
|
title: Traefik
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
/// note | Opinionated
|
||||||
|
|
||||||
|
This guide is opinionated. If you use other conventions or folder layouts, feel free to change the commands and paths.
|
||||||
|
///
|
||||||
|
|
||||||
|
## Create docker compose project
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo mkdir -p /etc/docker/containers/traefik
|
||||||
|
cd /etc/docker/containers/traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create docker compose file
|
||||||
|
|
||||||
|
File: `/etc/docker/containers/traefik/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:3.3
|
||||||
|
container_name: traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443/tcp"
|
||||||
|
- "443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /etc/docker/volumes/traefik/traefik.yml:/traefik.yml:ro
|
||||||
|
- /etc/docker/volumes/traefik/traefik_dynamic.yml:/traefik_dynamic.yml:ro
|
||||||
|
- /etc/docker/volumes/traefik/acme.json:/acme.json
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create traefik.yml
|
||||||
|
|
||||||
|
File: `/etc/docker/volumes/traefik/traefik.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80/tcp"
|
||||||
|
http:
|
||||||
|
redirections:
|
||||||
|
entryPoint:
|
||||||
|
to: websecure
|
||||||
|
scheme: https
|
||||||
|
websecure:
|
||||||
|
address: ":443/tcp"
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
- compress@file
|
||||||
|
- hsts@file
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
http3: {}
|
||||||
|
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: $mail@example.com$
|
||||||
|
storage: acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
watch: true
|
||||||
|
network: traefik
|
||||||
|
exposedByDefault: false
|
||||||
|
file:
|
||||||
|
filename: traefik_dynamic.yml
|
||||||
|
|
||||||
|
serversTransport:
|
||||||
|
insecureSkipVerify: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create traefik_dynamic.yml
|
||||||
|
|
||||||
|
File: `/etc/docker/volumes/traefik/traefik_dynamic.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
services:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- "$username$:$password$"
|
||||||
|
compress:
|
||||||
|
compress: {}
|
||||||
|
hsts:
|
||||||
|
headers:
|
||||||
|
stsSeconds: 2592000
|
||||||
|
routers:
|
||||||
|
api:
|
||||||
|
rule: Host(`traefik.$example.com$`)
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- services
|
||||||
|
service: api@internal
|
||||||
|
|
||||||
|
tls:
|
||||||
|
options:
|
||||||
|
default:
|
||||||
|
cipherSuites:
|
||||||
|
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||||
|
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||||
|
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
||||||
|
sniStrict: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create acme.json
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo touch /etc/docker/volumes/traefik/acme.json
|
||||||
|
sudo chmod 600 /etc/docker/volumes/traefik/acme.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create network
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo docker network create traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start traefik
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
You can no access the Traefik dashboard at `https://traefik.$example.com$` with the credentials you set in `traefik_dynamic.yml`.
|
||||||
|
|
||||||
|
## Add Labels to `wg-easy`
|
||||||
|
|
||||||
|
To add labels to your `wg-easy` service, you can add the following to your `docker-compose.yml` file:
|
||||||
|
|
||||||
|
File: `/etc/docker/containers/wg-easy/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
wg-easy:
|
||||||
|
...
|
||||||
|
container_name: wg-easy
|
||||||
|
networks:
|
||||||
|
...
|
||||||
|
traefik: {}
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.wg-easy.rule=Host(`wg-easy.$example.com$`)"
|
||||||
|
- "traefik.http.routers.wg-easy.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.wg-easy.service=wg-easy"
|
||||||
|
- "traefik.http.services.wg-easy.loadbalancer.server.port=51821"
|
||||||
|
...
|
||||||
|
|
||||||
|
networks:
|
||||||
|
...
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Restart `wg-easy`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd /etc/docker/containers/wg-easy
|
||||||
|
sudo docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now access `wg-easy` at `https://wg-easy.$example.com$` and start the setup.
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: FAQ
|
||||||
|
hide:
|
||||||
|
- navigation
|
||||||
|
---
|
||||||
|
|
||||||
|
Here are some frequently asked questions or errors about `wg-easy`. If you have a question that is not answered here, please feel free to open a discussion on GitHub.
|
||||||
|
|
||||||
|
## Error: WireGuard exited with the error: Cannot find device "wg0"
|
||||||
|
|
||||||
|
This error indicates that the WireGuard interface `wg0` does not exist. This can happen if the WireGuard kernel module is not loaded or if the interface was not created properly.
|
||||||
|
|
||||||
|
To resolve this issue, you can try the following steps:
|
||||||
|
|
||||||
|
1. **Load the WireGuard kernel module**: If the WireGuard kernel module is not loaded, you can load it manually by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo modprobe wireguard
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load the WireGuard kernel module on boot**: If you want to ensure that the WireGuard kernel module is loaded automatically on boot, you can add it to the `/etc/modules` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "wireguard" | sudo tee -a /etc/modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## can't initialize iptables table `nat': Table does not exist (do you need to insmod?)
|
||||||
|
|
||||||
|
This error indicates that the `nat` table in `iptables` does not exist. This can happen if the `iptables` kernel module is not loaded or if the `nat` table is not supported by your kernel.
|
||||||
|
|
||||||
|
To resolve this issue, you can try the following steps:
|
||||||
|
|
||||||
|
1. **Load the `nat` kernel module**: If the `nat` kernel module is not loaded, you can load it manually by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo modprobe iptable_nat
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load the `nat` kernel module on boot**: If you want to ensure that the `nat` kernel module is loaded automatically on boot, you can add it to the `/etc/modules` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "iptable_nat" | sudo tee -a /etc/modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## can't initialize ip6tables table `nat': Table does not exist (do you need to insmod?)
|
||||||
|
|
||||||
|
This error indicates that the `nat` table in `ip6tables` does not exist. This can happen if the `ip6tables` kernel module is not loaded or if the `nat` table is not supported by your kernel.
|
||||||
|
|
||||||
|
To resolve this issue, you can try the following steps:
|
||||||
|
|
||||||
|
1. **Load the `nat` kernel module**: If the `nat` kernel module is not loaded, you can load it manually by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo modprobe ip6table_nat
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load the `nat` kernel module on boot**: If you want to ensure that the `nat` kernel module is loaded automatically on boot, you can add it to the `/etc/modules` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "ip6table_nat" | sudo tee -a /etc/modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## can't initialize iptables table `filter': Permission denied
|
||||||
|
|
||||||
|
This error indicates that the `filter` table in `iptables` cannot be initialized due to permission issues. This can happen if you are not running the command with sufficient privileges.
|
||||||
|
|
||||||
|
To resolve this issue, you can try the following steps:
|
||||||
|
|
||||||
|
1. **Load the `filter` kernel module**: If the `filter` kernel module is not loaded, you can load it manually by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo modprobe iptable_filter
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load the `filter` kernel module on boot**: If you want to ensure that the `filter` kernel module is loaded automatically on boot, you can add it to the `/etc/modules` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "iptable_filter" | sudo tee -a /etc/modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## can't initialize ip6tables table `filter': Permission denied
|
||||||
|
|
||||||
|
This error indicates that the `filter` table in `ip6tables` cannot be initialized due to permission issues. This can happen if you are not running the command with sufficient privileges.
|
||||||
|
|
||||||
|
To resolve this issue, you can try the following steps:
|
||||||
|
|
||||||
|
1. **Load the `filter` kernel module**: If the `filter` kernel module is not loaded, you can load it manually by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo modprobe ip6table_filter
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load the `filter` kernel module on boot**: If you want to ensure that the `filter` kernel module is loaded automatically on boot, you can add it to the `/etc/modules` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "ip6table_filter" | sudo tee -a /etc/modules
|
||||||
|
```
|
||||||
@@ -4,7 +4,7 @@ hide:
|
|||||||
- navigation
|
- navigation
|
||||||
---
|
---
|
||||||
|
|
||||||
This page explains how to get started with wg-easy. The guide uses Docker Compose as a reference. In our examples, we mount the named volume `etc_wireguard` to `/etc/wireguard` inside the container.
|
This page explains how to get started with `wg-easy`. The guide uses Docker Compose as a reference. In our examples, we mount the named volume `etc_wireguard` to `/etc/wireguard` inside the container.
|
||||||
|
|
||||||
## Preliminary Steps
|
## Preliminary Steps
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ If you're using podman, make sure to read the related [documentation][docs-podma
|
|||||||
[docker-compose]: https://docs.docker.com/compose/
|
[docker-compose]: https://docs.docker.com/compose/
|
||||||
[docker-compose-installation]: https://docs.docker.com/compose/install/
|
[docker-compose-installation]: https://docs.docker.com/compose/install/
|
||||||
[docker-compose-specification]: https://docs.docker.com/compose/compose-file/
|
[docker-compose-specification]: https://docs.docker.com/compose/compose-file/
|
||||||
[docs-podman]: ./examples/tutorials/podman.md
|
[docs-podman]: ./examples/tutorials/podman-nft.md
|
||||||
|
|
||||||
## Deploying the Actual Image
|
## Deploying the Actual Image
|
||||||
|
|
||||||
@@ -41,10 +41,14 @@ To understand which tags you should use, read this section carefully. [Our CI][g
|
|||||||
|
|
||||||
All workflows are using the tagging convention listed below. It is subsequently applied to all images.
|
All workflows are using the tagging convention listed below. It is subsequently applied to all images.
|
||||||
|
|
||||||
| Event | Image Tags |
|
| tag | Type | Example | Description |
|
||||||
| ----------------------- | ----------------------------- |
|
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `cron` on `master` | `nightly` |
|
| `15` | latest minor for that major tag | `ghcr.io/wg-easy/wg-easy:15` | latest features for specific major versions, no breaking changes |
|
||||||
| `push` a tag (`v1.2.3`) | `1.2.3`, `1.2`, `1`, `latest` |
|
| `latest` | latest tag | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possible get bug fixes quickly when needed, see Releases for more information. |
|
||||||
|
| `15.0` | latest patch for that minor tag | `ghcr.io/wg-easy/wg-easy:15.0` | latest patches for specific minor version |
|
||||||
|
| `15.0.0` | specific tag | `ghcr.io/wg-easy/wg-easy:15.0.0` | specific release, don't use this as this will not get updated |
|
||||||
|
| `nightly` | [`master`](https://github.com/wg-easy/wg-easy/tree/master) | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against [`master`](https://github.com/wg-easy/wg-easy/tree/master). |
|
||||||
|
| `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into [`master`](https://github.com/wg-easy/wg-easy/tree/master). |
|
||||||
|
|
||||||
When publishing a tag we follow the [Semantic Versioning][semver] specification. The `latest` tag is always pointing to the latest stable release. If you want to avoid breaking changes, use the major version tag (e.g. `15`).
|
When publishing a tag we follow the [Semantic Versioning][semver] specification. The `latest` tag is always pointing to the latest stable release. If you want to avoid breaking changes, use the major version tag (e.g. `15`).
|
||||||
|
|
||||||
@@ -80,7 +84,7 @@ To stop the container, issue the following command:
|
|||||||
sudo docker compose down
|
sudo docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
/// danger | Using the Correct Commands For Stopping and Starting wg-easy
|
/// danger | Using the Correct Commands For Stopping and Starting `wg-easy`
|
||||||
|
|
||||||
**Use `sudo docker compose up / down`, not `sudo docker compose start / stop`**. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.
|
**Use `sudo docker compose up / down`, not `sudo docker compose start / stop`**. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: NGINX
|
title: 2FA
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Edit Account
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Admin Panel
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Edit Client
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Login
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Setup
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -11,7 +11,7 @@ hide:
|
|||||||
**Make sure** to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the `:latest` image tag - [the most recent stable release][docs-tagging].
|
**Make sure** to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the `:latest` image tag - [the most recent stable release][docs-tagging].
|
||||||
///
|
///
|
||||||
|
|
||||||
This documentation provides you not only with the basic setup and configuration of wg-easy but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.
|
This documentation provides you not only with the basic setup and configuration of `wg-easy` but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.
|
||||||
|
|
||||||
[docs-tagging]: ./getting-started.md#tagging-convention
|
[docs-tagging]: ./getting-started.md#tagging-convention
|
||||||
|
|
||||||
|
|||||||
+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.8.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
|
||||||
>
|
>
|
||||||
|
<button type="button" @click="open = !open">
|
||||||
<slot />
|
<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>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
|
<div class="bg-white">
|
||||||
<img :src="qrCode" />
|
<img :src="qrCode" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<DialogClose>
|
<DialogClose>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
v-model.trim="data"
|
v-model.trim="data"
|
||||||
:name="id"
|
:name="id"
|
||||||
type="text"
|
type="text"
|
||||||
:autcomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
v-model.trim="data"
|
v-model.trim="data"
|
||||||
:name="id"
|
:name="id"
|
||||||
type="text"
|
type="text"
|
||||||
:autcomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
|
:disabled="disabled"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ defineProps<{
|
|||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const data = defineModel<string>();
|
const data = defineModel<string>();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,21 +1,42 @@
|
|||||||
import type { NitroFetchRequest, NitroFetchOptions } from 'nitropack/types';
|
import type {
|
||||||
|
NitroFetchRequest,
|
||||||
|
NitroFetchOptions,
|
||||||
|
TypedInternalResponse,
|
||||||
|
ExtractedRouteMethod,
|
||||||
|
} from 'nitropack/types';
|
||||||
import { FetchError } from 'ofetch';
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
type RevertFn = (success: boolean) => Promise<void>;
|
type RevertFn<
|
||||||
|
R extends NitroFetchRequest,
|
||||||
|
T = unknown,
|
||||||
|
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
|
||||||
|
> = (
|
||||||
|
success: boolean,
|
||||||
|
data:
|
||||||
|
| TypedInternalResponse<
|
||||||
|
R,
|
||||||
|
T,
|
||||||
|
NitroFetchOptions<R> extends O ? 'get' : ExtractedRouteMethod<R, O>
|
||||||
|
>
|
||||||
|
| undefined
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
type SubmitOpts = {
|
type SubmitOpts<
|
||||||
revert: RevertFn;
|
R extends NitroFetchRequest,
|
||||||
|
T = unknown,
|
||||||
|
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
|
||||||
|
> = {
|
||||||
|
revert: RevertFn<R, T, O>;
|
||||||
successMsg?: string;
|
successMsg?: string;
|
||||||
errorMsg?: string;
|
|
||||||
noSuccessToast?: boolean;
|
noSuccessToast?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useSubmit<
|
export function useSubmit<
|
||||||
R extends NitroFetchRequest,
|
R extends NitroFetchRequest,
|
||||||
O extends NitroFetchOptions<R> & { body?: never },
|
O extends NitroFetchOptions<R> & { body?: never },
|
||||||
>(url: R, options: O, opts: SubmitOpts) {
|
T = unknown,
|
||||||
|
>(url: R, options: O, opts: SubmitOpts<R, T, O>) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { t: $t } = useI18n();
|
|
||||||
|
|
||||||
return async (data: unknown) => {
|
return async (data: unknown) => {
|
||||||
try {
|
try {
|
||||||
@@ -24,11 +45,6 @@ export function useSubmit<
|
|||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
if (!(res as any).success) {
|
|
||||||
throw new Error(opts.errorMsg || $t('toast.errored'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.noSuccessToast) {
|
if (!opts.noSuccessToast) {
|
||||||
toast.showToast({
|
toast.showToast({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -36,7 +52,8 @@ export function useSubmit<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await opts.revert(true);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await opts.revert(true, res as any);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof FetchError) {
|
if (e instanceof FetchError) {
|
||||||
toast.showToast({
|
toast.showToast({
|
||||||
@@ -51,7 +68,7 @@ export function useSubmit<
|
|||||||
} else {
|
} else {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
await opts.revert(false);
|
await opts.revert(false, undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('general.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>
|
||||||
@@ -75,11 +86,25 @@ const _changeCidr = useSubmit(
|
|||||||
{
|
{
|
||||||
revert,
|
revert,
|
||||||
successMsg: t('admin.interface.cidrSuccess'),
|
successMsg: t('admin.interface.cidrSuccess'),
|
||||||
errorMsg: t('admin.interface.cidrError'),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
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'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function restartInterface() {
|
||||||
|
await _restartInterface(undefined);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+47
-6
@@ -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"
|
||||||
@@ -29,6 +30,18 @@
|
|||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BaseInput
|
||||||
|
v-if="totpRequired"
|
||||||
|
v-model="totp"
|
||||||
|
type="text"
|
||||||
|
name="totp"
|
||||||
|
:placeholder="$t('general.2faCode')"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="6"
|
||||||
|
pattern="\d{6}"
|
||||||
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
class="flex gap-2 whitespace-nowrap"
|
class="flex gap-2 whitespace-nowrap"
|
||||||
:title="$t('login.rememberMeDesc')"
|
:title="$t('login.rememberMeDesc')"
|
||||||
@@ -54,10 +67,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
authStore.update();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
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<string>('');
|
||||||
const password = ref<null | string>(null);
|
const password = ref<string>('');
|
||||||
|
const totpRequired = ref(false);
|
||||||
|
const totp = ref<string>('');
|
||||||
|
|
||||||
const _submit = useSubmit(
|
const _submit = useSubmit(
|
||||||
'/api/session',
|
'/api/session',
|
||||||
@@ -65,13 +86,32 @@ const _submit = useSubmit(
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
revert: async (success) => {
|
revert: async (success, data) => {
|
||||||
authenticating.value = false;
|
|
||||||
password.value = null;
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
if (data?.status === 'success') {
|
||||||
await navigateTo('/');
|
await navigateTo('/');
|
||||||
|
} else if (data?.status === 'TOTP_REQUIRED') {
|
||||||
|
authenticating.value = false;
|
||||||
|
totpRequired.value = true;
|
||||||
|
toast.showToast({
|
||||||
|
title: t('general.2fa'),
|
||||||
|
message: t('login.2faRequired'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (data?.status === 'INVALID_TOTP_CODE') {
|
||||||
|
authenticating.value = false;
|
||||||
|
totp.value = '';
|
||||||
|
toast.showToast({
|
||||||
|
title: t('general.2fa'),
|
||||||
|
message: t('login.2faWrong'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
authenticating.value = false;
|
||||||
|
password.value = '';
|
||||||
},
|
},
|
||||||
noSuccessToast: true,
|
noSuccessToast: true,
|
||||||
}
|
}
|
||||||
@@ -86,6 +126,7 @@ async function submit() {
|
|||||||
username: username.value,
|
username: username.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
remember: remember.value,
|
remember: remember.value,
|
||||||
|
totpCode: totpRequired.value ? totp.value : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+140
-1
@@ -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"
|
||||||
@@ -48,12 +48,74 @@
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FormElement>
|
</FormElement>
|
||||||
|
<FormElement @submit.prevent>
|
||||||
|
<FormGroup>
|
||||||
|
<FormHeading>{{ $t('general.2fa') }}</FormHeading>
|
||||||
|
<div
|
||||||
|
v-if="!authStore.userData?.totpVerified && !twofa"
|
||||||
|
class="col-span-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<FormActionField :label="$t('me.enable2fa')" @click="setup2fa" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!authStore.userData?.totpVerified && twofa"
|
||||||
|
class="col-span-2"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $t('me.enable2faDesc') }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-2 flex flex-col gap-2">
|
||||||
|
<img :src="twofa.qrcode" size="128" class="bg-white" />
|
||||||
|
<FormTextField
|
||||||
|
id="2fakey"
|
||||||
|
:model-value="twofa.key"
|
||||||
|
:on-update:model-value="() => {}"
|
||||||
|
:label="$t('me.2faKey')"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $t('me.2faCodeDesc') }}
|
||||||
|
</p>
|
||||||
|
<FormTextField
|
||||||
|
id="2facode"
|
||||||
|
v-model="code"
|
||||||
|
:label="$t('general.2faCode')"
|
||||||
|
/>
|
||||||
|
<FormActionField
|
||||||
|
:label="$t('me.enable2fa')"
|
||||||
|
@click="enable2fa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="authStore.userData?.totpVerified"
|
||||||
|
class="col-span-2 flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $t('me.disable2faDesc') }}
|
||||||
|
</p>
|
||||||
|
<FormPasswordField
|
||||||
|
id="2fapassword"
|
||||||
|
v-model="disable2faPassword"
|
||||||
|
:label="$t('me.currentPassword')"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
<FormActionField
|
||||||
|
:label="$t('me.disable2fa')"
|
||||||
|
@click="disable2fa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</FormElement>
|
||||||
</PanelBody>
|
</PanelBody>
|
||||||
</Panel>
|
</Panel>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { encodeQR } from 'qr';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
authStore.update();
|
authStore.update();
|
||||||
|
|
||||||
@@ -101,4 +163,81 @@ function updatePassword() {
|
|||||||
confirmPassword: confirmPassword.value,
|
confirmPassword: confirmPassword.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const twofa = ref<{ key: string; qrcode: string } | null>(null);
|
||||||
|
|
||||||
|
const _setup2fa = useSubmit(
|
||||||
|
`/api/me/totp`,
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revert: async (success, data) => {
|
||||||
|
if (success && data?.type === 'setup') {
|
||||||
|
const qrcode = encodeQR(data.uri, 'svg', {
|
||||||
|
ecc: 'high',
|
||||||
|
scale: 4,
|
||||||
|
encoding: 'byte',
|
||||||
|
});
|
||||||
|
const svg = new Blob([qrcode], { type: 'image/svg+xml' });
|
||||||
|
twofa.value = { key: data.key, qrcode: URL.createObjectURL(svg) };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function setup2fa() {
|
||||||
|
return _setup2fa({
|
||||||
|
type: 'setup',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = ref<string>('');
|
||||||
|
|
||||||
|
const _enable2fa = useSubmit(
|
||||||
|
`/api/me/totp`,
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revert: async (success, data) => {
|
||||||
|
if (success && data?.type === 'created') {
|
||||||
|
authStore.update();
|
||||||
|
twofa.value = null;
|
||||||
|
code.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function enable2fa() {
|
||||||
|
return _enable2fa({
|
||||||
|
type: 'create',
|
||||||
|
code: code.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const disable2faPassword = ref('');
|
||||||
|
|
||||||
|
const _disable2fa = useSubmit(
|
||||||
|
`/api/me/totp`,
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revert: async (success, data) => {
|
||||||
|
if (success && data?.type === 'deleted') {
|
||||||
|
authStore.update();
|
||||||
|
disable2faPassword.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function disable2fa() {
|
||||||
|
return _disable2fa({
|
||||||
|
type: 'delete',
|
||||||
|
currentPassword: disable2faPassword.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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,14 +1,16 @@
|
|||||||
<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>
|
||||||
|
<div class="mt-4">
|
||||||
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton>
|
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+39
-25
@@ -15,7 +15,12 @@
|
|||||||
},
|
},
|
||||||
"me": {
|
"me": {
|
||||||
"currentPassword": "Current Password",
|
"currentPassword": "Current Password",
|
||||||
"confirmPassword": "Confirm Password"
|
"enable2fa": "Enable Two Factor Authentication",
|
||||||
|
"enable2faDesc": "Scan the QR code with your authenticator app or enter the key manually.",
|
||||||
|
"2faKey": "TOTP Key",
|
||||||
|
"2faCodeDesc": "Enter the code from your authenticator app.",
|
||||||
|
"disable2fa": "Disable Two Factor Authentication",
|
||||||
|
"disable2faDesc": "Enter your password to disable Two Factor Authentication."
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -32,19 +37,25 @@
|
|||||||
"host": "Host",
|
"host": "Host",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No"
|
"no": "No",
|
||||||
|
"confirmPassword": "Confirm Password",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"2fa": "Two Factor Authentication",
|
||||||
|
"2faCode": "TOTP Code"
|
||||||
},
|
},
|
||||||
"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!",
|
||||||
@@ -62,11 +73,10 @@
|
|||||||
"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": {
|
"2faRequired": "Two Factor Authentication is required",
|
||||||
"clear": "Clear",
|
"2faWrong": "Two Factor Authentication is wrong"
|
||||||
"login": "Log in error"
|
|
||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"empty": "There are no clients yet.",
|
"empty": "There are no clients yet.",
|
||||||
@@ -113,8 +123,7 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"error": "Error",
|
"error": "Error"
|
||||||
"errored": "Failed to save"
|
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
@@ -132,7 +141,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",
|
||||||
@@ -141,21 +150,27 @@
|
|||||||
"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 (global config)",
|
"allowedIpsDesc": "Allowed IPs clients will use (global config)",
|
||||||
"dnsDesc": "DNS server clients will use (global config)",
|
"dnsDesc": "DNS server clients will use (global config)",
|
||||||
"mtuDesc": "MTU clients will use (only for new clients)",
|
"mtuDesc": "MTU clients will use (only for new clients)",
|
||||||
"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 (only for new clients)",
|
||||||
|
"suggest": "Suggest",
|
||||||
|
"suggestDesc": "Choose a IP-Address or Hostname for the Host field"
|
||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"cidrSuccess": "Changed CIDR",
|
"cidrSuccess": "Changed CIDR",
|
||||||
"cidrError": "Failed to change CIDR",
|
|
||||||
"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"
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
@@ -178,15 +193,14 @@
|
|||||||
"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",
|
||||||
"emailInvalid": "Email must be a valid email",
|
"emailInvalid": "Email must be a valid email",
|
||||||
"passwordMatch": "Passwords must match"
|
"passwordMatch": "Passwords must match",
|
||||||
|
"totpEnable": "TOTP Enable",
|
||||||
|
"totpEnableTrue": "TOTP Enable must be true",
|
||||||
|
"totpCode": "TOTP Code"
|
||||||
},
|
},
|
||||||
"userConfig": {
|
"userConfig": {
|
||||||
"host": "Host"
|
"host": "Host"
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
+20
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wg-easy",
|
"name": "wg-easy",
|
||||||
"version": "15.0.0-beta.7",
|
"version": "15.0.0-beta.12",
|
||||||
"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,10 +19,12 @@
|
|||||||
},
|
},
|
||||||
"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.3",
|
||||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
"@nuxtjs/i18n": "^9.5.3",
|
||||||
"@pinia/nuxt": "^0.10.1",
|
"@nuxtjs/tailwindcss": "^6.13.2",
|
||||||
|
"@phc/format": "^1.0.0",
|
||||||
|
"@pinia/nuxt": "^0.11.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"argon2": "^0.41.1",
|
"argon2": "^0.41.1",
|
||||||
@@ -30,15 +32,16 @@
|
|||||||
"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.2",
|
||||||
"pinia": "^3.0.1",
|
"otpauth": "^9.4.0",
|
||||||
"qrcode": "^1.5.4",
|
"pinia": "^3.0.2",
|
||||||
|
"qr": "^0.4.0",
|
||||||
"radix-vue": "^1.9.17",
|
"radix-vue": "^1.9.17",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
@@ -48,17 +51,17 @@
|
|||||||
"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/qrcode": "^1.5.5",
|
"@types/phc__format": "^1.0.1",
|
||||||
"@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.24.0",
|
||||||
"eslint-config-prettier": "^10.0.2",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
"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.3",
|
||||||
"vue-tsc": "^2.2.8"
|
"vue-tsc": "^2.2.8"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.5.2"
|
"packageManager": "pnpm@10.8.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2033
-2619
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Secret, TOTP } from 'otpauth';
|
||||||
|
import { UserUpdateTotpSchema } from '#db/repositories/user/types';
|
||||||
|
|
||||||
|
type Response =
|
||||||
|
| {
|
||||||
|
success: boolean;
|
||||||
|
type: 'setup';
|
||||||
|
key: string;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
| { success: boolean; type: 'created' }
|
||||||
|
| { success: boolean; type: 'deleted' };
|
||||||
|
|
||||||
|
export default definePermissionEventHandler(
|
||||||
|
'me',
|
||||||
|
'update',
|
||||||
|
async ({ event, user, checkPermissions }) => {
|
||||||
|
const body = await readValidatedBody(
|
||||||
|
event,
|
||||||
|
validateZod(UserUpdateTotpSchema, event)
|
||||||
|
);
|
||||||
|
|
||||||
|
checkPermissions(user);
|
||||||
|
|
||||||
|
if (body.type === 'setup') {
|
||||||
|
const key = new Secret({ size: 20 });
|
||||||
|
|
||||||
|
const totp = new TOTP({
|
||||||
|
issuer: 'wg-easy',
|
||||||
|
label: user.username,
|
||||||
|
algorithm: 'SHA1',
|
||||||
|
digits: 6,
|
||||||
|
period: 30,
|
||||||
|
secret: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Database.users.updateTotpKey(user.id, key.base32);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
type: 'setup',
|
||||||
|
key: key.base32,
|
||||||
|
uri: totp.toString(),
|
||||||
|
} as Response;
|
||||||
|
} else if (body.type === 'create') {
|
||||||
|
await Database.users.verifyTotp(user.id, body.code);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
type: 'created',
|
||||||
|
} as Response;
|
||||||
|
} else if (body.type === 'delete') {
|
||||||
|
await Database.users.deleteTotpKey(user.id, body.currentPassword);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
type: 'deleted',
|
||||||
|
} as Response;
|
||||||
|
}
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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({
|
||||||
@@ -21,5 +20,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
totpVerified: user.totpVerified,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
import { UserLoginSchema } from '#db/repositories/user/types';
|
import { UserLoginSchema } from '#db/repositories/user/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { username, password, remember } = await readValidatedBody(
|
const { username, password, remember, totpCode } = await readValidatedBody(
|
||||||
event,
|
event,
|
||||||
validateZod(UserLoginSchema, event)
|
validateZod(UserLoginSchema, event)
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: timing can be used to enumerate usernames
|
const result = await Database.users.login(username, password, totpCode);
|
||||||
|
|
||||||
const user = await Database.users.getByUsername(username);
|
// TODO: add localization support
|
||||||
if (!user)
|
|
||||||
|
if (!result.success) {
|
||||||
|
switch (result.error) {
|
||||||
|
case 'INCORRECT_CREDENTIALS':
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
statusMessage: 'Incorrect credentials',
|
statusMessage: 'Invalid username or password',
|
||||||
});
|
});
|
||||||
|
case 'TOTP_REQUIRED':
|
||||||
const userHashPassword = user.password;
|
return { status: 'TOTP_REQUIRED' };
|
||||||
const passwordValid = await isPasswordValid(password, userHashPassword);
|
case 'INVALID_TOTP_CODE':
|
||||||
if (!passwordValid) {
|
return { status: 'INVALID_TOTP_CODE' };
|
||||||
|
case 'USER_DISABLED':
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
statusMessage: 'Incorrect credentials',
|
statusMessage: 'User disabled',
|
||||||
|
});
|
||||||
|
case 'UNEXPECTED_ERROR':
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Unexpected error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
assertUnreachable(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = result.user;
|
||||||
|
|
||||||
const session = await useWGSession(event, remember);
|
const session = await useWGSession(event, remember);
|
||||||
|
|
||||||
@@ -34,5 +47,5 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
SERVER_DEBUG(`New Session: ${data.id} for ${user.id} (${user.username})`);
|
SERVER_DEBUG(`New Session: ${data.id} for ${user.id} (${user.username})`);
|
||||||
|
|
||||||
return { success: true };
|
return { status: 'success' };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default defineSetupEventHandler(4, async () => {
|
||||||
|
const result = await cachedGetIpInformation();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
@@ -17,6 +17,7 @@ CREATE TABLE `clients_table` (
|
|||||||
`persistent_keepalive` integer NOT NULL,
|
`persistent_keepalive` integer NOT NULL,
|
||||||
`mtu` integer NOT NULL,
|
`mtu` integer NOT NULL,
|
||||||
`dns` text,
|
`dns` text,
|
||||||
|
`server_endpoint` 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,
|
||||||
@@ -80,6 +81,8 @@ CREATE TABLE `users_table` (
|
|||||||
`email` text,
|
`email` text,
|
||||||
`name` text NOT NULL,
|
`name` text NOT NULL,
|
||||||
`role` integer NOT NULL,
|
`role` integer NOT NULL,
|
||||||
|
`totp_key` text,
|
||||||
|
`totp_verified` integer NOT NULL,
|
||||||
`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": "8c2af02b-c4bd-4880-a9ad-b38805636208",
|
"id": "b812341a-1ec2-49a6-8bc8-0332f5b32df4",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"clients_table": {
|
"clients_table": {
|
||||||
@@ -137,6 +137,13 @@
|
|||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"server_endpoint": {
|
||||||
|
"name": "server_endpoint",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "enabled",
|
"name": "enabled",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -558,6 +565,20 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"totp_key": {
|
||||||
|
"name": "totp_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"totp_verified": {
|
||||||
|
"name": "totp_verified",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "enabled",
|
"name": "enabled",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "a61263b1-9af1-4d2e-99e9-80d08127b545",
|
"id": "c4c5bfb7-a66c-4e6b-a15c-232b16689dcf",
|
||||||
"prevId": "8c2af02b-c4bd-4880-a9ad-b38805636208",
|
"prevId": "b812341a-1ec2-49a6-8bc8-0332f5b32df4",
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -137,6 +137,13 @@
|
|||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"server_endpoint": {
|
||||||
|
"name": "server_endpoint",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "enabled",
|
"name": "enabled",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -558,6 +565,20 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"totp_key": {
|
||||||
|
"name": "totp_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"totp_verified": {
|
||||||
|
"name": "totp_verified",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "enabled",
|
"name": "enabled",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1741355094140,
|
"when": 1743515334198,
|
||||||
"tag": "0000_short_skin",
|
"tag": "0000_short_skin",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1741355098159,
|
"when": 1743515338707,
|
||||||
"tag": "0001_classy_the_stranger",
|
"tag": "0001_classy_the_stranger",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const client = sqliteTable('clients_table', {
|
|||||||
persistentKeepalive: int('persistent_keepalive').notNull(),
|
persistentKeepalive: int('persistent_keepalive').notNull(),
|
||||||
mtu: int().notNull(),
|
mtu: int().notNull(),
|
||||||
dns: text({ mode: 'json' }).$type<string[]>(),
|
dns: text({ mode: 'json' }).$type<string[]>(),
|
||||||
|
serverEndpoint: text('server_endpoint'),
|
||||||
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,
|
||||||
@@ -132,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({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user