Compare commits

..

7 Commits

Author SHA1 Message Date
Lucas Rattz 380ff5c2f1 Improve documentation on password hash (#1901)
* Improve documentation on password hash

* Change branch name

* Add single quote docker run info

* Tag versions, docker run version

* separate docker run and compose

---------

Co-authored-by: Bernd Storath <32197462+kaaax0815@users.noreply.github.com>
2025-06-03 09:58:52 +02:00
Bernd Storath ae540593fe fix iptables alias
!skipci
2025-05-31 21:29:43 +02:00
Bernd Storath 42adeb391c update dockerfile, update workflows
!skipci
2025-05-31 21:22:56 +02:00
Bernd Storath 02589a3ce9 early fail if old password variable (#1350) 2024-09-04 18:42:42 +02:00
Philip H. 0a6645b526 Version 14: Home Assistent support, PASSWORD_HASH (inc. Helper), translation updates, bugfixes and more (#1199) 2024-08-09 21:32:32 +02:00
Philip H 13616a2f1e Security vulnerability patched and minor improvements (#1071, #1072) 2024-05-27 21:06:22 +02:00
Philip H 69726cba75 fixup: WG_PORT in config.js (#1056)
patch for v13
2024-05-18 14:15:19 +02:00
276 changed files with 10709 additions and 21310 deletions
+1 -3
View File
@@ -1,3 +1 @@
/src/node_modules
/src/.nuxt
/src/.output
/src/node_modules
-23
View File
@@ -1,23 +0,0 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# The JSON files contain newlines inconsistently
[*.json]
insert_final_newline = ignore
# Minified JavaScript files shouldn't be changed
[**.min.js]
indent_style = ignore
insert_final_newline = ignore
[*.md]
trim_trailing_whitespace = false
+1 -1
View File
@@ -2,4 +2,4 @@
# Founder and Codeowner of WireGuard Easy (wg-easy)
# Maintained by Bernd Storath (kaaax0815)
* @WeeJeWel
* @kaaax0815
* @kaaax0815
+1 -1
View File
@@ -1,3 +1,3 @@
# These are supported funding model platforms
github: [weejewel, kaaax0815]
github: weejewel
-37
View File
@@ -1,37 +0,0 @@
---
name: 🐛 Bug Report
description: Create a report to help us improve
title: "[Bug]: "
type: Bug
body:
- type: markdown
attributes:
value: |
**Thanks :heart: for taking the time to fill out this bug report!**
We kindly ask that you search to see if an issue [already exists](https://github.com/wg-easy/wg-easy/issues?q=is%3Aissue+sort%3Acreated-desc+) for the bug you encountered.
- type: textarea
id: what-happened
attributes:
label: Describe the bug
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: what-should-happen
attributes:
label: Expected behavior
placeholder: Tell us what you expected!
value: "Work just fine!"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
@@ -1,47 +0,0 @@
---
name: 🛠️ Feature Request
description: Suggest an idea to help us improve
title: "[Feat]: "
type: Feature
body:
- type: markdown
attributes:
value: |
**Thanks :heart: for taking the time to fill out this feature request report!**
We kindly ask that you search to see if an issue [already exists](https://github.com/wg-easy/wg-easy/issues?q=is%3Aissue+sort%3Acreated-desc+) for your feature.
We are also happy to accept contributions from our users. For more details see [here](https://github.com/wg-easy/wg-easy/blob/master/contributing.md).
- type: textarea
attributes:
label: Description
description: |
A clear and concise description of the feature you're interested in.
validations:
required: true
- type: textarea
attributes:
label: Suggested Solution
description: |
Describe the solution you'd like. A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Alternatives
description: |
Describe alternatives you've considered.
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: |
Add any other context about the problem here.
validations:
required: false
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. macOS 12.1]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS 8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
-5
View File
@@ -1,5 +0,0 @@
contact_links:
- name: Get Help
url: https://github.com/wg-easy/wg-easy/discussions/new?category=q-a
about: If you can't get something to work the way you expect, open a question in the discussions.
blank_issues_enabled: false
-10
View File
@@ -5,13 +5,3 @@ updates:
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: "npm"
directory: "/src/"
schedule:
interval: "weekly"
rebase-strategy: "auto"
-43
View File
@@ -1,43 +0,0 @@
name: CodeQL
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: "15 0 * * *"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript-typescript"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
-71
View File
@@ -1,71 +0,0 @@
name: Development
on:
workflow_dispatch:
jobs:
docker:
name: Build & Deploy Docker
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Publish Docker Image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/wg-easy/wg-easy:development
cache-from: type=gha
cache-to: type=gha,mode=min
docs:
name: Build & Deploy Docs
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
contents: write
needs: docker
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.11.9
cache: "pip"
cache-dependency-path: docs/requirements.txt
- name: Install Dependencies
run: |
pip install -r docs/requirements.txt
- name: Setup Git User
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Build Docs Website
run: |
cd docs
git fetch origin gh-pages --depth=1 || true
mike deploy --push --update-aliases development
-77
View File
@@ -1,77 +0,0 @@
name: Nightly
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
docker:
name: Build & Deploy Docker
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: master
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Publish Docker Image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/wg-easy/wg-easy:nightly
cache-from: type=gha
cache-to: type=gha,mode=min
docs:
name: Build & Deploy Docs
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
contents: write
needs: docker
steps:
- uses: actions/checkout@v4
with:
ref: master
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.11.9
cache: "pip"
cache-dependency-path: docs/requirements.txt
- name: Install Dependencies
run: |
pip install -r docs/requirements.txt
- name: Setup Git User
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Build Docs Website
run: |
cd docs
git fetch origin gh-pages --depth=1 || true
mike deploy --push --update-aliases nightly
+20 -28
View File
@@ -1,43 +1,35 @@
name: Pull Request
name: Build Pull Request
on:
workflow_dispatch:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docker:
name: Build Docker
deploy:
name: Build & Deploy
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@v6
with:
context: .
push: false
platforms: linux/amd64,linux/arm64
tags: ghcr.io/wg-easy/wg-easy:pr
cache-from: type=gha
cache-to: type=gha,mode=min
- name: Build Docker Image
uses: docker/build-push-action@v6
with:
push: false
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ghcr.io/wg-easy/wg-easy:pr
+25 -87
View File
@@ -1,103 +1,41 @@
name: Production
name: Build & Publish Latest
on:
workflow_dispatch:
push:
tags:
- "v*"
branches:
- v14
jobs:
docker:
name: Build & Deploy Docker
deploy:
name: Build & Deploy
runs-on: ubuntu-latest
if: |
github.repository_owner == 'wg-easy' &&
startsWith(github.ref, 'refs/tags/v')
!contains(github.event.head_commit.message, '!skipci')
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
ref: v14
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Publish Docker Image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=min
docs:
name: Build & Deploy Docs
runs-on: ubuntu-latest
if: |
github.repository_owner == 'wg-easy' &&
startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
needs: docker
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.11.9
cache: "pip"
cache-dependency-path: docs/requirements.txt
- name: Install Dependencies
run: |
pip install -r docs/requirements.txt
- name: Setup Git User
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Build Docs Website
run: |
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
# Check if it's a stable release (only numbers, no '-')
if [[ "$DOCS_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Stable release detected: $DOCS_VERSION"
mike deploy --push --update-aliases $MINOR_VERSION latest
else
echo "Pre-release detected: $DOCS_VERSION"
mike deploy --push --update-aliases Pre-release
fi
- name: Build & Publish Docker Image
uses: docker/build-push-action@v6
with:
push: true
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ghcr.io/wg-easy/wg-easy:latest, ghcr.io/wg-easy/wg-easy:14
+6 -19
View File
@@ -3,7 +3,7 @@ name: Lint
on:
push:
branches:
- master
- v14
pull_request:
jobs:
@@ -11,31 +11,18 @@ jobs:
name: Lint
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
strategy:
fail-fast: false
max-parallel: 3
matrix:
command: ["lint", "typecheck", "format:check"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version: '20'
check-latest: true
cache: "pnpm"
cache: 'npm'
- name: pnpm ${{ matrix.command }}
- name: npm run lint
run: |
cd src
pnpm install
pnpm ${{ matrix.command }}
npm ci
npm run lint
-34
View File
@@ -1,34 +0,0 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
workflow_dispatch:
schedule:
- cron: "*/5 * * * *"
jobs:
stale:
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: 30
days-before-pr-close: 14
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity."
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 100
+4
View File
@@ -1,2 +1,6 @@
/config
/wg0.conf
/wg0.json
/src/node_modules
.DS_Store
*.swp
-14
View File
@@ -1,14 +0,0 @@
{
"recommendations": [
"aaron-bond.better-comments",
"dbaeumer.vscode-eslint",
"antfu.goto-alias",
"visualstudioexptteam.vscodeintellicode",
"Nuxtr.nuxtr-vscode",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors",
"bradlc.vscode-tailwindcss",
"vue.volar",
"lokalise.i18n-ally"
]
}
-25
View File
@@ -1,25 +0,0 @@
{
"editor.tabSize": 2,
"editor.useTabStops": false,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"nuxtr.vueFiles.style.addStyleTag": false,
"nuxtr.piniaFiles.defaultTemplate": "setup",
"nuxtr.monorepoMode.DirectoryName": "src",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.tsdk": "./src/node_modules/typescript/lib",
"i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.localesPaths": ["src/i18n/locales"],
"i18n-ally.sortKeys": false,
"i18n-ally.keepFulfilled": false,
"i18n-ally.keystyle": "nested",
"editor.gotoLocation.multipleDefinitions": "goto"
}
-32
View File
@@ -1,32 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
We're super excited to announce v15!
This update is an entire rewrite to make it even easier to set up your own VPN.
## Major Changes
- Almost all Environment variables removed
- New and Improved UI
- API Basic Authentication
- Added Docs
- Incrementing Version -> Semantic Versioning
- CIDR Support
- IPv6 Support
- Changed API Structure
- SQLite Database
- Deprecated Dockerless Installations
- Added Docker Volume Mount (`/lib/modules`)
- Removed ARMv6 and ARMv7 support
## [14.0.0] - 2024-09-04
### Major changes
- `PASSWORD` has been replaced by `PASSWORD_HASH`
+25 -30
View File
@@ -1,54 +1,49 @@
FROM docker.io/library/node:lts-alpine AS build
WORKDIR /app
# As a workaround we have to build on nodejs 18
# nodejs 20 hangs on build with armv6/armv7
FROM docker.io/library/node:lts-alpine AS build_node_modules
# update corepack
RUN npm install --global corepack@latest
# Install pnpm
RUN corepack enable pnpm
# Update npm to latest
RUN npm install -g npm@latest
# Copy Web UI
COPY src/package.json src/pnpm-lock.yaml ./
RUN pnpm install
# Build UI
COPY src ./
RUN pnpm build
COPY src /app
WORKDIR /app
RUN npm ci --omit=dev &&\
mv node_modules /node_modules
# Copy build result to a new image.
# This saves a lot of disk space.
FROM docker.io/library/node:lts-alpine
WORKDIR /app
HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3
COPY --from=build_node_modules /app /app
HEALTHCHECK --interval=1m --timeout=5s --retries=3 CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1"
# Move node_modules one directory up, so during development
# we don't have to mount it in a volume.
# This results in much faster reloading!
#
# Also, some node_modules might be native, and
# the architecture & OS of your development machine might differ
# than what runs inside of docker.
COPY --from=build_node_modules /node_modules /node_modules
# Copy build
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
# Copy the needed wg-password scripts
COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw
RUN chmod +x /bin/wgpw
# Install Linux packages
RUN apk add --no-cache \
dpkg \
dumb-init \
iptables \
ip6tables \
kmod \
iptables-legacy \
wireguard-tools
# Use iptables-legacy
RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save
RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tables-legacy 10 --slave /usr/sbin/ip6tables-restore ip6tables-restore /usr/sbin/ip6tables-legacy-restore --slave /usr/sbin/ip6tables-save ip6tables-save /usr/sbin/ip6tables-legacy-save
# Set Environment
ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821
ENV HOST=0.0.0.0
ENV INSECURE=false
LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy
ENV DEBUG=Server,WireGuard
# Run Web UI
CMD ["/usr/bin/dumb-init", "node", "server/index.mjs"]
WORKDIR /app
CMD ["/usr/bin/dumb-init", "node", "server.js"]
-37
View File
@@ -1,37 +0,0 @@
FROM docker.io/library/node:lts-alpine
WORKDIR /app
# update corepack
RUN npm install --global corepack@latest
# Install pnpm
RUN corepack enable pnpm
HEALTHCHECK --interval=1m --timeout=5s --retries=3 CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1"
# Install Linux packages
RUN apk add --no-cache \
dpkg \
dumb-init \
iptables \
ip6tables \
kmod \
iptables-legacy \
wireguard-tools
# Use iptables-legacy
RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save
RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tables-legacy 10 --slave /usr/sbin/ip6tables-restore ip6tables-restore /usr/sbin/ip6tables-legacy-restore --slave /usr/sbin/ip6tables-save ip6tables-save /usr/sbin/ip6tables-legacy-save
# Set Environment
ENV DEBUG=Server,WireGuard,Database,CMD
ENV PORT=51821
ENV HOST=0.0.0.0
ENV INSECURE=false
# Install Dependencies
COPY src/package.json src/pnpm-lock.yaml ./
RUN pnpm install
# Copy Project
COPY src ./
+45
View File
@@ -0,0 +1,45 @@
# Generating bcrypt-hashed password
With version 14 of wg-easy, a password hashed with bcrypt is needed instead of the plain-text password string. This doc explains how to generate the hash based on a plain-text password.
## Using Docker + node
- You are using docker compose
The easiest way to generate a bcrypt password hash with wgpw is using docker and node:
```sh
docker run ghcr.io/wg-easy/wg-easy:14 node -e 'const bcrypt = require("bcryptjs"); const hash = bcrypt.hashSync("YOUR_PASSWORD", 10); console.log(hash.replace(/\$/g, "$$$$"));'
```
The hashed password will get printed on your terminal. Copy it and use on the `PASSWORD_HASH` environment variable in your docker compose.
- You are using `docker run`
If you are using `docker run` for running wg-easy, you must enclose the hash string in single quotes (`'...'`). You can use this command:
```sh
docker run --rm ghcr.io/wg-easy/wg-easy:14 node -e "const bcrypt = require('bcryptjs'); const hash = bcrypt.hashSync('YOUR_PASSWORD', 10); console.log('\'' + hash + '\'');"
```
The hashed password will get printed on your terminal. Copy it and use on the `PASSWORD_HASH` environment variable in your docker run command.
## Using Docker + wgpw
`wg-password` (wgpw) is a script that generates bcrypt password hashes. You can use it with docker:
```sh
docker run ghcr.io/wg-easy/wg-easy:14 wgpw YOUR_PASSWORD
```
You will see an output similar to this:
```sh
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW'
```
In this example, the `$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW` string is your hashed password. For using it with docker-compose, you need to escape each `$` characters by adding another `$` before them, or they will get interpreted as variables. The final password you can use in docker-compose will look like this:
```sh
$$2b$$12$$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW
```
+437 -661
View File
File diff suppressed because it is too large Load Diff
+66 -121
View File
@@ -1,78 +1,49 @@
# WireGuard Easy
[![Build & Publish latest Image](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml/badge.svg?branch=production)](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml)
[![Build & Publish Docker Image to Docker Hub](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml/badge.svg?branch=production)](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml)
[![Lint](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml)
[![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)
<!-- TODO: remove after release -->
> [!WARNING]
> You are viewing the README of the pre-release of v15.
> If you want to setup wg-easy right now. Read the README in the production branch here: [README](https://github.com/wg-easy/wg-easy/tree/production) or here for the last nightly: [README](https://github.com/wg-easy/wg-easy/tree/c6dce0f6fb2e28e7e40ddac1498bd67e9bb17cba)
![Docker](https://img.shields.io/docker/pulls/weejewel/wg-easy.svg)
[![Sponsor](https://img.shields.io/github/sponsors/weejewel)](https://github.com/sponsors/WeeJeWel)
![GitHub Stars](https://img.shields.io/github/stars/wg-easy/wg-easy)
You have found the easiest way to install & manage WireGuard on any Linux host!
<!-- TOOD: update screenshot -->
<p align="center">
<img src="./assets/screenshot.png" width="802" />
</p>
## Features
- All-in-one: WireGuard + Web UI.
- Easy installation, simple to use.
- List, create, edit, delete, enable & disable clients.
- Show a client's QR code.
- Download a client's configuration file.
- Statistics for which clients are connected.
- Tx/Rx charts for each connected client.
- Gravatar support.
- Automatic Light / Dark Mode
- Multilanguage Support
- One Time Links
- Client Expiration
- Prometheus metrics support
- IPv6 support
- CIDR 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)
- [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/)
* All-in-one: WireGuard + Web UI.
* Easy installation, simple to use.
* List, create, edit, delete, enable & disable clients.
* Show a client's QR code.
* Download a client's configuration file.
* Statistics for which clients are connected.
* Tx/Rx charts for each connected client.
* Gravatar support.
* Automatic Light / Dark Mode
* Multilanguage Support
* UI_TRAFFIC_STATS (default off)
## Requirements
- A host with a kernel that supports WireGuard (all modern kernels).
- A host with Docker installed.
* 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). |
This branch is only for the v14 release of WireGuard Easy.
For newer versions, please refer to the [master branch](https://github.com/wg-easy/wg-easy/tree/master).
## Installation
### 1. Install Docker
If you haven't installed Docker yet, install it by running as root:
If you haven't installed Docker yet, install it by running:
```shell
```bash
curl -sSL https://get.docker.com | sh
sudo usermod -aG docker $(whoami)
exit
```
@@ -80,88 +51,79 @@ And log in again.
### 2. Run WireGuard Easy
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`.
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 \
--name=wg-easy \
-e LANG=de \
-e WG_HOST=<🚨YOUR_SERVER_IP> \
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
-e PORT=51821 \
--name wg-easy \
--ip6 fdcc:ad94:bacf:61a3::2a \
--ip 10.42.42.42 \
-e WG_PORT=51820 \
-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 \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--restart unless-stopped \
ghcr.io/wg-easy/wg-easy
ghcr.io/wg-easy/wg-easy:14
```
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
>
> 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI. See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash.
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`
-->
WireGuard Easy can be launched with Docker Compose as well - just download
[`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
execute `docker compose up --detach`.
### 3. Sponsor
Are you enjoying this project? Consider donating.
Founder: [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
Maintainer: [Buy kaaax0815 a coffee!](https://github.com/sponsors/kaaax0815) ☕
<!-- TOOD: add to docs
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
## Options
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
| 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 |
| Env | Default | Example | Description |
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
> If you change `WG_PORT`, make sure to also change the exposed port.
## Updating
To update to the latest version, simply run:
```shell
```bash
docker stop wg-easy
docker rm wg-easy
docker pull ghcr.io/wg-easy/wg-easy
docker pull ghcr.io/wg-easy/wg-easy:14
```
And then run the `docker run -d \ ...` command above again.
@@ -173,20 +135,3 @@ 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.
## 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).
-->
## License
This project is licensed under the AGPL-3.0-only License - see the [LICENSE](LICENSE) file for details
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security
"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld
+5 -12
View File
@@ -1,13 +1,10 @@
services:
wg-easy:
build:
dockerfile: ./Dockerfile.dev
command: pnpm run dev
dockerfile: ./Dockerfile
command: npm run serve
volumes:
- ./src/:/app/
- temp:/app/.nuxt/
- temp1:/app/node_modules/
- /lib/modules:/lib/modules:ro
# - ./data/:/etc/wireguard
ports:
- "51820:51820/udp"
@@ -15,10 +12,6 @@ services:
cap_add:
- NET_ADMIN
- SYS_MODULE
# folders should be generated inside container
volumes:
temp:
driver: local
temp1:
driver: local
environment:
# - PASSWORD_HASH=p
- WG_HOST=192.168.1.233
+26 -25
View File
@@ -3,21 +3,35 @@ volumes:
services:
wg-easy:
#environment:
# Optional:
# - PORT=51821
# - HOST=0.0.0.0
# - INSECURE=false
environment:
# Change Language:
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi)
- LANG=de
# ⚠️ Required:
# Change this to your host's public address
- WG_HOST=raspberrypi.local
image: ghcr.io/wg-easy/wg-easy
# Optional:
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG # (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
# - PORT=51821
# - WG_PORT=51820
# - WG_CONFIG_PORT=92820
# - WG_DEFAULT_ADDRESS=10.8.0.x
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_MTU=1420
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - WG_PERSISTENT_KEEPALIVE=25
# - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt
# - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
# - UI_TRAFFIC_STATS=true
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
image: ghcr.io/wg-easy/wg-easy:14
container_name: wg-easy
networks:
wg:
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
volumes:
- etc_wireguard:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
@@ -25,20 +39,7 @@ services:
cap_add:
- NET_ADMIN
- SYS_MODULE
# - NET_RAW # ⚠️ Uncomment if using Podman
# - NET_RAW # ⚠️ Uncomment if using Podman
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
networks:
wg:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64
-5
View File
@@ -1,5 +0,0 @@
---
title: API
---
TODO
@@ -1,5 +0,0 @@
---
title: Optional Configuration
---
TODO
@@ -1,47 +0,0 @@
---
title: Migrate from v14 to v15
---
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.
## Migration
### Backup
Before you start the migration, make sure to backup your existing configuration files.
Go into the Web Ui and click the Backup button, this should download a `wg0.json` file.
Or download the `wg0.json` file from your container volume to your pc.
You will need this file for the migration
### Remove old container
1. Stop the running container
If you are using `docker run`
```shell
docker stop wg-easy
```
If you are using `docker-compose`
```shell
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.
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.
### Done
You have now successfully migrated to `v15` of `wg-easy`.
-23
View File
@@ -1,23 +0,0 @@
---
title: General Information
---
## Coding Style
When refactoring, writing or altering files, adhere to these rules:
1. **Adjust your style of coding to the style that is already present**! Even if you do not like it, this is due to consistency. There was a lot of work involved in making all files consistent.
2. **Use `pnpm lint` to check your scripts**! Your contributions are checked by GitHub Actions too, so you will need to do this.
3. **Use the provided `.vscode/settings.json`** file.
## Documentation
Make sure to select `nightly` in the dropdown menu at the top. Navigate to the page you would like to edit and click the edit button in the top right. This allows you to make changes and create a pull-request.
Alternatively you can make the changes locally. For that you'll need to have Docker installed. Run
```sh
pnpm docs:serve
```
This serves the documentation on your local machine on port `8080`. Each change will be hot-reloaded onto the page you view, just edit, save and look at the result.
@@ -1,58 +0,0 @@
---
title: Issues and Pull Requests
---
This project is Open Source. That means that you can contribute on enhancements, bug fixing or improving the documentation.
## Opening an Issue
/// note | Attention
**Before opening an issue**, read the [`README`][github-file-readme] carefully, study the docs for your version (maybe [latest][docs-latest]) and your search engine you trust. The issue tracker is not meant to be used for unrelated questions!
///
When opening an issue, please provide details use case to let the community reproduce your problem.
/// note | Attention
**Use the issue templates** to provide the necessary information. Issues which do not use these templates are not worked on and closed.
///
By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution.
Maintainers take the time to improve on this project and help by solving issues together. It is therefore expected from others to make an effort and **comply with the rules**.
### 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!
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`_).
Markdown formatting can be used in almost all text fields (_unless stated otherwise in the description_).
Be as precise as possible, and if in doubt, it's best to add more information that too few.
When an option is marked with "not officially supported" / "unsupported", then support is dependent on availability from specific maintainers.
## Pull Requests
/// question | Motivation
You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results.
///
### Submit a Pull-Request
The development workflow is the following:
1. Fork the project
2. Write the code that is needed :D
3. Document your improvements if necessary
4. [Commit][commit] (and [sign your commit][gpg]), push and create a pull-request to merge into `master`. Please **use the pull-request template** to provide a minimum of contextual information and make sure to meet the requirements of the checklist.
Pull requests are automatically tested against the CI and will be reviewed when tests pass. When your changes are validated, your branch is merged. CI builds the new `:nightly` image every night and your changes will be includes in the next version release.
[docs-latest]: https://wg-easy.github.io/wg-easy/latest
[github-file-readme]: https://github.com/wg-easy/wg-easy/blob/master/README.md
[commit]: https://help.github.com/articles/closing-issues-via-commit-messages/
[gpg]: https://docs.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key
@@ -1,56 +0,0 @@
---
title: Basic Installation
---
<!-- TOOD: add docs for pihole, nginx, caddy, traefik -->
## Requirements
1. You need to have a host that you can manage
2. You need to have a domain name or a public IP address
3. You need a supported architecture (x86_64, arm64)
4. You need curl installed on your host
## Install Docker
Follow the Docs here: <https://docs.docker.com/engine/install/> and install Docker on your host.
## Install `wg-easy`
1. Create a directory for the configuration files (you can choose any directory you like):
```shell
DIR=/docker/wg-easy
sudo mkdir -p $DIR
```
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
```
3. Start `wg-easy`
```shell
sudo docker-compose -f $DIR/docker-compose.yml up -d
```
## Setup Firewall
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
## Access the Web UI
Open your browser and navigate to `https://<your-domain>:51821` or `https://<your-ip>:51821`.
Follow the instructions to set up your WireGuard VPN.
@@ -1,5 +0,0 @@
---
title: Without Docker
---
TODO
-96
View File
@@ -1,96 +0,0 @@
---
title: Podman
---
This guide will show you how to run `wg-easy` with rootful Podman and nftables.
## Requirements
1. Podman installed with version 4.4 or higher
## Configuration
Create a Folder for the configuration files:
```shell
sudo mkdir -p /etc/containers/systemd/wg-easy
sudo mkdir -p /etc/containers/volumes/wg-easy
```
Create a file `/etc/containers/systemd/wg-easy/wg-easy.container` with the following content:
```ini
[Container]
ContainerName=wg-easy
Image=ghcr.io/wg-easy/wg-easy:latest
Volume=/etc/containers/volumes/wg-easy:/etc/wireguard:Z
Network=wg-easy.network
PublishPort=51820:51820/udp
PublishPort=51821:51821/tcp
AddCapability=NET_ADMIN
AddCapability=SYS_MODULE
AddCapability=NET_RAW
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
[Install]
# this is used to start the container on boot
WantedBy=default.target
```
Create a file `/etc/containers/systemd/wg-easy/wg-easy.network` with the following content:
```ini
[Network]
NetworkName=wg-easy
IPv6=true
```
## Load Kernel Modules
You will need to load the following kernel modules
```txt
wireguard
nft_masq
```
Create a file `/etc/modules-load.d/wg-easy.conf` with the following content:
```txt
wireguard
nft_masq
```
## Start the Container
```shell
sudo systemctl daemon-reload
sudo systemctl start wg-easy
```
## Edit Hooks
In the Admin Panel of your WireGuard server, go to the `Hooks` tab and add the following hook:
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;
```
2. PostDown
```shell
nft delete table inet wg_table
```
<!--
TODO: improve docs after better nftables support
TODO: fix accept web ui port
-->
-92
View File
@@ -1,92 +0,0 @@
---
title: Getting Started
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.
## Preliminary Steps
Before you can get started with deploying your own VPN, there are some requirements to be met:
1. You need to have a host that you can manage
2. You need to have a domain name or a public IP address
3. You need a supported architecture (x86_64, arm64)
### Host Setup
There are a few requirements for a suitable host system:
1. You need to have a container runtime installed
/// note | About the Container Runtime
On the host, you need to have a suitable container runtime (like _Docker_ or _Podman_) installed. We assume [_Docker Compose_][docker-compose] is [installed][docker-compose-installation]. We have aligned file names and configuration conventions with the latest [Docker Compose specification][docker-compose-specification].
If you're using podman, make sure to read the related [documentation][docs-podman].
///
[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
## Deploying the Actual Image
### Tagging Convention
To understand which tags you should use, read this section carefully. [Our CI][github-ci] will automatically build, test and push new images to the following container registry:
1. GitHub Container Registry ([`ghcr.io/wg-easy/wg-easy`][ghcr-image])
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` |
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`).
[github-ci]: https://github.com/wg-easy/wg-easy/actions
[ghcr-image]: https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy
[semver]: https://semver.org/
### Get All Files
Issue the following command to acquire the necessary file:
```shell
wget "https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml"
```
### Start the Container
To start the container, issue the following command:
```shell
sudo docker compose up -d
```
### Configuration Steps
Now follow the setup process in your web browser
### Stopping the Container
To stop the container, issue the following command:
```shell
sudo docker compose down
```
/// 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.
///
**That's it! It really is that easy**.
If you need more help you can read the [Basic Installation Tutorial][basic-installation].
[basic-installation]: ./examples/tutorials/basic-installation.md
-35
View File
@@ -1,35 +0,0 @@
---
title: Home
hide:
- navigation
---
# Welcome to the Documentation for `wg-easy`
/// info | This Documentation is Versioned
**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.
[docs-tagging]: ./usage.md#tagging-convention
## About
`wg-easy` is the easiest way to run WireGuard VPN + Web-based Admin UI.
## Contents
### 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].
[docs-usage]: ./usage.md
[docs-examples]: ./examples/tutorials/basic-installation.md
### Contributing
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
-81
View File
@@ -1,81 +0,0 @@
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>'
repo_url: https://github.com/wg-easy/wg-easy
repo_name: wg-easy
edit_uri: "edit/master/docs/content"
docs_dir: "content/"
site_url: https://wg-easy.github.io/wg-easy
theme:
name: material
favicon: assets/logo/favicon.png
logo: assets/logo/logo.png
icon:
repo: fontawesome/brands/github
features:
- navigation.tabs
- navigation.top
- navigation.expand
- navigation.instant
- content.action.edit
- content.action.view
- content.code.annotate
palette:
# Light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: grey
accent: red
toggle:
icon: material/weather-night
name: Switch to dark mode
# Dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: grey
accent: red
toggle:
icon: material/weather-sunny
name: Switch to light mode
extra:
version:
provider: mike
markdown_extensions:
- toc:
anchorlink: true
- abbr
- attr_list
- pymdownx.blocks.admonition:
types:
- danger
- note
- info
- question
- warning
- pymdownx.details
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tabbed:
alternate_style: true
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.magiclink
- pymdownx.inlinehilite
- pymdownx.tilde
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
-4
View File
@@ -1,4 +0,0 @@
mkdocs-material
pillow
cairosvg
mike
+11
View File
@@ -0,0 +1,11 @@
{
"name": "wg-easy",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"version": "1.0.1"
}
}
}
+8 -8
View File
@@ -1,10 +1,10 @@
{
"version": "1.0.0",
"private": true,
"version": "1.0.1",
"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"
},
"packageManager": "pnpm@10.5.2"
}
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .",
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
"sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy",
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
}
}
-9
View File
@@ -1,9 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.: {}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "athom",
"ignorePatterns": [
"**/vendor/*.js"
],
"rules": {
"consistent-return": "off",
"no-shadow": "off",
"max-len": "off"
}
}
-26
View File
@@ -1,26 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
wg-easy.db
-1
View File
@@ -1 +0,0 @@
public-hoist-pattern[]=@libsql/linux*
-2
View File
@@ -1,2 +0,0 @@
pnpm-lock.yaml
server/database/migrations/meta
-7
View File
@@ -1,7 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss"]
}
-57
View File
@@ -1,57 +0,0 @@
<template>
<ToastProvider>
<NuxtLayout>
<NuxtPage />
<ToastViewport
class="fixed bottom-0 right-0 z-[2147483647] m-0 flex w-[390px] max-w-[100vw] list-none flex-col gap-[10px] p-[var(--viewport-padding)] outline-none [--viewport-padding:_25px]"
>
<BaseToast ref="toastRef" />
</ToastViewport>
</NuxtLayout>
</ToastProvider>
</template>
<script setup lang="ts">
const toast = useToast();
const toastRef = useTemplateRef('toastRef');
toast.setToast(toastRef);
// make sure to fetch release early
useGlobalStore();
useHead({
bodyAttrs: {
class: 'bg-gray-50 dark:bg-neutral-800',
},
link: [
{
rel: 'manifest',
href: '/manifest.json',
},
{
rel: 'icon',
type: 'image/png',
href: '/favicon.png',
},
{
rel: 'apple-touch-icon',
href: '/apple-touch-icon.png',
},
],
meta: [
{
name: 'mobile-web-app-capable',
content: 'yes',
},
{
name: 'apple-mobile-web-app-capable',
content: 'yes',
},
{
name: 'apple-mobile-web-app-status-bar-style',
content: 'black-translucent',
},
],
title: 'WireGuard',
});
</script>
-34
View File
@@ -1,34 +0,0 @@
<template>
<BaseDialog :trigger-class="triggerClass">
<template #trigger><slot /></template>
<template #title>{{ $t('admin.interface.changeCidr') }}</template>
<template #description>
<FormGroup>
<FormTextField id="ipv4Cidr" v-model="ipv4Cidr" label="IPv4" />
<FormTextField id="ipv6Cidr" v-model="ipv6Cidr" label="IPv6" />
</FormGroup>
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('change', ipv4Cidr, ipv6Cidr)">
{{ $t('dialog.change') }}
</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>
defineEmits(['change']);
const props = defineProps<{
triggerClass?: string;
ipv4Cidr: string;
ipv6Cidr: string;
}>();
const ipv4Cidr = ref(props.ipv4Cidr);
const ipv6Cidr = ref(props.ipv6Cidr);
</script>
-20
View File
@@ -1,20 +0,0 @@
<template>
<AvatarRoot
class="mr-2 inline-flex select-none items-center justify-center overflow-hidden rounded-full align-middle"
>
<AvatarImage
class="h-full w-full rounded-[inherit] object-cover"
:src="img ?? ''"
/>
<AvatarFallback
class="leading-1 flex h-full w-full items-center justify-center bg-white text-sm font-medium"
:delay-ms="600"
>
<slot />
</AvatarFallback>
</AvatarRoot>
</template>
<script lang="ts" setup>
defineProps<{ img?: string }>();
</script>
-26
View File
@@ -1,26 +0,0 @@
<template>
<component
:is="elementType"
role="button"
class="inline-flex items-center rounded border-2 border-gray-100 px-4 py-2 text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white dark:border-neutral-600 dark:text-neutral-200"
v-bind="attrs"
>
<slot />
</component>
</template>
<script setup lang="ts">
const props = defineProps({
as: {
type: String,
default: 'button',
},
});
const elementType = computed(() => props.as);
const attrs = computed(() => {
const { as, ...attrs } = props;
return attrs;
});
</script>
-20
View File
@@ -1,20 +0,0 @@
<template>
<ClientOnly>
<apexchart
width="100%"
height="100%"
v-bind="$attrs"
:options="options"
:series="series"
/>
</ClientOnly>
</template>
<script setup lang="ts">
import type { VueApexChartsComponent } from 'vue3-apexcharts';
defineProps<{
options: VueApexChartsComponent['options'];
series: VueApexChartsComponent['series'];
}>();
</script>
-31
View File
@@ -1,31 +0,0 @@
<template>
<DialogRoot :modal="true">
<DialogTrigger :class="triggerClass"><slot name="trigger" /></DialogTrigger>
<DialogPortal>
<DialogOverlay
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"
>
<DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
>
<slot name="title" />
</DialogTitle>
<DialogDescription
class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300"
>
<slot name="description" />
</DialogDescription>
<div class="mt-6 flex justify-end gap-2">
<slot name="actions" />
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
<script lang="ts" setup>
defineProps<{ triggerClass?: string }>();
</script>
-10
View File
@@ -1,10 +0,0 @@
<template>
<input
v-model="data"
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"
/>
</template>
<script lang="ts" setup>
const data = defineModel<unknown>();
</script>
-17
View File
@@ -1,17 +0,0 @@
<template>
<SwitchRoot
:id="id"
v-model:checked="data"
:name="id"
class="relative flex h-6 w-10 cursor-default rounded-full bg-gray-200 shadow-sm focus-within:outline focus-within:outline-red-700 data-[state=checked]:bg-red-800 dark:bg-neutral-400"
>
<SwitchThumb
class="my-auto block h-4 w-4 translate-x-1 rounded-full bg-white shadow-sm transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[20px]"
/>
</SwitchRoot>
</template>
<script lang="ts" setup>
defineProps<{ id?: string }>();
const data = defineModel<boolean>();
</script>
-46
View File
@@ -1,46 +0,0 @@
<template>
<ToastRoot
v-for="(e, i) in count"
:key="i"
:class="[
`grid grid-cols-[auto_max-content] items-center gap-x-3 rounded-md p-3 text-neutral-200 shadow-lg [grid-template-areas:_'title_action'_'description_action'] data-[swipe=cancel]:translate-x-0 data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]`,
{
'bg-green-800': e.type === 'success',
'bg-red-800': e.type === 'error',
},
]"
>
<ToastTitle class="mb-1 text-sm font-medium [grid-area:_title]">
{{ e.title }}
</ToastTitle>
<ToastDescription class="m-0 text-sm [grid-area:_description]">{{
e.message
}}</ToastDescription>
<ToastAction as-child alt-text="toast" class="[grid-area:_action]">
<slot />
</ToastAction>
<ToastClose aria-label="Close">
<span aria-hidden>×</span>
</ToastClose>
</ToastRoot>
</template>
<script setup lang="ts">
import {
ToastAction,
ToastClose,
ToastDescription,
ToastRoot,
ToastTitle,
} from 'radix-vue';
defineExpose({
publish,
});
const count = reactive<ToastParams[]>([]);
function publish(e: ToastParams) {
count.push({ type: e.type, title: e.title, message: e.message });
}
</script>
-24
View File
@@ -1,24 +0,0 @@
<template>
<TooltipProvider>
<TooltipRoot>
<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"
>
<slot />
</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]"
:side-offset="5"
>
{{ text }}
<TooltipArrow class="fill-gray-600" :width="8" />
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
</TooltipProvider>
</template>
<script lang="ts" setup>
defineProps<{ text: string }>();
</script>
-11
View File
@@ -1,11 +0,0 @@
<template>
<span class="inline-block">
{{ client.ipv4Address }}, {{ client.ipv6Address }}
</span>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
-30
View File
@@ -1,30 +0,0 @@
<template>
<div class="relative mt-2 h-10 w-10 self-start rounded-full bg-gray-50">
<BaseAvatar :img="client.avatar" class="h-10 w-10">
<IconsAvatar class="h-6 w-6 text-gray-300" />
</BaseAvatar>
<div
v-if="
isPeerConnected({
latestHandshakeAt: client.latestHandshakeAt
? new Date(client.latestHandshakeAt)
: null,
})
"
>
<div
class="absolute -bottom-1 -right-1 h-4 w-4 animate-ping rounded-full bg-red-100 p-1 dark:bg-red-100"
/>
<div
class="absolute bottom-0 right-0 h-2 w-2 rounded-full bg-red-800 dark:bg-red-600"
/>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
-136
View File
@@ -1,136 +0,0 @@
<template>
<div
:class="`absolute bottom-0 left-0 right-0 z-0 h-6 ${globalStore.uiChartType === 'line' && 'line-chart'}`"
>
<BaseChart :options="chartOptionsTX" :series="client.transferTxSeries" />
</div>
<div
:class="`absolute left-0 right-0 top-0 z-0 h-6 ${globalStore.uiChartType === 'line' && 'line-chart'}`"
>
<BaseChart
:options="chartOptionsRX"
:series="client.transferRxSeries"
style="transform: scaleY(-1)"
/>
</div>
</template>
<script setup lang="ts">
import type { ApexOptions } from 'apexcharts';
defineProps<{
client: LocalClient;
}>();
const globalStore = useGlobalStore();
const theme = useTheme();
const chartOptionsTX = computed(() => {
const opts = {
...chartOptions,
colors: [CHART_COLORS.tx[theme.value]],
};
opts.chart.type = globalStore.uiChartType;
opts.stroke.width = UI_CHART_PROPS[globalStore.uiChartType].strokeWidth;
return opts;
});
const chartOptionsRX = computed(() => {
const opts = {
...chartOptions,
colors: [CHART_COLORS.rx[theme.value]],
};
opts.chart.type = globalStore.uiChartType;
opts.stroke.width = UI_CHART_PROPS[globalStore.uiChartType].strokeWidth;
return opts;
});
const chartOptions = {
chart: {
type: undefined as ApexChart['type'],
background: 'transparent',
stacked: false,
toolbar: {
show: false,
},
animations: {
enabled: false,
},
parentHeightOffset: 0,
sparkline: {
enabled: true,
},
},
colors: [],
stroke: {
curve: 'smooth',
width: 0,
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'vertical',
shadeIntensity: 0,
gradientToColors: CHART_COLORS.gradient[theme.value],
inverseColors: false,
opacityTo: 0,
stops: [0, 100],
},
},
dataLabels: {
enabled: false,
},
plotOptions: {
bar: {
horizontal: false,
},
},
xaxis: {
labels: {
show: false,
},
axisTicks: {
show: false,
},
axisBorder: {
show: false,
},
},
yaxis: {
labels: {
show: false,
},
min: 0,
},
tooltip: {
enabled: false,
},
legend: {
show: false,
},
grid: {
show: false,
padding: {
left: -10,
right: 0,
bottom: -15,
top: -15,
},
column: {
opacity: 0,
},
xaxis: {
lines: {
show: false,
},
},
},
} satisfies ApexOptions;
</script>
<style scoped lang="css">
.line-chart .apexcharts-svg {
transform: translateY(3px);
}
</style>
@@ -1,51 +0,0 @@
<template>
<ClientCardCharts :client="client" />
<div
class="relative z-10 flex flex-col justify-between gap-3 px-3 py-3 sm:flex-row md:py-5"
>
<div class="flex w-full items-center gap-3 md:gap-4">
<ClientCardAvatar :client="client" />
<div class="flex w-full flex-col gap-2 xxs:flex-row">
<div class="flex flex-grow flex-col gap-1">
<ClientCardName :client="client" />
<div
class="flex flex-col pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400"
>
<div>
<ClientCardAddress :client="client" />
</div>
<div>
<ClientCardLastSeen :client="client" />
</div>
</div>
<ClientCardOneTimeLink :client="client" />
<ClientCardExpireDate :client="client" />
</div>
<div
class="mt-px flex shrink-0 items-center justify-end gap-2 text-xs text-gray-400 dark:text-neutral-400"
>
<ClientCardTransfer :client="client" />
</div>
</div>
</div>
<div class="flex items-center justify-end">
<div
class="flex items-center justify-between gap-1 text-gray-400 dark:text-neutral-400"
>
<ClientCardSwitch :client="client" />
<ClientCardEdit :client="client" />
<ClientCardQRCode :client="client" />
<ClientCardConfig :client="client" />
<ClientCardOneTimeLinkBtn :client="client" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<a
:href="'/api/client/' + client.id + '/configuration'"
download
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('client.downloadConfig')"
>
<IconsDownload class="w-5" />
</a>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
-14
View File
@@ -1,14 +0,0 @@
<template>
<NuxtLink
class="rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:to="`/clients/${client.id}`"
>
<IconsEdit class="w-5" />
</NuxtLink>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
@@ -1,23 +0,0 @@
<template>
<div
class="block pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400"
>
<span class="inline-block">{{ expiredDateFormat(client.expiresAt) }}</span>
</div>
</template>
<script setup lang="ts">
defineProps<{ client: LocalClient }>();
const { t, locale } = useI18n();
function expiredDateFormat(value: string | null) {
if (value === null) return t('client.permanent');
const dateTime = new Date(value);
return dateTime.toLocaleDateString(locale.value, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
</script>
@@ -1,16 +0,0 @@
<template>
<span
v-if="client.latestHandshakeAt"
:title="$t('client.lastSeen') + $d(new Date(client.latestHandshakeAt))"
>
{{ timeago(new Date(client.latestHandshakeAt)) }}
</span>
</template>
<script setup lang="ts">
import { format as timeago } from 'timeago.js';
defineProps<{
client: LocalClient;
}>();
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<div
class="text-sm text-gray-700 md:text-base dark:text-neutral-200"
:title="$t('client.createdOn') + $d(new Date(client.createdAt))"
>
<span class="border-b-2 border-t-2 border-transparent">
{{ client.name }}
</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
@@ -1,51 +0,0 @@
<template>
<div v-if="client.oneTimeLink !== null" class="text-xs text-gray-400">
<a :href="'./cnf/' + client.oneTimeLink.oneTimeLink">{{ path }}</a>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ client: LocalClient }>();
const path = ref('Loading...');
const timer = ref<NodeJS.Timeout | null>(null);
const { localeProperties } = useI18n();
onMounted(() => {
timer.value = setIntervalImmediately(() => {
if (props.client.oneTimeLink === null) {
return;
}
const timeLeft =
new Date(props.client.oneTimeLink.expiresAt).getTime() - Date.now();
if (timeLeft <= 0) {
path.value = `${document.location.protocol}//${document.location.host}/cnf/${props.client.oneTimeLink.oneTimeLink} (00:00)`;
return;
}
const formatter = new Intl.DateTimeFormat(localeProperties.value.language, {
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
});
const minutes = Math.floor(timeLeft / 60000);
const seconds = Math.floor((timeLeft % 60000) / 1000);
const date = new Date(0);
date.setMinutes(minutes);
date.setSeconds(seconds);
path.value = `${document.location.protocol}//${document.location.host}/cnf/${props.client.oneTimeLink.oneTimeLink} (${formatter.format(date)})`;
}, 1000);
});
onUnmounted(() => {
if (timer.value) {
clearTimeout(timer.value);
}
});
</script>
@@ -1,32 +0,0 @@
<template>
<button
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('client.otlDesc')"
@click="showOneTimeLink"
>
<IconsLink class="w-5" />
</button>
</template>
<script setup lang="ts">
const props = defineProps<{ client: LocalClient }>();
const clientsStore = useClientsStore();
const _showOneTimeLink = useSubmit(
`/api/client/${props.client.id}/generateOneTimeLink`,
{
method: 'post',
},
{
revert: async () => {
await clientsStore.refresh();
},
noSuccessToast: true,
}
);
function showOneTimeLink() {
return _showOneTimeLink(undefined);
}
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<ClientsQRCodeDialog :qr-code="`./api/client/${client.id}/qrcode.svg`">
<div
class="rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white"
:title="$t('client.showQR')"
>
<IconsQRCode class="w-5" />
</div>
</ClientsQRCodeDialog>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
-53
View File
@@ -1,53 +0,0 @@
<template>
<BaseSwitch
v-model="enabled"
:title="
client.enabled ? $t('client.disableClient') : $t('client.enableClient')
"
@click="toggleClient"
/>
</template>
<script setup lang="ts">
const props = defineProps<{
client: LocalClient;
}>();
const enabled = ref(props.client.enabled);
const clientsStore = useClientsStore();
const _disableClient = useSubmit(
`/api/client/${props.client.id}/disable`,
{
method: 'post',
},
{
revert: async () => {
await clientsStore.refresh();
},
noSuccessToast: true,
}
);
const _enableClient = useSubmit(
`/api/client/${props.client.id}/enable`,
{
method: 'post',
},
{
revert: async () => {
await clientsStore.refresh();
},
noSuccessToast: true,
}
);
async function toggleClient() {
if (props.client.enabled) {
await _disableClient(undefined);
} else {
await _enableClient(undefined);
}
}
</script>
@@ -1,45 +0,0 @@
<template>
<!-- Transfer TX -->
<div v-if="client.transferTx" class="min-w-20 md:min-w-24">
<span
class="flex gap-1"
:title="$t('client.totalDownload') + bytes(client.transferTx)"
>
<IconsArrowDown class="mt-0.5 inline h-3 align-middle" />
<div>
<span class="text-gray-700 dark:text-neutral-200"
>{{ bytes(client.transferTxCurrent) }}/s</span
>
<!-- Total TX -->
<br /><span class="font-regular" style="font-size: 0.85em">{{
bytes(client.transferTx)
}}</span>
</div>
</span>
</div>
<!-- Transfer RX -->
<div v-if="client.transferRx" class="min-w-20 md:min-w-24">
<span
class="flex gap-1"
:title="$t('client.totalUpload') + bytes(client.transferRx)"
>
<IconsArrowUp class="mt-0.5 inline h-3 align-middle" />
<div>
<span class="text-gray-700 dark:text-neutral-200"
>{{ bytes(client.transferRxCurrent) }}/s</span
>
<!-- Total RX -->
<br /><span class="font-regular" style="font-size: 0.85em">{{
bytes(client.transferRx)
}}</span>
</div>
</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
</script>
@@ -1,53 +0,0 @@
<template>
<BaseDialog :trigger-class="triggerClass">
<template #trigger>
<slot />
</template>
<template #title>
{{ $t('client.new') }}
</template>
<template #description>
<div class="flex flex-col">
<FormTextField id="name" v-model="name" :label="$t('client.name')" />
<FormDateField
id="expiresAt"
v-model="expiresAt"
:label="$t('client.expireDate')"
/>
</div>
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="createClient">{{ $t('client.create') }}</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>
const name = ref<string>('');
const expiresAt = ref<string | null>(null);
const clientsStore = useClientsStore();
const { t } = useI18n();
defineProps<{ triggerClass?: string }>();
function createClient() {
return _createClient({ name: name.value, expiresAt: expiresAt.value });
}
const _createClient = useSubmit(
'/api/client',
{
method: 'post',
},
{
revert: () => clientsStore.refresh(),
successMsg: t('client.created'),
}
);
</script>
@@ -1,26 +0,0 @@
<template>
<BaseDialog :trigger-class="triggerClass">
<template #trigger><slot /></template>
<template #title>{{ $t('client.deleteClient') }}</template>
<template #description>
{{ $t('client.deleteDialog1') }}
<strong>{{ clientName }}</strong
>? {{ $t('client.deleteDialog2') }}
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('delete')">{{
$t('client.deleteClient')
}}</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>
defineEmits(['delete']);
defineProps<{ triggerClass?: string; clientName: string }>();
</script>
-11
View File
@@ -1,11 +0,0 @@
<template>
<p class="m-10 text-center text-sm text-gray-400 dark:text-neutral-400">
{{ $t('client.empty') }}<br /><br />
<ClientsCreateDialog>
<BaseButton as="span">
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm">{{ $t('client.new') }}</span>
</BaseButton>
</ClientsCreateDialog>
</p>
</template>
-13
View File
@@ -1,13 +0,0 @@
<template>
<div
v-for="client in clientsStore.clients"
:key="client.id"
class="relative overflow-hidden border-b border-solid border-gray-100 last:border-b-0 dark:border-neutral-600"
>
<ClientCard :client="client" />
</div>
</template>
<script setup lang="ts">
const clientsStore = useClientsStore();
</script>
-8
View File
@@ -1,8 +0,0 @@
<template>
<ClientsCreateDialog>
<BaseButton as="span">
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden">{{ $t('client.newShort') }}</span>
</BaseButton>
</ClientsCreateDialog>
</template>
@@ -1,19 +0,0 @@
<template>
<BaseDialog>
<template #trigger>
<slot />
</template>
<template #description>
<img :src="qrCode" />
</template>
<template #actions>
<DialogClose>
<BaseButton>{{ $t('dialog.cancel') }}</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script setup lang="ts">
defineProps<{ qrCode: string }>();
</script>
-20
View File
@@ -1,20 +0,0 @@
<template>
<BaseButton @click="toggleSort">
<IconsArrowDown
v-if="globalStore.sortClient === true"
class="w-4 md:mr-2"
/>
<IconsArrowUp v-else class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden"> {{ $t('client.sort') }}</span>
</BaseButton>
</template>
<script setup lang="ts">
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();
function toggleSort() {
globalStore.sortClient = !globalStore.sortClient;
clientsStore.refresh().catch(console.error);
}
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<input
:value="label"
:type="type ?? 'button'"
class="col-span-2 rounded-lg border-2 border-gray-100 py-2 text-gray-500 hover:border-red-800 hover:bg-red-800 hover:text-white 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"
/>
</template>
<script lang="ts" setup>
import type { InputTypeHTMLAttribute } from 'vue';
defineProps<{
label: string;
type?: InputTypeHTMLAttribute;
}>();
</script>
-51
View File
@@ -1,51 +0,0 @@
<template>
<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">
<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" value="-" @click="del(i)" />
</div>
</div>
<div class="mt-2">
<BaseButton
as="input"
type="button"
:value="$t('form.add')"
@click="add"
/>
</div>
</div>
</template>
<script lang="ts" setup>
const data = defineModel<string[]>();
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() {
data.value?.push('');
}
function del(i: number) {
if (!data.value) {
return;
}
data.value.splice(i, 1);
}
</script>
-44
View File
@@ -1,44 +0,0 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseInput
:id="id"
:model-value="formattedDate"
:name="id"
type="date"
max="9999-12-31"
@update:model-value="updateDate"
/>
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<string | null>();
const date = ref(data);
const formattedDate = computed(() => {
return date.value ? date.value.split('T')[0] : '';
});
const updateDate = (value: unknown) => {
if (typeof value !== 'string' && value !== null) {
return;
}
const temp = value?.trim() ?? null;
if (temp === '' || temp === null) {
date.value = null;
} else {
date.value = new Date(temp).toISOString();
}
};
</script>
-5
View File
@@ -1,5 +0,0 @@
<template>
<form>
<slot />
</form>
</template>
-9
View File
@@ -1,9 +0,0 @@
<template>
<section class="grid grid-cols-1 gap-4 md:grid-cols-2">
<slot />
<Separator
decorative
class="col-span-2 h-px w-full bg-gray-100 dark:bg-neutral-600"
/>
</section>
</template>
-12
View File
@@ -1,12 +0,0 @@
<template>
<h4 class="col-span-full flex items-center py-6 text-2xl">
<slot />
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</h4>
</template>
<script lang="ts" setup>
defineProps<{ description?: string }>();
</script>
-11
View File
@@ -1,11 +0,0 @@
<template>
<RLabel :for="props.for" class="md:align-middle md:leading-10"
><slot
/></RLabel>
</template>
<script lang="ts" setup>
import { Label as RLabel } from 'radix-vue';
const props = defineProps<{ for: string }>();
</script>
-38
View File
@@ -1,38 +0,0 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="text"
:autcomplete="autocomplete"
:placeholder="placeholder"
/>
</template>
<script lang="ts" setup>
defineProps<{
id: string;
label: string;
description?: string;
autocomplete?: string;
placeholder?: string;
}>();
const data = defineModel<string | null>({
set(value) {
const temp = value?.trim() ?? null;
if (temp === '') {
return null;
}
return temp;
},
});
</script>
-17
View File
@@ -1,17 +0,0 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseInput :id="id" v-model.number="data" :name="id" type="number" />
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<number>();
</script>
-18
View File
@@ -1,18 +0,0 @@
<template>
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="password"
:autocomplete="autocomplete"
/>
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; autocomplete: string }>();
const data = defineModel<string>();
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseSwitch :id="id" v-model="data" />
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
const data = defineModel<boolean>();
</script>
-28
View File
@@ -1,28 +0,0 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseInput
:id="id"
v-model.trim="data"
:name="id"
type="text"
:autcomplete="autocomplete"
/>
</template>
<script lang="ts" setup>
defineProps<{
id: string;
label: string;
description?: string;
autocomplete?: string;
}>();
const data = defineModel<string>();
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<Toggle
:pressed="globalStore.uiShowCharts"
class="group inline-flex h-8 w-8 cursor-pointer items-center justify-center whitespace-nowrap rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
:title="$t('layout.toggleCharts')"
@update:pressed="globalStore.toggleCharts"
>
<IconsChart
class="h-5 w-5 fill-gray-400 transition group-data-[state=on]:fill-gray-600 dark:fill-neutral-600 dark:group-data-[state=on]:fill-neutral-400"
/>
</Toggle>
</template>
<script lang="ts" setup>
const globalStore = useGlobalStore();
</script>
@@ -1,44 +0,0 @@
<template>
<SelectRoot v-model="langProxy" :default-value="locale">
<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-700 dark:text-neutral-400"
aria-label="Select language"
>
<IconsLanguage class="size-3" />
<SelectValue />
<IconsArrowDown class="size-3" />
</SelectTrigger>
<SelectPortal>
<SelectContent
class="min-w-28 rounded bg-gray-300 dark:bg-neutral-500"
position="popper"
>
<SelectViewport class="p-2">
<SelectItem
v-for="(option, index) in langs"
:key="index"
:value="option.code"
class="relative flex h-6 items-center rounded px-3 text-sm leading-none outline-none hover:bg-red-800 hover:text-white data-[state=checked]:underline dark:text-white"
>
<SelectItemText>
{{ option.name }}
</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
<script setup lang="ts">
const { locales, locale, setLocale } = useI18n();
const langProxy = ref(locale);
watchEffect(() => {
setLocale(langProxy.value);
});
const langs = locales.value.sort((a, b) => a.code.localeCompare(b.code));
</script>
-11
View File
@@ -1,11 +0,0 @@
<template>
<NuxtLink to="/" class="mb-4 flex-grow self-start">
<h1 class="text-4xl font-medium dark:text-neutral-200">
<img
src="/logo.png"
width="32"
class="dark:bg mr-2 inline align-middle"
/><span class="align-middle">WireGuard</span>
</h1>
</NuxtLink>
</template>
-28
View File
@@ -1,28 +0,0 @@
<template>
<button
class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
:title="$t(`theme.${theme.preference}`)"
@click="toggleTheme"
>
<IconsSun v-if="theme.preference === 'light'" class="h-5 w-5" />
<IconsMoon
v-else-if="theme.preference === 'dark'"
class="h-5 w-5 text-neutral-400"
/>
<IconsHalfMoon v-else class="h-5 w-5 fill-gray-600 dark:fill-neutral-400" />
</button>
</template>
<script lang="ts" setup>
const theme = useTheme();
function toggleTheme() {
const themeCycle = {
system: 'light',
light: 'dark',
dark: 'system',
} as const;
theme.preference = themeCycle[theme.preference];
}
</script>
-28
View File
@@ -1,28 +0,0 @@
<template>
<div
v-if="globalStore.release?.updateAvailable"
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}`"
>
<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>
</div>
<a
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.release.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"
>
{{ $t('update.update') }}
</a>
</div>
</div>
</template>
<script lang="ts" setup>
const globalStore = useGlobalStore();
// TODO: only show this to admins
</script>
-13
View File
@@ -1,13 +0,0 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</template>
-16
View File
@@ -1,16 +0,0 @@
<template>
<svg
inline
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</template>
@@ -1,15 +0,0 @@
<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>
</template>

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