Compare commits

...

41 Commits

Author SHA1 Message Date
Bernd Storath 3ef258a28a Bump version to 15.0.0-beta.12 2025-04-11 23:35:51 +02:00
Bernd Storath ff783fd4d1 Feat: Improve Docs (#1791)
* improve docs

* preplan guides

* fix spelling

* fix nftables rules

* consistent wg-easy code block

* fix grammar
2025-04-11 23:25:58 +02:00
Bernd Storath 65aa067100 update packages 2025-04-11 22:46:03 +02:00
Edgars 48e6949a4d Update docker-run.md (#1804)
Small fixes were made to:

- adjust indentation for the first line in code blocks;
- fix backticks;
- make the URL interactive.
2025-04-09 11:25:30 +02:00
Bernd Storath 9ebf2c1d33 chore: disable latest tag for docker
so old installations wont break until v15 is stable enough
2025-04-02 09:32:52 +02:00
Bernd Storath d0f85316a6 chore: delete changelog from docs 2025-04-02 09:20:18 +02:00
Bernd Storath ff9fd553c5 Fix: sync / rekey issue (#1789)
only sync if needed
2025-04-02 08:49:04 +02:00
Bernd Storath e92ee0464e Feat: Server Endpoint (#1785)
* add server endpoint to client

* be able to update endpoint over api
2025-04-01 16:13:51 +02:00
Bernd Storath 9df049d3f4 Feat: startup info (#1784)
add startup info
2025-04-01 15:18:13 +02:00
Bernd Storath 32b73b850a Feat: 2fa (#1783)
* preplan otp, better qrcode library

* add 2fa as feature

* add totp generation

* working totp lifecycle

* don't allow disabled user to log in

not a security issue as permission handler would fail anyway

* require 2fa on login

if enabled

* update packages

* fix typo

* remove console.logs
2025-04-01 14:43:48 +02:00
Bernd Storath 1c7f64ebd5 Bump version to 15.0.0-beta.11 2025-03-31 10:32:27 +02:00
Bernd Storath 589ec1fe9a Feat: Show insecure warning (#1779)
show insecure warning
2025-03-31 10:29:22 +02:00
Bernd Storath 6e0d758e36 Feat: Hash metrics password (#1778)
hash the metrics password

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

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

* remove char validation altogether

---------

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

* use heroicons

* add host field

* get private info

* unstyled prototype

* styled select

* add to setup

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

* improve setup

* improve mobile view

* move base admin route

* admin panel mobile view

* set initial host and port

* add docs

* properly setup everything, use for dev env

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

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

* check if ip is included on update

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

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

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

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

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

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

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

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

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

* link donate to readme

* implement global config for allowed ips

* change translations, fix generation

* improve docs
2025-03-07 14:59:06 +01:00
Bernd Storath 9fc6ebafb3 Bump version to 15.0.0-beta.6 2025-03-07 12:02:03 +01:00
Bernd Storath 9a029eeb23 improve docs, add version script 2025-03-07 12:02:02 +01:00
Bernd Storath e5fb6ff3a6 Fix: OneTimeLinks (#1719)
* fix otls

* one otl per client

* revert some code

* revert some more code, add comments

* adjust migration
2025-03-07 09:16:24 +01:00
杨黄林 fcb5049dab Add PreUp, PostUp, PreDown, PostDown for client (#1714)
* Fix create client popup background is not white

* Fix no Add button when client Allowed Ips or Server Allowed Ips is empty

* Add preUp preDown postUp postDown for client

* Add description of hooks for client config

* Move hooks's label text into 'hooks' in en.json

---------

Co-authored-by: yanghuanglin <yanghuanglin@qq.com>
Co-authored-by: Bernd Storath <999999bst@gmail.com>
2025-03-07 08:17:33 +01:00
Bernd Storath 93db67bab6 fix: only require metrics password if set (#1715) 2025-03-06 11:45:03 +01:00
Bernd Storath 842475f799 Fix: Cidr Change (#1712)
* only calculate ip if cidr changed

if the cidr did not change, the ip will not change to prevent ip shifts

* fix lint
2025-03-06 10:04:49 +01:00
Bernd Storath f4d3608da7 Fix: Various (#1711)
* fix docs

* fix migration
2025-03-06 08:15:18 +01:00
139 changed files with 4644 additions and 3445 deletions
+5 -2
View File
@@ -6,6 +6,9 @@ on:
tags:
- "v*"
# This workflow does not support fixing old versions
# as this will break the latest and major tags
jobs:
docker:
name: Build & Deploy Docker
@@ -31,6 +34,8 @@ jobs:
with:
images: |
ghcr.io/wg-easy/wg-easy
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
@@ -87,8 +92,6 @@ jobs:
cd docs
git fetch origin gh-pages --depth=1 || true
# latest will point to old docs if old tag is pushed
# Extract version numbers
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
+3
View File
@@ -6,6 +6,9 @@
"nuxtr.vueFiles.style.addStyleTag": false,
"nuxtr.piniaFiles.defaultTemplate": "setup",
"nuxtr.monorepoMode.DirectoryName": "src",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
+8
View File
@@ -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!
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
- 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
- Added Docker Volume Mount (`/lib/modules`)
- 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
+3 -1
View File
@@ -26,7 +26,7 @@ COPY --from=build /app/.output /app
# Copy migrations
COPY --from=build /app/server/database/migrations /app/server/database/migrations
# libsql
RUN npm install --no-save libsql
RUN cd /app/server && npm install --no-save libsql
# Install Linux packages
RUN apk add --no-cache \
@@ -34,6 +34,7 @@ RUN apk add --no-cache \
dumb-init \
iptables \
ip6tables \
nftables \
kmod \
iptables-legacy \
wireguard-tools
@@ -47,6 +48,7 @@ ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821
ENV HOST=0.0.0.0
ENV INSECURE=false
ENV INIT_ENABLED=false
LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy
+2 -1
View File
@@ -26,7 +26,8 @@ RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tab
ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821
ENV HOST=0.0.0.0
ENV INSECURE=false
ENV INSECURE=true
ENV INIT_ENABLED=false
# Install Dependencies
COPY src/package.json src/pnpm-lock.yaml ./
+33 -98
View File
@@ -5,7 +5,7 @@
[![GitHub Stars](https://img.shields.io/github/stars/wg-easy/wg-easy)](https://github.com/wg-easy/wg-easy/stargazers)
[![License](https://img.shields.io/github/license/wg-easy/wg-easy)](LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/wg-easy/wg-easy)](https://github.com/wg-easy/wg-easy/releases/latest)
[![Image Pulls](https://img.shields.io/badge/image_pulls-11M-blue)](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
[![Image Pulls](https://img.shields.io/badge/image_pulls-12M+-blue)](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
<!-- TODO: remove after release -->
@@ -38,35 +38,32 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
- Prometheus metrics support
- IPv6 support
- CIDR support
- 2FA support
> [!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)
<!-- TODO: remove after release -->
> [!WARNING]
> As the Docs are still in Pre-release, you can access them here [https://wg-easy.github.io/wg-easy/Pre-release](https://wg-easy.github.io/wg-easy/Pre-release)
- [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/)
- [Caddy](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/caddy/)
- [Traefik](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/traefik/)
- [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/)
## 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). |
> [!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/)
## 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
If you haven't installed Docker yet, install it by running as root:
@@ -82,59 +79,13 @@ And log in again.
The easiest way to run WireGuard Easy is with Docker Compose.
Just download [`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
execute `sudo docker compose up -d`.
Just download [`docker-compose.yml`](docker-compose.yml) and execute `sudo docker compose up -d`.
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
<!-- TOOD: add to docs: Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) -->
<!-- TOOD: add to docs
To setup the IPv6 Network, simply run once:
```bash
docker network create \
-d bridge --ipv6 \
-d default \
--subnet 10.42.42.0/24 \
--subnet fdcc:ad94:bacf:61a3::/64 wg \
```
To automatically install & run wg-easy, simply run:
```bash
docker run -d \
--net wg \
-e PORT=51821 \
--name wg-easy \
--ip6 fdcc:ad94:bacf:61a3::2a \
--ip 10.42.42.42 \
-v ~/.wg-easy:/etc/wireguard \
-v /lib/modules:/lib/modules:ro \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add NET_ADMIN \
--cap-add SYS_MODULE \
--sysctl net.ipv4.ip_forward=1 \
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
--sysctl net.ipv6.conf.all.forwarding=1 \
--sysctl net.ipv6.conf.default.forwarding=1 \
--restart unless-stopped \
ghcr.io/wg-easy/wg-easy
```
The Web UI will now be available on `http://0.0.0.0:51821`.
The Prometheus metrics will now be available on `http://0.0.0.0:51821/api/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/)
> 💡 Your configuration files will be saved in `~/.wg-easy`
-->
### 3. Sponsor
## Donate
Are you enjoying this project? Consider donating.
@@ -142,46 +93,30 @@ Founder: [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
Maintainer: [Buy kaaax0815 a coffee!](https://github.com/sponsors/kaaax0815) ☕
<!-- TOOD: add to docs
## Development
## Options
### Prerequisites
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
- Docker
- Node LTS & corepack enabled
- Visual Studio Code
| Env | Default | Example | Description |
| ---------- | --------- | ----------- | ------------------------------ |
| `PORT`. | `51821` | `6789` | TCP port for Web UI. |
| `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `INSECURE` | `false` | `true` | If access over http is allowed |
### Dev Server
## Updating
To update to the latest version, simply run:
This starts the development server with docker
```shell
docker stop wg-easy
docker rm wg-easy
docker pull ghcr.io/wg-easy/wg-easy
pnpm dev
```
And then run the `docker run -d \ ...` command above again.
### Update Auto Imports
With Docker Compose WireGuard Easy can be updated with a single command:
`docker compose up --detach --pull always` (if an image tag is specified in the
Compose file and it is not `latest`, make sure that it is changed to the desired
one; by default it is omitted and
[defaults to `latest`](https://docs.docker.com/engine/reference/run/#image-references)). \
The WireGuard Easy container will be automatically recreated if a newer image
was pulled.
If you add something that should be auto-importable and VSCode complains, run:
## Common Use Cases
- [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole)
- [Using WireGuard-Easy with nginx/SSL](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-nginx-SSL)
For less common or specific edge-case scenarios, please refer to the detailed information provided in the [Wiki](https://github.com/wg-easy/wg-easy/wiki).
-->
```shell
cd src
pnpm install
```
## License
+6
View File
@@ -15,6 +15,12 @@ services:
cap_add:
- NET_ADMIN
- 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
volumes:
+2 -2
View File
@@ -9,7 +9,7 @@ services:
# - HOST=0.0.0.0
# - INSECURE=false
image: ghcr.io/wg-easy/wg-easy
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
networks:
wg:
@@ -25,7 +25,7 @@ services:
cap_add:
- NET_ADMIN
- SYS_MODULE
# - NET_RAW # ⚠️ Uncomment if using Podman
# - NET_RAW # ⚠️ Uncomment if using Podman Compose
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
+34 -1
View File
@@ -2,4 +2,37 @@
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,4 +2,10 @@
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 |
| ---------- | --------- | ----------- | ------------------------------ |
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `INSECURE` | `false` | `true` | If access over http is allowed |
@@ -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.
///
@@ -0,0 +1,42 @@
---
title: Prometheus
---
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.
## 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:
[![Grafana Dashboard](https://grafana.com/api/dashboards/21733/images/16863/image)](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.
///
@@ -6,7 +6,9 @@ This guide will help you migrate from `v14` to version `v15` of `wg-easy`.
## Changes
This is a complete rewrite of the `wg-easy` project. Therefore the configuration files and the way you interact with the project have changed.
- This is a complete rewrite of the `wg-easy` project. Therefore the configuration files and the way you interact with the project have changed.
- If you use armv6 or armv7, you can't migrate to `v15` yet. We are working on it.
- If you are connecting to the web ui via HTTP, you need to set the `INSECURE` environment variable to `true` in the new container.
## Migration
@@ -38,10 +40,13 @@ docker-compose down
### Start new container
Follow the instructions in the [Getting Started](../../usage.md) or [Basic Installation](../../examples/tutorials/basic-installation.md) guide to start the new container.
Follow the instructions in the [Getting Started][docs-getting-started] or [Basic Installation][docs-examples] guide to start the new container.
In the setup wizard, select that you already already have a configuration file and upload the `wg0.json` file you downloaded in the backup step.
[docs-getting-started]: ../../getting-started.md
[docs-examples]: ../../examples/tutorials/basic-installation.md
### Done
You have now successfully migrated to `v15` of `wg-easy`.
+7
View File
@@ -0,0 +1,7 @@
---
title: Migrate
---
If you want to migrate from an older version of `wg-easy` to the new version, you can find the migration guides listed below.
- [Migrate from v14 to v15](./from-14-to-15.md) : This guide should also work for any version before `v14`.
-16
View File
@@ -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
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_).
@@ -0,0 +1,9 @@
---
title: AdGuard Home
---
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 -->
@@ -0,0 +1,72 @@
---
title: Auto Updates
---
## Docker Compose
With Docker Compose `wg-easy` can be updated with a single command:
```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
cd /etc/docker/containers/watchtower
sudo docker compose up -d
```
## Docker Run
```shell
sudo docker stop wg-easy
sudo docker rm wg-easy
sudo docker pull ghcr.io/wg-easy/wg-easy
```
And then run the `docker run -d \ ...` command from [Docker Run][docker-run] again.
[docker-run]: ./docker-run.md
## Podman
To update `wg-easy` (and every container that has auto updates enabled), you can run the following command:
```shell
sudo podman auto-update
```
@@ -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):
```shell
DIR=/docker/wg-easy
sudo mkdir -p $DIR
sudo mkdir -p /etc/docker/containers/wg-easy
```
2. Download docker compose file
```shell
sudo curl -o $URL/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`
```shell
sudo docker-compose -f $DIR/docker-compose.yml up -d
cd /etc/docker/containers/wg-easy
sudo docker-compose up -d
```
## Setup Firewall
@@ -41,16 +41,26 @@ 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:
- 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.
## 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
## Update `wg-easy`
Open your browser and navigate to `https://<your-domain>:51821` or `https://<your-ip>:51821`.
To update `wg-easy` to the latest version, run:
Follow the instructions to set up your WireGuard VPN.
```shell
cd /etc/docker/containers/wg-easy
sudo docker-compose pull
sudo docker-compose up -d
```
## Auto Update
If you want to enable auto-updates, follow the instructions here: [Auto Updates][auto-updates]
[auto-updates]: ./auto-updates.md
+9
View File
@@ -0,0 +1,9 @@
---
title: Caddy
---
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 -->
@@ -0,0 +1,41 @@
---
title: Docker Run
---
To setup the IPv6 Network, simply run once:
```shell
docker network create \
-d bridge --ipv6 \
-d default \
--subnet 10.42.42.0/24 \
--subnet fdcc:ad94:bacf:61a3::/64 wg \
```
<!-- ref: major version -->
To automatically install & run `wg-easy`, simply run:
```shell
docker run -d \
--net wg \
-e INSECURE=true \
--name wg-easy \
--ip6 fdcc:ad94:bacf:61a3::2a \
--ip 10.42.42.42 \
-v ~/.wg-easy:/etc/wireguard \
-v /lib/modules:/lib/modules:ro \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add NET_ADMIN \
--cap-add SYS_MODULE \
--sysctl net.ipv4.ip_forward=1 \
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
--sysctl net.ipv6.conf.all.forwarding=1 \
--sysctl net.ipv6.conf.default.forwarding=1 \
--restart unless-stopped \
ghcr.io/wg-easy/wg-easy:15
```
The Web UI will now be available at <http://0.0.0.0:51821>.
@@ -2,4 +2,6 @@
title: Without Docker
---
TODO
This is currently not yet supported.
<!-- TODO -->
@@ -1,5 +1,5 @@
---
title: Podman
title: Podman + nftables
---
This guide will show you how to run `wg-easy` with rootful Podman and nftables.
@@ -19,16 +19,23 @@ sudo mkdir -p /etc/containers/volumes/wg-easy
Create a file `/etc/containers/systemd/wg-easy/wg-easy.container` with the following content:
<!-- ref: major version -->
```ini
[Container]
ContainerName=wg-easy
Image=ghcr.io/wg-easy/wg-easy:latest
Image=ghcr.io/wg-easy/wg-easy:15
AutoUpdate=registry
Volume=/etc/containers/volumes/wg-easy:/etc/wireguard:Z
Network=wg-easy.network
PublishPort=51820:51820/udp
PublishPort=51821:51821/tcp
# this is used to allow access over HTTP
# remove this when using a reverse proxy
Environment=INSECURE=true
AddCapability=NET_ADMIN
AddCapability=SYS_MODULE
AddCapability=NET_RAW
@@ -81,7 +88,7 @@ In the Admin Panel of your WireGuard server, go to the `Hooks` tab and add the f
1. PostUp
```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
@@ -90,7 +97,12 @@ In the Admin Panel of your WireGuard server, go to the `Hooks` tab and add the f
nft delete table inet wg_table
```
<!--
TODO: improve docs after better nftables support
TODO: fix accept web ui port
-->
If you don't have iptables loaded on your server, you could see many errors in the logs or in the UI. You can ignore them.
## Restart the Container
Restart the container to apply the new hooks:
```shell
sudo systemctl restart wg-easy
```
+184
View File
@@ -0,0 +1,184 @@
---
title: Traefik
---
/// 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.
+97
View File
@@ -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
```
+11 -7
View File
@@ -4,7 +4,7 @@ hide:
- 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
@@ -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-installation]: https://docs.docker.com/compose/install/
[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
@@ -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.
| Event | Image Tags |
| ----------------------- | ----------------------------- |
| `cron` on `master` | `nightly` |
| `push` a tag (`v1.2.3`) | `1.2.3`, `1.2`, `1`, `latest` |
| tag | Type | 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). |
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
```
/// 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.
///
+5
View File
@@ -0,0 +1,5 @@
---
title: 2FA
---
TODO
+5
View File
@@ -0,0 +1,5 @@
---
title: Edit Account
---
TODO
+5
View File
@@ -0,0 +1,5 @@
---
title: Admin Panel
---
TODO
+5
View File
@@ -0,0 +1,5 @@
---
title: Edit Client
---
TODO
+5
View File
@@ -0,0 +1,5 @@
---
title: Login
---
TODO
+5
View File
@@ -0,0 +1,5 @@
---
title: Setup
---
TODO
+10 -4
View File
@@ -11,9 +11,9 @@ 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].
///
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]: ./usage.md#tagging-convention
[docs-tagging]: ./getting-started.md#tagging-convention
## About
@@ -23,9 +23,9 @@ This documentation provides you not only with the basic setup and configuration
### Getting Started
If you're new to wg-easy, make sure to read the [_Usage_ chapter][docs-usage] first. If you want to look at examples for Docker Run and Compose, we have an [_Examples_ page][docs-examples].
If you're new to wg-easy, make sure to read the [_Getting Started_ chapter][docs-getting-started] first. If you want to look at examples for Docker Run and Compose, we have an [_Examples_ page][docs-examples].
[docs-usage]: ./usage.md
[docs-getting-started]: ./getting-started.md
[docs-examples]: ./examples/tutorials/basic-installation.md
### Contributing
@@ -33,3 +33,9 @@ If you're new to wg-easy, make sure to read the [_Usage_ chapter][docs-usage] fi
We are always happy to welcome new contributors. For guidelines and entrypoints please have a look at the [Contributing section][docs-contributing].
[docs-contributing]: ./contributing/issues-and-pull-requests.md
### Migration
If you are migrating from an older version of `wg-easy`, please read the [_Migration_ chapter][docs-migration].
[docs-migration]: ./advanced/migrate/from-14-to-15.md
+8 -2
View File
@@ -1,7 +1,13 @@
site_name: "wg-easy"
site_description: "The easiest way to run WireGuard VPN + Web-based Admin UI."
site_author: "wg-easy (Github Organization)"
copyright: '<p>&copy <a href="https://github.com/wg-easy"><em>Wireguard Easy Organization</em></a><br/><span>This project is licensed under the GNU Affero General Public License v3.0 or later.</span></p>'
site_author: "WireGuard Easy"
copyright: >
<p>
&copy <a href="https://github.com/wg-easy"><em>Wireguard Easy</em></a><br/>
<span>This project is licensed under AGPL-3.0-only.</span><br/>
<span>This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security</span><br/>
<span>"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld</span>
</p>
repo_url: https://github.com/wg-easy/wg-easy
repo_name: wg-easy
+3 -2
View File
@@ -4,7 +4,8 @@
"scripts": {
"dev": "docker compose -f docker-compose.dev.yml up --build",
"build": "docker build -t wg-easy .",
"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"
},
"packageManager": "pnpm@10.5.2"
"packageManager": "pnpm@10.8.0"
}
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
package_json="src/package.json"
# Function to update the version in package.json
update_version() {
local new_version=$1
jq --arg new_version "$new_version" '.version = $new_version' $package_json > tmp.json && mv tmp.json $package_json
}
# Get the current version from package.json
current_version=$(jq -r '.version' $package_json)
echo "Current version: $current_version"
# Prompt the user for the new version
read -p "Enter the new version (following SemVer): " new_version
# Official SemVer regex for validation
semver_regex="^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
# Validate the new version
if ! echo "$new_version" | grep -Eq "$semver_regex"; then
echo "Invalid version format. Please use SemVer format (e.g., 1.0.0 or 1.0.0-alpha)."
exit 1
fi
# Update the version in package.json
update_version $new_version
echo "Updated package.json to version $new_version"
echo "----"
echo "If you changed the major version, remember to update the docker-compose.yml file and docs (search for: ref: major version)"
echo "----"
echo "If you did everything press 'y' to commit the changes and create a new tag"
read -p "Do you want to continue? (y/n): " confirm
if [ "$confirm" != "y" ]; then
echo "Aborted."
exit 1
fi
# Commit the changes
git add $package_json
git commit -m "Bump version to $new_version"
echo "Committed the changes"
# Create a new Git tag
git tag -a "v$new_version" -m "Release version $new_version"
echo "Created Git tag v$new_version"
# Push the commit & tag to the remote repository
git push origin master --follow-tags
echo "Pushed Git commit and tag v$new_version to remote repository"
@@ -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>
+1 -1
View File
@@ -6,7 +6,7 @@
class="fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
<DialogContent
class="fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
class="fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
>
<DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
+37
View File
@@ -0,0 +1,37 @@
<template>
<SelectRoot v-model="selected">
<SelectTrigger
class="inline-flex h-8 items-center justify-around gap-2 rounded bg-gray-200 px-3 text-sm leading-none dark:bg-neutral-500 dark:text-neutral-200"
aria-label="Choose option"
>
<SelectValue placeholder="Select..." />
<IconsArrowDown class="size-3" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="z-[100] min-w-28 rounded bg-gray-300 dark:bg-neutral-500"
>
<SelectViewport class="p-2">
<SelectItem
v-for="(option, index) in options"
:key="index"
:value="option.value"
class="relative flex h-6 items-center rounded px-3 text-sm leading-none outline-none hover:bg-red-800 hover:text-white dark:text-white"
>
<SelectItemText>
{{ option.value }} - {{ option.label }}
</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
<script lang="ts" setup>
defineProps<{
options: { label: string; value: string }[];
}>();
const selected = defineModel<string>();
</script>
+8 -3
View File
@@ -1,14 +1,17 @@
<template>
<TooltipProvider>
<TooltipRoot>
<TooltipRoot :open="open" @update:open="open = $event">
<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 />
</button>
</TooltipTrigger>
<TooltipPortal>
<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"
>
{{ text }}
@@ -21,4 +24,6 @@
<script lang="ts" setup>
defineProps<{ text: string }>();
const open = ref(false);
</script>
@@ -4,7 +4,9 @@
<slot />
</template>
<template #description>
<div class="bg-white">
<img :src="qrCode" />
</div>
</template>
<template #actions>
<DialogClose>
+11 -4
View File
@@ -1,10 +1,10 @@
<template>
<div class="flex flex-col gap-2">
<div v-if="data?.length === 0">
{{ emptyText || $t('form.noItems') }}
</div>
<div v-else class="flex flex-col gap-2">
<div v-for="(item, i) in data" :key="i">
<div class="flex flex-row gap-1">
<div v-for="(item, i) in data" v-else :key="i">
<div class="mt-1 flex flex-row gap-1">
<input
:value="item"
:name="name"
@@ -12,13 +12,20 @@
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
@input="update($event, i)"
/>
<BaseButton as="input" type="button" value="-" @click="del(i)" />
<BaseButton
as="input"
type="button"
class="rounded-lg"
value="-"
@click="del(i)"
/>
</div>
</div>
<div class="mt-2">
<BaseButton
as="input"
type="button"
class="rounded-lg"
:value="$t('form.add')"
@click="add"
/>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<section class="grid grid-cols-1 gap-4 md:grid-cols-2">
<section class="grid grid-cols-2 gap-4">
<slot />
<Separator
decorative
+50
View File
@@ -0,0 +1,50 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<div class="flex">
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="text"
class="w-full"
:placeholder="placeholder"
/>
<ClientOnly>
<AdminSuggestDialog :url="url" @change="data = $event">
<BaseButton as="span">
<div class="flex items-center gap-3">
<IconsSparkles class="w-4" />
<span>{{ $t('admin.config.suggest') }}</span>
</div>
</BaseButton>
</AdminSuggestDialog>
</ClientOnly>
</div>
</template>
<script lang="ts" setup>
defineProps<{
id: string;
label: string;
description?: string;
placeholder?: string;
url: '/api/admin/ip-info' | '/api/setup/4';
}>();
const data = defineModel<string | null>({
set(value) {
const temp = value?.trim() ?? null;
if (temp === '') {
return null;
}
return temp;
},
});
</script>
@@ -0,0 +1,68 @@
<template>
<div class="flex flex-col gap-2">
<div v-if="data === null">
{{ emptyText || $t('form.nullNoItems') }}
</div>
<div v-for="(item, i) in data" v-else :key="i">
<div class="mt-1 flex flex-row gap-1">
<input
:value="item"
:name="name"
type="text"
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
@input="update($event, i)"
/>
<BaseButton
as="input"
type="button"
class="rounded-lg"
value="-"
@click="del(i)"
/>
</div>
</div>
<div class="mt-2">
<BaseButton
as="input"
type="button"
class="rounded-lg"
:value="$t('form.add')"
@click="add"
/>
</div>
</div>
</template>
<script lang="ts" setup>
const data = defineModel<string[] | null>();
defineProps<{ emptyText?: string[]; name: string }>();
function update(e: Event, i: number) {
const v = (e.target as HTMLInputElement).value;
if (!data.value) {
return;
}
data.value[i] = v;
}
function add() {
if (data.value === undefined) {
return;
}
if (data.value === null) {
data.value = [''];
} else {
data.value.push('');
}
}
function del(i: number) {
if (!data.value) {
return;
}
data.value.splice(i, 1);
if (data.value.length === 0) {
data.value = null;
}
}
</script>
+1 -1
View File
@@ -12,7 +12,7 @@
v-model.trim="data"
:name="id"
type="text"
:autcomplete="autocomplete"
:autocomplete="autocomplete"
:placeholder="placeholder"
/>
</template>
+3 -1
View File
@@ -12,7 +12,8 @@
v-model.trim="data"
:name="id"
type="text"
:autcomplete="autocomplete"
:autocomplete="autocomplete"
:disabled="disabled"
/>
</template>
@@ -22,6 +23,7 @@ defineProps<{
label: string;
description?: string;
autocomplete?: string;
disabled?: boolean;
}>();
const data = defineModel<string>();
+22
View File
@@ -0,0 +1,22 @@
<template>
<div
v-if="!globalStore.information?.insecure && !https"
class="container mx-auto w-fit rounded-md bg-red-800 p-4 text-white shadow-lg dark:bg-red-100 dark:text-red-600"
>
<p class="text-center">{{ $t('login.insecure') }}</p>
</div>
</template>
<script lang="ts" setup>
const globalStore = useGlobalStore();
const https = ref(false);
onMounted(() => {
if (window.location.protocol === 'https:') {
https.value = true;
} else {
https.value = false;
}
});
</script>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<NuxtLink to="/" class="mb-4 flex-grow self-start">
<NuxtLink to="/" class="mb-4">
<h1 class="text-4xl font-medium dark:text-neutral-200">
<img
src="/logo.png"
+9 -6
View File
@@ -1,17 +1,21 @@
<template>
<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"
: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="flex-grow">
<p class="font-bold">{{ $t('update.updateAvailable') }}</p>
<p>{{ globalStore.release.latestRelease.changelog }}</p>
<p>{{ globalStore.information.latestRelease.changelog }}</p>
</div>
<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"
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>
const globalStore = useGlobalStore();
// TODO: only show this to admins
const authStore = useAuthStore();
</script>
+5 -11
View File
@@ -1,13 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
<ArrowDownIcon />
</template>
<script lang="ts" setup>
import ArrowDownIcon from '@heroicons/vue/24/outline/esm/ArrowDownIcon';
</script>
+5 -14
View File
@@ -1,16 +1,7 @@
<template>
<svg
inline
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
<ArrowPathIcon />
</template>
<script lang="ts" setup>
import ArrowPathIcon from '@heroicons/vue/24/outline/esm/ArrowPathIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="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>
<ArrowLeftCircleIcon />
</template>
<script lang="ts" setup>
import ArrowLeftCircleIcon from '@heroicons/vue/24/outline/esm/ArrowLeftCircleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m12.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>
<ArrowRightCircleIcon />
</template>
<script lang="ts" setup>
import ArrowRightCircleIcon from '@heroicons/vue/24/outline/esm/ArrowLeftCircleIcon';
</script>
+5 -11
View File
@@ -1,13 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
<ArrowUpIcon />
</template>
<script lang="ts" setup>
import ArrowUpIcon from '@heroicons/vue/24/outline/esm/ArrowUpIcon';
</script>
+5 -10
View File
@@ -1,12 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke-width="1.5"
fill="currentColor"
>
<path
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z"
/>
</svg>
<ChartBarIcon />
</template>
<script lang="ts" setup>
import ChartBarIcon from '@heroicons/vue/24/outline/esm/ChartBarIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<CheckCircleIcon />
</template>
<script lang="ts" setup>
import CheckCircleIcon from '@heroicons/vue/24/outline/esm/CheckCircleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<XMarkIcon />
</template>
<script lang="ts" setup>
import XMarkIcon from '@heroicons/vue/24/outline/esm/XMarkIcon';
</script>
+5 -11
View File
@@ -1,13 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
<TrashIcon />
</template>
<script lang="ts" setup>
import TrashIcon from '@heroicons/vue/24/outline/esm/TrashIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<ArrowDownTrayIcon />
</template>
<script lang="ts" setup>
import ArrowDownTrayIcon from '@heroicons/vue/24/outline/esm/ArrowDownTrayIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
<PencilSquareIcon />
</template>
<script lang="ts" setup>
import PencilSquareIcon from '@heroicons/vue/24/outline/esm/PencilSquareIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="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>
<InformationCircleIcon />
</template>
<script lang="ts" setup>
import InformationCircleIcon from '@heroicons/vue/24/outline/esm/InformationCircleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="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>
<LanguageIcon />
</template>
<script lang="ts" setup>
import LanguageIcon from '@heroicons/vue/24/outline/esm/LanguageIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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>
<LinkIcon />
</template>
<script lang="ts" setup>
import LinkIcon from '@heroicons/vue/24/outline/esm/LinkIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<ArrowRightStartOnRectangleIcon />
</template>
<script lang="ts" setup>
import ArrowRightStartOnRectangleIcon from '@heroicons/vue/24/outline/esm/ArrowRightStartOnRectangleIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
/>
</svg>
<MoonIcon />
</template>
<script lang="ts" setup>
import MoonIcon from '@heroicons/vue/24/outline/esm/MoonIcon';
</script>
+5 -14
View File
@@ -1,16 +1,7 @@
<template>
<svg
inline
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
<PlusIcon />
</template>
<script lang="ts" setup>
import PlusIcon from '@heroicons/vue/24/outline/esm/PlusIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"
/>
</svg>
<QrCodeIcon />
</template>
<script lang="ts" setup>
import QrCodeIcon from '@heroicons/vue/24/outline/esm/QrCodeIcon';
</script>
+7
View File
@@ -0,0 +1,7 @@
<template>
<SparklesIcon />
</template>
<script lang="ts" setup>
import SparklesIcon from '@heroicons/vue/24/outline/esm/SparklesIcon';
</script>
+5 -14
View File
@@ -1,16 +1,7 @@
<template>
<svg
inline
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"
/>
</svg>
<ServerStackIcon />
</template>
<script lang="ts" setup>
import ServerStackIcon from '@heroicons/vue/24/outline/esm/ServerStackIcon';
</script>
+5 -13
View File
@@ -1,15 +1,7 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
/>
</svg>
<SunIcon />
</template>
<script lang="ts" setup>
import SunIcon from '@heroicons/vue/24/outline/esm/SunIcon';
</script>
+5 -15
View File
@@ -1,17 +1,7 @@
<template>
<!-- Heroicon name: outline/exclamation -->
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<ExclamationTriangleIcon />
</template>
<script lang="ts" setup>
import ExclamationTriangleIcon from '@heroicons/vue/24/outline/esm/ExclamationTriangleIcon';
</script>
+2 -2
View File
@@ -7,7 +7,7 @@
href="https://github.com/wg-easy/wg-easy"
>WireGuard Easy</a
>
({{ globalStore.release?.currentRelease }}) © 2021-2025 by
({{ globalStore.information?.currentRelease }}) © 2021-2025 by
<a
class="hover:underline"
target="_blank"
@@ -24,7 +24,7 @@
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
href="https://github.com/wg-easy/wg-easy#donate"
target="_blank"
>{{ $t('layout.donate') }}</a
>
+31 -14
View File
@@ -1,21 +1,42 @@
import type { NitroFetchRequest, NitroFetchOptions } from 'nitropack/types';
import type {
NitroFetchRequest,
NitroFetchOptions,
TypedInternalResponse,
ExtractedRouteMethod,
} from 'nitropack/types';
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 = {
revert: RevertFn;
type SubmitOpts<
R extends NitroFetchRequest,
T = unknown,
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
> = {
revert: RevertFn<R, T, O>;
successMsg?: string;
errorMsg?: string;
noSuccessToast?: boolean;
};
export function useSubmit<
R extends NitroFetchRequest,
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 { t: $t } = useI18n();
return async (data: unknown) => {
try {
@@ -24,11 +45,6 @@ export function useSubmit<
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) {
toast.showToast({
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) {
if (e instanceof FetchError) {
toast.showToast({
@@ -51,7 +68,7 @@ export function useSubmit<
} else {
console.error(e);
}
await opts.revert(false);
await opts.revert(false, undefined);
}
};
}
+5 -5
View File
@@ -1,23 +1,23 @@
<template>
<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
class="mb-5"
class="mb-5 w-full"
:class="
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'
"
>
<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 />
<HeaderThemeSwitch />
<HeaderChartToggle v-if="loggedIn" />
<UiUserMenu v-if="loggedIn" />
</div>
</div>
<HeaderUpdate class="mt-5" />
<HeaderUpdate class="mt-4" />
</header>
<slot />
<UiFooter />
+2 -2
View File
@@ -11,8 +11,8 @@
</header>
<main>
<Panel>
<PanelBody class="mx-auto mt-10 p-4 md:w-[70%] lg:w-[60%]">
<h2 class="mb-16 mt-8 text-3xl font-medium">
<PanelBody class="m-4 mx-auto mt-10 md:w-[70%] lg:w-[60%]">
<h2 class="mb-16 mt-8 text-center text-3xl font-medium">
{{ $t('setup.welcome') }}
</h2>
+10 -5
View File
@@ -1,8 +1,8 @@
<template>
<div>
<div class="container mx-auto p-4">
<div class="flex">
<div class="mr-4 w-64 rounded-lg bg-white p-4 dark:bg-neutral-700">
<div class="flex flex-col gap-4 lg:flex-row">
<div class="rounded-lg bg-white p-4 lg:w-64 dark:bg-neutral-700">
<NuxtLink to="/admin">
<h2 class="mb-4 text-xl font-bold dark:text-neutral-200">
{{ t('pages.admin.panel') }}
@@ -13,6 +13,7 @@
v-for="(item, index) in menuItems"
:key="index"
:to="`/admin/${item.id}`"
active-class="bg-red-800 rounded"
>
<BaseButton
as="span"
@@ -27,7 +28,7 @@
<div
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 />
</div>
</div>
@@ -44,13 +45,17 @@ const { t } = useI18n();
const route = useRoute();
const menuItems = [
{ id: '', name: t('pages.admin.general') },
{ id: 'general', name: t('pages.admin.general') },
{ id: 'config', name: t('pages.admin.config') },
{ id: 'interface', name: t('pages.admin.interface') },
{ id: 'hooks', name: t('pages.admin.hooks') },
];
const defaultItem = { id: '', name: t('pages.admin.panel') };
const activeMenuItem = computed(() => {
return menuItems.find((item) => route.path === `/admin/${item.id}`);
return (
menuItems.find((item) => route.path === `/admin/${item.id}`) ?? defaultItem
);
});
</script>
+8 -7
View File
@@ -3,11 +3,12 @@
<FormElement @submit.prevent="submit">
<FormGroup>
<FormHeading>{{ $t('admin.config.connection') }}</FormHeading>
<FormTextField
<FormHostField
id="host"
v-model="data.host"
:label="$t('general.host')"
:description="$t('admin.config.hostDesc')"
url="/api/admin/ip-info"
/>
<FormNumberField
id="port"
@@ -17,18 +18,18 @@
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('admin.config.allowedIpsDesc')">{{
$t('general.allowedIps')
}}</FormHeading>
<FormHeading :description="$t('admin.config.allowedIpsDesc')">
{{ $t('general.allowedIps') }}
</FormHeading>
<FormArrayField
v-model="data.defaultAllowedIps"
name="defaultAllowedIps"
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('admin.config.dnsDesc')">{{
$t('admin.config.dns')
}}</FormHeading>
<FormHeading :description="$t('admin.config.dnsDesc')">
{{ $t('general.dns') }}
</FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" />
</FormGroup>
<FormGroup>
+64
View File
@@ -0,0 +1,64 @@
<template>
<main v-if="data">
<FormElement @submit.prevent="submit">
<FormGroup>
<FormNumberField
id="session"
v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.general.metrics') }}</FormHeading>
<FormNullTextField
id="password"
v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
/>
<FormSwitchField
id="prometheus"
v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
/>
<FormSwitchField
id="json"
v-model="data.metricsJson"
:label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" />
<FormActionField :label="$t('form.revert')" @click="revert" />
</FormGroup>
</FormElement>
</main>
</template>
<script setup lang="ts">
const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get',
});
const data = toRef(_data.value);
const _submit = useSubmit(
`/api/admin/general`,
{
method: 'post',
},
{ revert }
);
function submit() {
return _submit(data.value);
}
async function revert() {
await refresh();
data.value = toRef(_data.value).value;
}
</script>
+20 -4
View File
@@ -2,10 +2,26 @@
<main v-if="data">
<FormElement @submit.prevent="submit">
<FormGroup>
<FormTextField id="PreUp" v-model="data.preUp" label="PreUp" />
<FormTextField id="PostUp" v-model="data.postUp" label="PostUp" />
<FormTextField id="PreDown" v-model="data.preDown" label="PreDown" />
<FormTextField id="PostDown" v-model="data.postDown" label="PostDown" />
<FormTextField
id="PreUp"
v-model="data.preUp"
:label="$t('hooks.preUp')"
/>
<FormTextField
id="PostUp"
v-model="data.postUp"
:label="$t('hooks.postUp')"
/>
<FormTextField
id="PreDown"
v-model="data.preDown"
:label="$t('hooks.preDown')"
/>
<FormTextField
id="PostDown"
v-model="data.postDown"
:label="$t('hooks.postDown')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
+2 -61
View File
@@ -1,64 +1,5 @@
<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 class="flex flex-col gap-3">
<p class="whitespace-pre-line">{{ $t('admin.introText') }}</p>
</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>
+26 -1
View File
@@ -34,8 +34,19 @@
<FormActionField
:label="$t('admin.interface.changeCidr')"
class="w-full"
tabindex="-1"
/>
</AdminCidrDialog>
<AdminRestartInterfaceDialog
trigger-class="col-span-2"
@restart="restartInterface"
>
<FormActionField
:label="$t('admin.interface.restart')"
class="w-full"
tabindex="-1"
/>
</AdminRestartInterfaceDialog>
</FormGroup>
</FormElement>
</main>
@@ -75,11 +86,25 @@ const _changeCidr = useSubmit(
{
revert,
successMsg: t('admin.interface.cidrSuccess'),
errorMsg: t('admin.interface.cidrError'),
}
);
async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) {
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>
+47 -9
View File
@@ -41,21 +41,26 @@
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('client.allowedIpsDesc')">{{
$t('general.allowedIps')
}}</FormHeading>
<FormArrayField v-model="data.allowedIps" name="allowedIps" />
<FormHeading :description="$t('client.allowedIpsDesc')">
{{ $t('general.allowedIps') }}
</FormHeading>
<FormNullArrayField v-model="data.allowedIps" name="allowedIps" />
</FormGroup>
<FormGroup>
<FormHeading :description="$t('client.serverAllowedIpsDesc')">{{
$t('client.serverAllowedIps')
}}</FormHeading>
<FormHeading :description="$t('client.serverAllowedIpsDesc')">
{{ $t('client.serverAllowedIps') }}
</FormHeading>
<FormArrayField
v-model="data.serverAllowedIps"
name="serverAllowedIps"
/>
</FormGroup>
<FormGroup></FormGroup>
<FormGroup>
<FormHeading :description="$t('client.dnsDesc')">
{{ $t('general.dns') }}
</FormHeading>
<FormNullArrayField v-model="data.dns" name="dns" />
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading>
<FormNumberField
@@ -71,6 +76,35 @@
:label="$t('general.persistentKeepalive')"
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('client.hooksDescription')">
{{ $t('client.hooks') }}
</FormHeading>
<FormTextField
id="PreUp"
v-model="data.preUp"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.preUp')"
/>
<FormTextField
id="PostUp"
v-model="data.postUp"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.postUp')"
/>
<FormTextField
id="PreDown"
v-model="data.preDown"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.preDown')"
/>
<FormTextField
id="PostDown"
v-model="data.postDown"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.postDown')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" />
@@ -113,8 +147,12 @@ const _submit = useSubmit(
method: 'post',
},
{
revert: async () => {
revert: async (success) => {
if (success) {
await navigateTo('/');
} else {
await revert();
}
},
}
);
+47 -6
View File
@@ -1,6 +1,7 @@
<template>
<main>
<UiBanner />
<HeaderInsecure />
<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"
@submit.prevent="submit"
@@ -29,6 +30,18 @@
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
class="flex gap-2 whitespace-nowrap"
:title="$t('login.rememberMeDesc')"
@@ -54,10 +67,18 @@
</template>
<script setup lang="ts">
const authStore = useAuthStore();
authStore.update();
const toast = useToast();
const { t } = useI18n();
const authenticating = ref(false);
const remember = ref(false);
const username = ref<null | string>(null);
const password = ref<null | string>(null);
const username = ref<string>('');
const password = ref<string>('');
const totpRequired = ref(false);
const totp = ref<string>('');
const _submit = useSubmit(
'/api/session',
@@ -65,13 +86,32 @@ const _submit = useSubmit(
method: 'post',
},
{
revert: async (success) => {
authenticating.value = false;
password.value = null;
revert: async (success, data) => {
if (success) {
if (data?.status === 'success') {
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,
}
@@ -86,6 +126,7 @@ async function submit() {
username: username.value,
password: password.value,
remember: remember.value,
totpCode: totpRequired.value ? totp.value : undefined,
});
}
</script>
+140 -1
View File
@@ -40,7 +40,7 @@
id="confirm-password"
v-model="confirmPassword"
autocomplete="new-password"
:label="$t('me.confirmPassword')"
:label="$t('general.confirmPassword')"
/>
<FormActionField
type="submit"
@@ -48,12 +48,74 @@
/>
</FormGroup>
</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>
</Panel>
</main>
</template>
<script setup lang="ts">
import { encodeQR } from 'qr';
const authStore = useAuthStore();
authStore.update();
@@ -101,4 +163,81 @@ function updatePassword() {
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>
+3 -3
View File
@@ -1,9 +1,9 @@
<template>
<div>
<p class="px-8 pt-8 text-center text-2xl">
<div class="flex flex-col items-center">
<p class="px-8 text-center text-2xl">
{{ $t('setup.welcomeDesc') }}
</p>
<NuxtLink to="/setup/2">
<NuxtLink to="/setup/2" class="mt-8">
<BaseButton as="span">{{ $t('general.continue') }}</BaseButton>
</NuxtLink>
</div>
+17 -4
View File
@@ -1,9 +1,9 @@
<template>
<div>
<p class="p-8 text-center text-lg">
<p class="text-center text-lg">
{{ $t('setup.createAdminDesc') }}
</p>
<div class="flex flex-col gap-3">
<div class="mt-8 flex flex-col gap-3">
<div class="flex flex-col">
<FormNullTextField
id="username"
@@ -20,7 +20,15 @@
:label="$t('general.password')"
/>
</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>
</div>
</div>
@@ -37,6 +45,7 @@ setupStore.setStep(2);
const username = ref<null | string>(null);
const password = ref<string>('');
const confirmPassword = ref<string>('');
const _submit = useSubmit(
'/api/setup/2',
@@ -54,6 +63,10 @@ const _submit = useSubmit(
);
function submit() {
return _submit({ username: username.value, password: password.value });
return _submit({
username: username.value,
password: password.value,
confirmPassword: confirmPassword.value,
});
}
</script>
+10 -6
View File
@@ -1,14 +1,18 @@
<template>
<div>
<p class="p-8 text-center text-lg">
<p class="text-center text-lg">
{{ $t('setup.existingSetup') }}
</p>
<div class="mb-8 flex justify-center">
<NuxtLink to="/setup/4">
<BaseButton as="span">{{ $t('general.no') }}</BaseButton>
<div class="mt-4 flex justify-center gap-3">
<NuxtLink to="/setup/4" class="w-20">
<BaseButton as="span" class="w-full justify-center">
{{ $t('general.no') }}
</BaseButton>
</NuxtLink>
<NuxtLink to="/setup/migrate">
<BaseButton as="span">{{ $t('general.yes') }}</BaseButton>
<NuxtLink to="/setup/migrate" class="w-20">
<BaseButton as="span" class="w-full justify-center">
{{ $t('general.yes') }}
</BaseButton>
</NuxtLink>
</div>
</div>
+12 -5
View File
@@ -1,21 +1,28 @@
<template>
<div>
<p class="p-8 text-center text-lg">
<p class="text-center text-lg">
{{ $t('setup.setupConfigDesc') }}
</p>
<div class="flex flex-col gap-3">
<div class="mt-8 flex flex-col gap-3">
<div class="flex flex-col">
<FormNullTextField
<FormHostField
id="host"
v-model="host"
:label="$t('general.host')"
placeholder="vpn.example.com"
:description="$t('setup.hostDesc')"
url="/api/setup/4"
/>
</div>
<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 class="mt-4 flex justify-center">
<BaseButton @click="submit">{{ $t('general.continue') }}</BaseButton>
</div>
</div>
+5 -3
View File
@@ -1,14 +1,16 @@
<template>
<div>
<p class="p-8 text-center text-lg">
<div class="flex flex-col items-center">
<p class="text-center text-lg">
{{ $t('setup.setupMigrationDesc') }}
</p>
<div>
<div class="mt-8 flex gap-3">
<Label for="migration">{{ $t('setup.migration') }}</Label>
<input id="migration" type="file" @change="onChangeFile" />
</div>
<div class="mt-4">
<BaseButton @click="submit">{{ $t('setup.upload') }}</BaseButton>
</div>
</div>
</template>
<script lang="ts" setup>
+2 -2
View File
@@ -1,7 +1,7 @@
<template>
<div>
<div class="flex flex-col items-center">
<p>{{ $t('setup.successful') }}</p>
<NuxtLink to="/login">
<NuxtLink to="/login" class="mt-4">
<BaseButton as="span">{{ $t('login.signIn') }}</BaseButton>
</NuxtLink>
</div>
+2 -2
View File
@@ -1,5 +1,5 @@
export const useGlobalStore = defineStore('Global', () => {
const { data: release } = useFetch('/api/release', {
const { data: information } = useFetch('/api/information', {
method: 'get',
});
@@ -21,7 +21,7 @@ export const useGlobalStore = defineStore('Global', () => {
return {
sortClient,
release,
information,
uiShowCharts,
toggleCharts,
uiChartType,
+58 -32
View File
@@ -15,7 +15,12 @@
},
"me": {
"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": {
"name": "Name",
@@ -25,25 +30,32 @@
"updatePassword": "Update Password",
"mtu": "MTU",
"allowedIps": "Allowed IPs",
"dns": "DNS",
"persistentKeepalive": "Persistent Keepalive",
"logout": "Logout",
"continue": "Continue",
"host": "Host",
"port": "Port",
"yes": "Yes",
"no": "No"
"no": "No",
"confirmPassword": "Confirm Password",
"loading": "Loading...",
"2fa": "Two Factor Authentication",
"2faCode": "TOTP Code"
},
"setup": {
"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!",
"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",
"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.",
"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.",
"upload": "Upload",
"migration": "Restore the backup",
"migration": "Restore the backup:",
"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": {
"updateAvailable": "There is an update available!",
@@ -61,11 +73,10 @@
"login": {
"signIn": "Sign In",
"rememberMe": "Remember me",
"rememberMeDesc": "Stay logged after closing the browser"
},
"error": {
"clear": "Clear",
"login": "Log in error"
"rememberMeDesc": "Stay logged after closing the browser",
"insecure": "You can't log in with an insecure connection. Use HTTPS.",
"2faRequired": "Two Factor Authentication is required",
"2faWrong": "Two Factor Authentication is wrong"
},
"client": {
"empty": "There are no clients yet.",
@@ -95,10 +106,14 @@
"noPrivKey": "This client has no known private key. Cannot create Configuration.",
"showQR": "Show QR Code",
"downloadConfig": "Download Configuration",
"allowedIpsDesc": "Which IPs will be routed through the VPN",
"allowedIpsDesc": "Which IPs will be routed through the VPN (overrides global config)",
"serverAllowedIpsDesc": "Which IPs the server will route to the client",
"mtuDesc": "Sets the maximum transmission unit (packet size) for the VPN tunnel",
"persistentKeepaliveDesc": "Sets the interval (in seconds) for keep-alive packets. 0 disables it"
"persistentKeepaliveDesc": "Sets the interval (in seconds) for keep-alive packets. 0 disables it",
"hooks": "Hooks",
"hooksDescription": "Hooks only work with wg-quick",
"hooksLeaveEmpty": "Only for wg-quick. Otherwise, leave it empty",
"dnsDesc": "DNS server clients will use (overrides global config)"
},
"dialog": {
"change": "Change",
@@ -108,8 +123,7 @@
"toast": {
"success": "Success",
"saved": "Saved",
"error": "Error",
"errored": "Failed to save"
"error": "Error"
},
"form": {
"actions": "Actions",
@@ -118,6 +132,7 @@
"sectionGeneral": "General",
"sectionAdvanced": "Advanced",
"noItems": "No items",
"nullNoItems": "No items. Using global config",
"add": "Add"
},
"admin": {
@@ -126,7 +141,7 @@
"sessionTimeoutDesc": "Session duration for Remember Me (seconds)",
"metrics": "Metrics",
"metricsPassword": "Password",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (argon2 hash)",
"metricsPasswordDesc": "Bearer Password for the metrics endpoint (password or argon2 hash)",
"json": "JSON",
"jsonDesc": "Route for metrics in JSON format",
"prometheus": "Prometheus",
@@ -135,22 +150,27 @@
"config": {
"connection": "Connection",
"hostDesc": "Public hostname clients will connect to (invalidates config)",
"portDesc": "Public UDP port clients will connect to (invalidates config)",
"allowedIpsDesc": "Allowed IPs clients will use (invalidates config)",
"dns": "DNS",
"dnsDesc": "DNS server clients will use (invalidates config)",
"mtuDesc": "MTU clients will use (invalidates config)",
"persistentKeepaliveDesc": "Interval in seconds to send keepalives to the server. 0 = disabled (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)",
"dnsDesc": "DNS server clients will use (global config)",
"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)",
"suggest": "Suggest",
"suggestDesc": "Choose a IP-Address or Hostname for the Host field"
},
"interface": {
"cidrSuccess": "Changed CIDR",
"cidrError": "Failed to change CIDR",
"device": "Device",
"deviceDesc": "Ethernet device the wireguard traffic should be forwarded through",
"mtuDesc": "MTU WireGuard will use",
"portDesc": "UDP Port WireGuard will listen on (could invalidate config)",
"changeCidr": "Change CIDR"
}
"portDesc": "UDP Port WireGuard will listen on (you probably want to change Config Port too)",
"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": {
"generic": {
@@ -173,15 +193,14 @@
"user": {
"username": "Username",
"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",
"name": "Name",
"email": "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": {
"host": "Host"
@@ -193,7 +212,8 @@
},
"interface": {
"cidr": "CIDR",
"device": "Device"
"device": "Device",
"cidrValid": "CIDR must be valid"
},
"otl": "One Time link",
"stringMalformed": "String is malformed",
@@ -207,5 +227,11 @@
"dns": "DNS",
"allowedIps": "Allowed IPs",
"file": "File"
},
"hooks": {
"preUp": "PreUp",
"postUp": "PostUp",
"preDown": "PreDown",
"postDown": "PostDown"
}
}
+2
View File
@@ -28,7 +28,9 @@ export default defineNuxtConfig({
},
locales: [
{
// same as i18n.config.ts
code: 'en',
// BCP 47 language tag
language: 'en-US',
name: 'English',
},
+20 -17
View File
@@ -1,6 +1,6 @@
{
"name": "wg-easy",
"version": "15.0.0-beta.1",
"version": "15.0.0-beta.12",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"private": true,
"type": "module",
@@ -19,10 +19,12 @@
},
"dependencies": {
"@eschricht/nuxt-color-mode": "^1.1.5",
"@libsql/client": "^0.14.0",
"@nuxtjs/i18n": "^9.2.1",
"@nuxtjs/tailwindcss": "^6.13.1",
"@pinia/nuxt": "^0.10.1",
"@heroicons/vue": "^2.2.0",
"@libsql/client": "^0.15.3",
"@nuxtjs/i18n": "^9.5.3",
"@nuxtjs/tailwindcss": "^6.13.2",
"@phc/format": "^1.0.0",
"@pinia/nuxt": "^0.11.0",
"@tailwindcss/forms": "^0.5.10",
"apexcharts": "^4.5.0",
"argon2": "^0.41.1",
@@ -30,15 +32,16 @@
"cidr-tools": "^11.0.3",
"crc-32": "^1.2.2",
"debug": "^4.4.0",
"drizzle-orm": "^0.40.0",
"drizzle-orm": "^0.41.0",
"ip-bigint": "^8.2.1",
"is-cidr": "^5.1.1",
"is-ip": "^5.0.1",
"js-sha256": "^0.11.0",
"lowdb": "^7.0.1",
"nuxt": "^3.15.4",
"pinia": "^3.0.1",
"qrcode": "^1.5.4",
"nuxt": "^3.16.2",
"otpauth": "^9.4.0",
"pinia": "^3.0.2",
"qr": "^0.4.0",
"radix-vue": "^1.9.17",
"semver": "^7.7.1",
"tailwindcss": "^3.4.17",
@@ -48,17 +51,17 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@nuxt/eslint": "1.1.0",
"@nuxt/eslint": "1.3.0",
"@types/debug": "^4.1.12",
"@types/qrcode": "^1.5.5",
"@types/semver": "^7.5.8",
"drizzle-kit": "^0.30.5",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2",
"@types/phc__format": "^1.0.1",
"@types/semver": "^7.7.0",
"drizzle-kit": "^0.30.6",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.8"
},
"packageManager": "pnpm@10.5.2"
"packageManager": "pnpm@10.8.0"
}
+2033 -2619
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
export default definePermissionEventHandler('admin', 'any', async () => {
await WireGuard.Restart();
return { success: true };
});
+4
View File
@@ -0,0 +1,4 @@
export default definePermissionEventHandler('admin', 'any', async () => {
const result = await cachedGetIpInformation();
return result;
});
@@ -3,9 +3,11 @@ import { gt } from 'semver';
export default defineEventHandler(async () => {
const latestRelease = await cachedFetchLatestRelease();
const updateAvailable = gt(latestRelease.version, RELEASE);
const insecure = WG_ENV.INSECURE;
return {
currentRelease: RELEASE,
latestRelease: latestRelease,
updateAvailable,
insecure,
};
});

Some files were not shown because too many files have changed in this diff Show More