mirror of https://github.com/wg-easy/wg-easy
Browse Source
# Conflicts: # README.md # docker-compose.yml # src/config.js # src/lib/WireGuard.jspull/1714/head
271 changed files with 21101 additions and 11524 deletions
@ -1 +1,3 @@ |
|||||
/src/node_modules |
/src/node_modules |
||||
|
/src/.nuxt |
||||
|
/src/.output |
||||
|
@ -1,3 +1,3 @@ |
|||||
# These are supported funding model platforms |
# These are supported funding model platforms |
||||
|
|
||||
github: weejewel |
github: [weejewel, kaaax0815] |
||||
|
@ -1,37 +1,71 @@ |
|||||
name: Build & Publish Development |
name: Development |
||||
|
|
||||
on: |
on: |
||||
workflow_dispatch: |
workflow_dispatch: |
||||
|
|
||||
jobs: |
jobs: |
||||
deploy: |
docker: |
||||
name: Build & Deploy |
name: Build & Deploy Docker |
||||
runs-on: ubuntu-latest |
runs-on: ubuntu-latest |
||||
if: github.repository_owner == 'wg-easy' |
if: github.repository_owner == 'wg-easy' |
||||
permissions: |
permissions: |
||||
packages: write |
packages: write |
||||
contents: read |
contents: read |
||||
steps: |
steps: |
||||
- uses: actions/checkout@v4 |
- uses: actions/checkout@v4 |
||||
with: |
|
||||
ref: master |
- 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 |
||||
- name: Login to GitHub Container Registry |
with: |
||||
uses: docker/login-action@v3 |
registry: ghcr.io |
||||
with: |
username: ${{ github.actor }} |
||||
registry: ghcr.io |
password: ${{ secrets.GITHUB_TOKEN }} |
||||
username: ${{ github.actor }} |
|
||||
password: ${{ secrets.GITHUB_TOKEN }} |
- name: Build & Publish Docker Image |
||||
|
uses: docker/build-push-action@v6 |
||||
- name: Build & Publish Docker Image |
with: |
||||
uses: docker/build-push-action@v6 |
context: . |
||||
with: |
push: true |
||||
push: true |
platforms: linux/amd64,linux/arm64 |
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 |
tags: ghcr.io/wg-easy/wg-easy:development |
||||
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 |
||||
|
@ -1,38 +1,43 @@ |
|||||
name: Build Pull Request |
name: Pull Request |
||||
|
|
||||
on: |
on: |
||||
workflow_dispatch: |
workflow_dispatch: |
||||
pull_request: |
pull_request: |
||||
|
|
||||
|
concurrency: |
||||
|
group: ${{ github.workflow }}-${{ github.ref }} |
||||
|
cancel-in-progress: true |
||||
|
|
||||
jobs: |
jobs: |
||||
deploy: |
docker: |
||||
name: Build & Deploy |
name: Build Docker |
||||
runs-on: ubuntu-latest |
runs-on: ubuntu-latest |
||||
if: github.repository_owner == 'wg-easy' |
if: github.repository_owner == 'wg-easy' |
||||
permissions: |
permissions: |
||||
packages: write |
packages: write |
||||
contents: read |
contents: read |
||||
steps: |
steps: |
||||
- uses: actions/checkout@v4 |
- uses: actions/checkout@v4 |
||||
with: |
|
||||
ref: master |
|
||||
|
|
||||
- name: Set up QEMU |
- name: Set up QEMU |
||||
uses: docker/setup-qemu-action@v3 |
uses: docker/setup-qemu-action@v3 |
||||
|
|
||||
- name: Set up Docker Buildx |
- name: Set up Docker Buildx |
||||
uses: docker/setup-buildx-action@v3 |
uses: docker/setup-buildx-action@v3 |
||||
|
|
||||
- name: Login to GitHub Container Registry |
- name: Login to GitHub Container Registry |
||||
uses: docker/login-action@v3 |
uses: docker/login-action@v3 |
||||
with: |
with: |
||||
registry: ghcr.io |
registry: ghcr.io |
||||
username: ${{ github.actor }} |
username: ${{ github.actor }} |
||||
password: ${{ secrets.GITHUB_TOKEN }} |
password: ${{ secrets.GITHUB_TOKEN }} |
||||
|
|
||||
- name: Build Docker Image |
- name: Build Docker Image |
||||
uses: docker/build-push-action@v6 |
uses: docker/build-push-action@v6 |
||||
with: |
with: |
||||
push: false |
context: . |
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 |
push: false |
||||
tags: ghcr.io/wg-easy/wg-easy:pr |
platforms: linux/amd64,linux/arm64 |
||||
|
tags: ghcr.io/wg-easy/wg-easy:pr |
||||
|
cache-from: type=gha |
||||
|
cache-to: type=gha,mode=min |
||||
|
@ -1,43 +1,103 @@ |
|||||
name: Build & Publish Latest |
name: Production |
||||
|
|
||||
on: |
on: |
||||
workflow_dispatch: |
workflow_dispatch: |
||||
push: |
push: |
||||
branches: |
tags: |
||||
- production |
- "v*" |
||||
|
|
||||
jobs: |
jobs: |
||||
deploy: |
docker: |
||||
name: Build & Deploy |
name: Build & Deploy Docker |
||||
runs-on: ubuntu-latest |
runs-on: ubuntu-latest |
||||
if: github.repository_owner == 'wg-easy' |
if: | |
||||
|
github.repository_owner == 'wg-easy' && |
||||
|
startsWith(github.ref, 'refs/tags/v') |
||||
permissions: |
permissions: |
||||
packages: write |
packages: write |
||||
contents: read |
contents: read |
||||
steps: |
steps: |
||||
- uses: actions/checkout@v4 |
- uses: actions/checkout@v4 |
||||
with: |
|
||||
ref: production |
- 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 |
||||
- name: Login to GitHub Container Registry |
uses: docker/metadata-action@v5 |
||||
uses: docker/login-action@v3 |
with: |
||||
with: |
images: | |
||||
registry: ghcr.io |
ghcr.io/wg-easy/wg-easy |
||||
username: ${{ github.actor }} |
tags: | |
||||
password: ${{ secrets.GITHUB_TOKEN }} |
type=semver,pattern={{version}} |
||||
|
type=semver,pattern={{major}} |
||||
- name: Set environment variables |
type=semver,pattern={{major}}.{{minor}} |
||||
run: echo RELEASE=$(cat ./src/package.json | jq -r .release | jq -r .version) >> $GITHUB_ENV |
|
||||
|
- name: Login to GitHub Container Registry |
||||
- name: Build & Publish Docker Image |
uses: docker/login-action@v3 |
||||
uses: docker/build-push-action@v6 |
with: |
||||
with: |
registry: ghcr.io |
||||
push: true |
username: ${{ github.actor }} |
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 |
password: ${{ secrets.GITHUB_TOKEN }} |
||||
tags: ghcr.io/wg-easy/wg-easy:latest, ghcr.io/wg-easy/wg-easy:${{ env.RELEASE }} |
|
||||
|
- 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 |
||||
|
@ -1,6 +1,2 @@ |
|||||
/config |
|
||||
/wg0.conf |
|
||||
/wg0.json |
|
||||
/src/node_modules |
|
||||
.DS_Store |
.DS_Store |
||||
*.swp |
*.swp |
||||
|
@ -0,0 +1,14 @@ |
|||||
|
{ |
||||
|
"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" |
||||
|
] |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"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" |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
# 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` |
@ -0,0 +1,37 @@ |
|||||
|
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 ./ |
||||
|
|
@ -1,42 +0,0 @@ |
|||||
# wg-password |
|
||||
|
|
||||
`wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `wg-easy`, enhancing security by requiring passwords. |
|
||||
|
|
||||
## Features |
|
||||
|
|
||||
- Generate bcrypt password hashes. |
|
||||
- Easily integrate with `wg-easy` to enforce password requirements. |
|
||||
|
|
||||
## Usage with Docker |
|
||||
|
|
||||
To generate a bcrypt password hash using docker, run the following command : |
|
||||
|
|
||||
```sh |
|
||||
docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'YOUR_PASSWORD' |
|
||||
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD |
|
||||
``` |
|
||||
If a password is not provided, the tool will prompt you for one : |
|
||||
```sh |
|
||||
docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw |
|
||||
Enter your password: // hidden prompt, type in your password |
|
||||
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' |
|
||||
``` |
|
||||
|
|
||||
**Important** : make sure to enclose your password in **single quotes** when you run `docker run` command : |
|
||||
|
|
||||
```bash |
|
||||
$ echo $2b$12$coPqCsPtcF <-- not correct |
|
||||
b2 |
|
||||
$ echo "$2b$12$coPqCsPtcF" <-- not correct |
|
||||
b2 |
|
||||
$ echo '$2b$12$coPqCsPtcF' <-- correct |
|
||||
$2b$12$coPqCsPtcF |
|
||||
``` |
|
||||
|
|
||||
**Important** : Please note: don't wrap the generated hash password in single quotes when you use `docker-compose.yml`. Instead, replace each `$` symbol with two `$$` symbols. For example: |
|
||||
|
|
||||
``` yaml |
|
||||
- PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG |
|
||||
``` |
|
||||
|
|
||||
This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw 'foobar123'` and then inserted an additional `$` before each existing `$` symbol. |
|
File diff suppressed because it is too large
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
title: API |
||||
|
--- |
||||
|
|
||||
|
TODO |
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
title: Optional Configuration |
||||
|
--- |
||||
|
|
||||
|
TODO |
@ -0,0 +1,47 @@ |
|||||
|
--- |
||||
|
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`. |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,23 @@ |
|||||
|
--- |
||||
|
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. |
@ -0,0 +1,58 @@ |
|||||
|
--- |
||||
|
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 |
@ -0,0 +1,56 @@ |
|||||
|
--- |
||||
|
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. |
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
title: Without Docker |
||||
|
--- |
||||
|
|
||||
|
TODO |
@ -0,0 +1,96 @@ |
|||||
|
--- |
||||
|
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 |
||||
|
--> |
@ -0,0 +1,92 @@ |
|||||
|
--- |
||||
|
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 |
@ -0,0 +1,35 @@ |
|||||
|
--- |
||||
|
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 |
@ -0,0 +1,81 @@ |
|||||
|
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>© <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 |
@ -0,0 +1,4 @@ |
|||||
|
mkdocs-material |
||||
|
pillow |
||||
|
cairosvg |
||||
|
mike |
@ -1,11 +0,0 @@ |
|||||
{ |
|
||||
"name": "wg-easy", |
|
||||
"version": "1.0.1", |
|
||||
"lockfileVersion": 3, |
|
||||
"requires": true, |
|
||||
"packages": { |
|
||||
"": { |
|
||||
"version": "1.0.1" |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,10 +1,10 @@ |
|||||
{ |
{ |
||||
"version": "1.0.1", |
"version": "1.0.0", |
||||
|
"private": true, |
||||
"scripts": { |
"scripts": { |
||||
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .", |
"dev": "docker compose -f docker-compose.dev.yml up --build", |
||||
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .", |
"build": "docker build -t wg-easy .", |
||||
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up", |
"docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs squidfunk/mkdocs-material serve -a 0.0.0.0:8080" |
||||
"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" |
"packageManager": "pnpm@10.5.2" |
||||
} |
} |
||||
} |
|
||||
|
@ -0,0 +1,9 @@ |
|||||
|
lockfileVersion: '9.0' |
||||
|
|
||||
|
settings: |
||||
|
autoInstallPeers: true |
||||
|
excludeLinksFromLockfile: false |
||||
|
|
||||
|
importers: |
||||
|
|
||||
|
.: {} |
@ -1,11 +0,0 @@ |
|||||
{ |
|
||||
"extends": "athom", |
|
||||
"ignorePatterns": [ |
|
||||
"**/vendor/*.js" |
|
||||
], |
|
||||
"rules": { |
|
||||
"consistent-return": "off", |
|
||||
"no-shadow": "off", |
|
||||
"max-len": "off" |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,26 @@ |
|||||
|
# 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 |
@ -0,0 +1 @@ |
|||||
|
public-hoist-pattern[]=@libsql/linux* |
@ -0,0 +1,2 @@ |
|||||
|
pnpm-lock.yaml |
||||
|
server/database/migrations/meta |
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"trailingComma": "es5", |
||||
|
"tabWidth": 2, |
||||
|
"semi": true, |
||||
|
"singleQuote": true, |
||||
|
"plugins": ["prettier-plugin-tailwindcss"] |
||||
|
} |
@ -0,0 +1,57 @@ |
|||||
|
<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> |
@ -0,0 +1,34 @@ |
|||||
|
<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> |
@ -0,0 +1,20 @@ |
|||||
|
<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> |
@ -0,0 +1,26 @@ |
|||||
|
<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> |
@ -0,0 +1,20 @@ |
|||||
|
<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> |
@ -0,0 +1,31 @@ |
|||||
|
<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> |
@ -0,0 +1,10 @@ |
|||||
|
<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> |
@ -0,0 +1,17 @@ |
|||||
|
<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> |
@ -0,0 +1,46 @@ |
|||||
|
<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> |
@ -0,0 +1,24 @@ |
|||||
|
<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> |
@ -0,0 +1,11 @@ |
|||||
|
<template> |
||||
|
<span class="inline-block"> |
||||
|
{{ client.ipv4Address }}, {{ client.ipv6Address }} |
||||
|
</span> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
client: LocalClient; |
||||
|
}>(); |
||||
|
</script> |
@ -0,0 +1,30 @@ |
|||||
|
<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> |
@ -0,0 +1,136 @@ |
|||||
|
<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> |
@ -0,0 +1,51 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,14 @@ |
|||||
|
<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> |
@ -0,0 +1,23 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,51 @@ |
|||||
|
<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> |
@ -0,0 +1,32 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,53 @@ |
|||||
|
<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> |
@ -0,0 +1,45 @@ |
|||||
|
<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> |
@ -0,0 +1,53 @@ |
|||||
|
<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> |
@ -0,0 +1,26 @@ |
|||||
|
<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> |
@ -0,0 +1,11 @@ |
|||||
|
<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> |
@ -0,0 +1,13 @@ |
|||||
|
<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> |
@ -0,0 +1,8 @@ |
|||||
|
<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> |
@ -0,0 +1,19 @@ |
|||||
|
<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> |
@ -0,0 +1,20 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,51 @@ |
|||||
|
<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> |
@ -0,0 +1,44 @@ |
|||||
|
<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> |
@ -0,0 +1,5 @@ |
|||||
|
<template> |
||||
|
<form> |
||||
|
<slot /> |
||||
|
</form> |
||||
|
</template> |
@ -0,0 +1,9 @@ |
|||||
|
<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> |
@ -0,0 +1,12 @@ |
|||||
|
<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> |
@ -0,0 +1,11 @@ |
|||||
|
<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> |
@ -0,0 +1,38 @@ |
|||||
|
<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> |
@ -0,0 +1,17 @@ |
|||||
|
<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> |
@ -0,0 +1,18 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,28 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,44 @@ |
|||||
|
<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> |
@ -0,0 +1,11 @@ |
|||||
|
<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> |
@ -0,0 +1,28 @@ |
|||||
|
<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> |
@ -0,0 +1,28 @@ |
|||||
|
<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> |
@ -0,0 +1,13 @@ |
|||||
|
<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> |
@ -0,0 +1,16 @@ |
|||||
|
<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> |
@ -0,0 +1,15 @@ |
|||||
|
<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> |
@ -0,0 +1,15 @@ |
|||||
|
<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> |
||||
|
</template> |
@ -0,0 +1,13 @@ |
|||||
|
<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> |
||||
|
</template> |
@ -0,0 +1,13 @@ |
|||||
|
<template> |
||||
|
<svg |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
viewBox="0 0 20 20" |
||||
|
fill="currentColor" |
||||
|
> |
||||
|
<path |
||||
|
fill-rule="evenodd" |
||||
|
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" |
||||
|
clip-rule="evenodd" |
||||
|
/> |
||||
|
</svg> |
||||
|
</template> |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue