This commit is contained in:
Shahrad Elahi 2024-05-29 20:10:18 +03:30 committed by GitHub
parent 66a1fe2ece
commit efb93e5e31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
162 changed files with 2780 additions and 2758 deletions

View File

@ -7,5 +7,9 @@
"access": "restricted",
"baseBranch": "canary",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": [],
"privatePackages": {
"version": true,
"tag": true
}
}

View File

@ -1,5 +1,5 @@
---
'wireadmin': patch
"wireadmin": patch
---
fix: Improve password hashing method and env loader

View File

@ -0,0 +1,5 @@
---
"wireadmin": major
---
BREAKING: `UI_PASSWORD` has been removed. Please use `ADMIN_PASSWORD` instead.

View File

@ -1,5 +1,5 @@
---
'wireadmin': patch
"wireadmin": patch
---
fix: tor config generation when container restarts

View File

@ -0,0 +1,5 @@
---
"wireadmin": major
---
feat: Creates a Dnsmasq server on port 53 and forwards DNS queries through the Tor network.

View File

@ -9,4 +9,4 @@ Dockerfile
*.md
tests/
*.log
tmp/
tmp/

View File

@ -10,6 +10,12 @@ concurrency:
group: '${{ github.workflow }}-${{ github.event.number || github.sha }}'
cancel-in-progress: true
env:
BUILD_PLATFORMS: linux/amd64,linux/arm64
DOCKERHUB_SLUG: shahradel/wireadmin
GHCR_SLUG: ghcr.io/wireadmin/wireadmin
TAG: dev
jobs:
lint:
runs-on: ubuntu-latest
@ -24,25 +30,37 @@ jobs:
cache: 'pnpm'
- run: pnpm -r install --frozen-lockfile
- run: pnpm --if-present format:check
- run: pnpm --if-present lint
- run: pnpm format:check
image:
if: github.repository == 'wireadmin/wireadmin'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.PRIVATE_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image
- name: Build & Publish
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
context: .
file: ./Dockerfile
push: false
push: ${{ github.event_name != 'pull_request' }}
platforms: '${{ env.BUILD_PLATFORMS }}'
tags: '${{ env.GHCR_SLUG }}:${{ env.TAG }},${{ env.DOCKERHUB_SLUG }}:${{ env.TAG }}'

View File

@ -1,75 +0,0 @@
name: Build Prerelease Image
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build'
required: true
##
# Invoke:
# gh workflow run "Build Prerelease Image" -f tag=2.0.0-canary.0
##
env:
IMAGE_TAG: ${{ github.event.inputs.tag }}
BUILD_PLATFORMS: linux/amd64,linux/arm64
permissions:
contents: read
packages: write
jobs:
ghcr-build:
runs-on: ubuntu-latest
env:
IMAGE_NAME: shahradelahi/wireadmin
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.PRIVATE_TOKEN }}
- name: Push to GitHub Container Registry
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
args: VERSION=${{ env.IMAGE_TAG }}
tags: ghcr.io/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
docker-build:
runs-on: ubuntu-latest
env:
IMAGE_NAME: litehex/wireadmin
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v1
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to DockerHub
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: ${{ env.BUILD_PLATFORMS }}
args: VERSION=${{ env.IMAGE_TAG }}
tags: docker.io/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}

View File

@ -1,87 +1,100 @@
name: Release Package
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build'
required: true
push:
branches:
- canary
concurrency: '${{ github.workflow }}-${{ github.ref }}'
env:
BUILD_PLATFORMS: linux/amd64,linux/arm64
IMAGE_TAG: ${{ github.event.inputs.tag }}
permissions:
contents: read
packages: write
DOCKERHUB_SLUG: shahradel/wireadmin
GHCR_SLUG: ghcr.io/wireadmin/wireadmin
jobs:
release:
needs: [github-registry, docker-hub]
if: github.repository == 'wireadmin/wireadmin'
runs-on: ubuntu-latest
outputs:
published: ${{ steps.changesets.outputs.published }}
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
fetch-depth: 0
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
commit: 'chore(release): version package'
title: 'chore(release): version package'
publish: changeset publish
publish: pnpm ci:publish
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
github-registry:
name: Push to GitHub Container Registry
build:
name: Build & Publish
runs-on: ubuntu-latest
env:
IMAGE_NAME: shahradelahi/wireadmin
needs: release
if: needs.release.outputs.published == 'true'
strategy:
fail-fast: true
matrix:
package: ${{ fromJson(needs.release.outputs.publishedPackages) }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-qemu-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.PRIVATE_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push to GitHub Container Registry
uses: docker/build-push-action@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
context: .
file: ./Dockerfile
images: |
${{ env.DOCKERHUB_SLUG }}
${{ env.GHCR_SLUG }}
tags: |
type=semver,pattern={{version}},value=${{ matrix.package.version }}
- name: Build and push
uses: docker/bake-action@v4
with:
files: |
./docker-bake.hcl
${{ steps.meta.outputs.bake-file }}
targets: image-all
push: true
platforms: ${{ env.BUILD_PLATFORMS }}
args: VERSION=${{ env.IMAGE_TAG }}
tags: ghcr.io/${{ env.IMAGE_NAME }}:latest,ghcr.io/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
docker-hub:
name: Push to DockerHub
runs-on: ubuntu-latest
env:
IMAGE_NAME: litehex/wireadmin
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v3
- name: Check manifest
run: |
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
- name: Login to DockerHub
uses: docker/login-action@v1
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to DockerHub
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: ${{ env.BUILD_PLATFORMS }}
args: VERSION=${{ env.IMAGE_TAG }}
tags: docker.io/${{ env.IMAGE_NAME }}:latest,docker.io/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
- name: Inspect image
run: |
docker pull ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker image inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker pull ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
docker image inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}

View File

@ -1,7 +1,8 @@
.DS_Store
node_modules
/build
web
build
dist
.svelte-kit
/package
.env
.env.*

View File

@ -1,9 +1,11 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "all",
"trailingComma": "es5",
"endOfLine": "lf",
"printWidth": 100,
"overrides": [
{
@ -12,6 +14,25 @@
"parser": "markdown",
"printWidth": 79
}
},
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"plugins": ["prettier-plugin-svelte"]
}
},
{
"files": "Dockerfile",
"options": {
"spaceRedirects": false
}
}
],
"importOrder": ["<THIRD_PARTY_MODULES>", "", "^types$", "^\\$lib/(.*)$", "^@/(.*)$", "", "^[./]"],
"plugins": [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss",
"prettier-plugin-sh"
]
}

View File

@ -1,95 +1,90 @@
ARG ALPINE_VERSION=3.19
ARG LYREBIRD_VERSION=0.2.0
ARG NODE_VERSION=20
ARG VERSION=0.0.0-canary
FROM --platform=$BUILDPLATFORM chriswayg/tor-alpine:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base
LABEL Maintainer="Shahrad Elahi <https://github.com/shahradelahi>"
WORKDIR /app
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as node
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone
RUN apk update \
&& apk upgrade \
&& apk add -U --no-cache \
iptables net-tools \
screen logrotate bash \
wireguard-tools \
dnsmasq \
tor \
&& rm -rf /var/cache/apk/*
COPY --from=tor /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy
COPY --from=tor /usr/local/bin/meek-server /usr/local/bin/meek-server
FROM --platform=${BUILDPLATFORM} golang:alpine AS pluggables
ARG LYREBIRD_VERSION
RUN apk update \
&& apk upgrade \
&& apk add -U --no-cache \
bash \
make \
&& rm -rf /var/cache/apk/*
SHELL ["/bin/bash", "-c"]
RUN <<EOT
set -ex
cd /tmp
# Install required packages
RUN apk add -U --no-cache \
iproute2 iptables net-tools \
screen curl bash \
wireguard-tools \
tor &&\
# NPM packages
npm install -g @litehex/node-checksum@0.2 &&\
# Clear APK cache
rm -rf /var/cache/apk/*
# Lyrebird - https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird
wget "https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird/-/archive/lyrebird-$LYREBIRD_VERSION/lyrebird-lyrebird-$LYREBIRD_VERSION.tar.gz"
tar -xvf lyrebird-lyrebird-$LYREBIRD_VERSION.tar.gz
pushd lyrebird-lyrebird-$LYREBIRD_VERSION || exit 1
make build -e VERSION=$LYREBIRD_VERSION
cp ./lyrebird /usr/local/bin
popd || exit 1
COPY /config/torrc.template /etc/tor/torrc.template
cp -rv /go/bin /usr/local/bin
rm -rf /go
rm -rf /tmp/*
EOT
# Copy user scripts
COPY /bin /usr/local/bin
RUN chmod -R +x /usr/local/bin
COPY web/package.json web/pnpm-lock.yaml ./
# Base env
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
FROM base AS build
# Setup Pnpm - Pnpm only used for build stage
FROM node AS build
WORKDIR /app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY web .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile \
# build
&& mkdir -p /data \
&& echo gA== > /data/storage.b64 \
&& NODE_ENV=production pnpm run build \
# Omit devDependencies
&& pnpm prune --prod \
# Move the goods to a temporary location
&& mv node_modules /tmp/node_modules \
&& mv build /tmp/build \
# Remove everything else
&& rm -rf ./*
&& NODE_ENV=production pnpm build \
&& pnpm prune --prod \
&& cp -R node_modules build package.json /tmp \
&& rm -rf ./*
FROM node
WORKDIR /app
FROM base AS release
COPY --from=pluggables /usr/local/bin/lyrebird /usr/local/bin/lyrebird
COPY rootfs /
# Copy the goods from the build stage
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
ENV NODE_ENV=production
ENV LOG_LEVEL=error
# Copy the goodies from the build stage
COPY --from=build /tmp/package.json package.json
COPY --from=build /tmp/node_modules node_modules
COPY --from=build /tmp/build build
# Fix permissions
RUN mkdir -p /data && chmod 700 /data
RUN mkdir -p /etc/torrc.d && chmod -R 400 /etc/torrc.d
RUN mkdir -p /var/vlogs && touch /var/vlogs/web && chmod -R 600 /var/vlogs
RUN mkdir -p /data/ /etc/tor/torrc.d/ /var/log/wireadmin/ \
&& chmod 700 /data/ \
&& chmod -R 400 /etc/tor/ \
&& touch /var/log/wireadmin/web.log
ENV NODE_ENV=production
ENV LOG_LEVEL=error
RUN echo '* * * * * /usr/bin/env logrotate /etc/logrotate.d/rotator' >/etc/crontabs/root
# Setup entrypoint
COPY docker-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# Healthcheck
HEALTHCHECK --interval=60s --timeout=3s --start-period=20s --retries=3 \
CMD curl -f http://127.0.0.1:3000/api/health || exit 1
# Volumes
VOLUME ["/etc/torrc.d", "/data", "/var/vlogs"]
# Overwrite package version
RUN node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('/app/package.json')); pkg.version = process.env.VERSION; fs.writeFileSync('/app/package.json', JSON.stringify(pkg, null, 2));"
VOLUME ["/etc/tor", "/var/lib/tor", "/data"]
# Run the app
EXPOSE 3000/tcp
CMD [ "npm", "run", "start" ]
CMD [ "node", "/app/build/index.js" ]

View File

@ -1,57 +1,42 @@
ARG ALPINE_VERSION=3.19
ARG NODE_VERSION=20
ARG VERSION=0.0.0-dev
FROM --platform=$BUILDPLATFORM chriswayg/tor-alpine:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base
LABEL Maintainer="Shahrad Elahi <https://github.com/shahradelahi>"
WORKDIR /app
FROM --platform=$BUILDPLATFORM shahradel/torproxy:latest as tor
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as node
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apk update \
&& apk upgrade \
&& apk add -U --no-cache \
iptables net-tools \
screen logrotate bash \
wireguard-tools \
dnsmasq \
tor \
&& rm -rf /var/cache/apk/*
COPY --from=tor /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy
COPY --from=tor /usr/local/bin/meek-server /usr/local/bin/meek-server
# Install required packages
RUN apk add -U --no-cache \
iproute2 iptables net-tools \
screen vim curl bash \
wireguard-tools \
tor &&\
# NPM packages
npm install -g @litehex/node-checksum@0.2 &&\
# Clear APK cache
rm -rf /var/cache/apk/*
# Copy Tor Configs
COPY /config/torrc.template /etc/tor/torrc.template
COPY /config/obfs4-bridges.conf /etc/torrc.d/obfs4-bridges.conf
# Copy user scripts
COPY /bin /usr/local/bin
RUN chmod -R +x /usr/local/bin
FROM node
WORKDIR /app
# Setup Pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# Base env
ENV NODE_ENV=development
ENV LOG_LEVEL=debug
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
FROM base AS runner
ENV NODE_ENV=development
ENV LOG_LEVEL=debug
COPY --from=tor /usr/local/bin/lyrebird /usr/local/bin/lyrebird
COPY rootfs /
# Fix permissions
RUN mkdir -p /data && chmod 700 /data
RUN mkdir -p /etc/torrc.d && chmod -R 400 /etc/torrc.d
RUN mkdir -p /var/vlogs && touch /var/vlogs/web && chmod -R 600 /var/vlogs
RUN mkdir -p /data/ && chmod 700 /data/
RUN mkdir -p /etc/tor/torrc.d/ && chmod -R 400 /etc/tor/
RUN mkdir -p /var/log/wireadmin/ && touch /var/log/wireadmin/web.log
RUN echo '* * * * * /usr/bin/env logrotate /etc/logrotate.d/rotator' > /etc/crontabs/root
# Setup entrypoint
COPY docker-entrypoint.sh /entrypoint.sh
@ -59,7 +44,7 @@ RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# Volumes
VOLUME ["/etc/torrc.d", "/data", "/var/vlogs"]
VOLUME ["/etc/tor", "/var/lib/tor", "/data"]
# Run the app
EXPOSE 5173/tcp

153
README.md
View File

@ -1,6 +1,6 @@
# WireGuard (Easy Admin UI)
[![CI](https://github.com/shahradelahi/wireadmin/actions/workflows/ci.yml/badge.svg)](https://github.com/shahradelahi/wireadmin/actions/workflows/ci.yml)
[![CI](https://github.com/wireadmin/wireadmin/actions/workflows/ci.yml/badge.svg)](https://github.com/wireadmin/wireadmin/actions/workflows/ci.yml)
[![GPL-3.0 Licensed](https://img.shields.io/badge/License-GPL3.0-blue.svg?style=flat)](https://opensource.org/licenses/GPL-3.0)
![Screenshot](assets/screenshot-1.png)
@ -9,6 +9,21 @@
| :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: |
| <img src="assets/screenshot-2.png" alt="screenshot" style="width:100%;max-height:300px;"/> | <img src="assets/screenshot-4.png" alt="screenshot" style="width:100%;max-height:300px;"/> | <img src="assets/screenshot-3.png" alt="screenshot" style="width:100%;max-height:300px;"/> |
---
- [Features](#features)
- [Build locally](#build-locally)
- [Image](#image)
- [Ports](#ports)
- [Usage](#usage)
- [Docker Compose](#docker-compose)
- [Command line](#command-line)
- [Persistent Data](#persistent-data)
- [Environment variables](#environment-variables)
- [Upgrade](#upgrade)
- [Contributing](#contributing)
- [License](#license)
## Features
- Simple and friendly UI
@ -20,61 +35,46 @@
- Easily download the client configurations.
- Automatic Light/Dark Mode
## Installation
## Image
### 1. Prerequisites
| Registry | Image |
| ------------------------------------------------------------------------------------------------------- | ----------------------------- |
| [Docker Hub](https://hub.docker.com/r/shahradel/wireadmin/) | `shahradel/wireadmin` |
| [GitHub Container Registry](https://github.com/users/shahradelahi/packages/container/package/cfw-proxy) | `ghcr.io/wireadmin/wireadmin` |
- [Docker Engine](https://docs.docker.com/engine/install/)
## Ports
### 2. Docker Image
- `53`: Dnsmasq
- `3000`: WebUI
#### Build from source (recommended)
And for any additional ports of WireGuard instance, should be exposed through Docker.
## Usage
### Docker Compose
Docker compose is the recommended way to run this image. You can use the following
[docker compose template](docker-compose.yml), then run the container:
```bash
git clone https://github.com/shahradelahi/wireadmin
docker buildx build --tag litehex/wireadmin ./wireadmin
docker compose up -d
docker compose logs -f
```
#### Pull from Docker Hub
```bash
docker pull litehex/wireadmin # OR ghcr.io/shahradelahi/wireadmin
```
### 3. Persistent Data
WireAdmin store configurations at `/data`. It's important to mount a volume at this location to ensure that
your data is not lost during container restarts or updates.
#### Create a docker volume
```bash
docker volume create wireadmin-data --driver local
```
### 4. Run WireAdmin
When creating each server, ensure that you add the port exposure through Docker. In the below command, the port `51820`
is added for the WireGuard server.
> 💡 The port `3000` is for the WebUI, and can be changed with `PORT` environment variable, but for security
> reasons, it's recommended to NOT expose **_any kind of WebUI_** to the public. It's up to you to remove it after
> configuring
> the Servers/Peers.
### Command line
```shell
docker run --detach \
--name wireadmin \
-e WG_HOST=<YOUR_SERVER_IP> \
-e UI_PASSWORD=<ADMIN_PASSWORD> \
-p "3000:3000/tcp" \
-p "51820:51820/udp" \
-v "wireadmin-data:/data" \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
litehex/wireadmin
$ docker run -d \
--name wireadmin \
-e WG_HOST="<YOUR_SERVER_IP>" \
-e ADMIN_PASSWORD="<ADMIN_PASSWORD>" \
-p "3000:3000/tcp" \
-p "51820:51820/udp" \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
ghcr.io/wireadmin/wireadmin
```
> 💡 Replace `<YOUR_SERVER_IP>` with the IP address of your server.
@ -83,24 +83,59 @@ docker run --detach \
The Web UI will now be available on `http://0.0.0.0:3000`.
## Options
### Persistent Data
It's important to mount a volume to ensure that your data is not lost during container restarts or updates. Here is the list of required volumes:
- `wireadmin-data`: `/data`
- `tor-data`: `/var/lib/tor`
To create a docker volume, use the following command:
```bash
$ docker volume create "<volume>" --driver local
```
> 💡 Replace `<volume>` with the name of the volume.
Finally, to mount the volumes with `-v` flag in the `docker run` command:
```bash
$ docker run -d \
-v wireadmin-data:/data \
-v tor-data:/var/lib/tor \
ghcr.io/wireadmin/wireadmin
```
### Environment variables
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
| Option | Description | Default | Optional |
| ----------------- | ------------------------------------------------------------------------------- | ------------------- | -------- |
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
| `UI_PASSWORD` | The password for the admin UI. | `insecure-password` | |
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
| `PORT` | The port for the WebUI. | `3000` | ✔️ |
| `TOR_USE_BRIDGES` | Set this to `1` and then mount the bridges file at `/etc/torrc.d/bridges.conf`. | - | ✔️ |
| `TOR_*` | The `Torrc` proxy configuration. (e.g. `SocksPort` as `TOR_SOCKS_PORT="9050"`) | - | ✔️ |
| Option | Description | Default | Optional |
| ----------------- | ----------------------------------------------------------------------------------- | ------------------- | -------- |
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
| `ADMIN_PASSWORD` | The password for the web UI. | `insecure-password` | |
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
| `PORT` | The port for the WebUI. | `3000` | ✔️ |
| `TOR_USE_BRIDGES` | Set this to `1` and then mount the bridges file at `/etc/tor/torrc.d/bridges.conf`. | - | ✔️ |
| `TOR_*` | The `Torrc` proxy configuration. (e.g. `SocksPort` as `TOR_SOCKS_PORT="9050"`) | - | ✔️ |
## Reporting
## Upgrade
For bug reports, and feature requests, please create an issue
on [GitHub](https://github.com/shahradelahi/wireadmin/issues).
Recreate the container whenever I push an update:
```bash
$ docker compose pull
$ docker compose up -d
```
## Contributing
Want to contribute? Awesome! To show your support is to star the project, or to raise issues
on [GitHub](https://github.com/wireadmin/wireadmin)
Thanks again for your support, it is much appreciated! 🙏
## License
[GPL-3.0](LICENSE) © [Shahrad Elahi](https://github.com/shahradelahi)
[GPL-3.0](/LICENSE) © [Shahrad Elahi](https://github.com/shahradelahi)

BIN
assets/screenshot-1.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 65 KiB

BIN
assets/screenshot-2.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/screenshot-3.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

BIN
assets/screenshot-4.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,16 +0,0 @@
#!/bin/bash
# This script is used for getting last 100 lines of a screen session
SCREEN_NAME="$1"
LIMIT="${2:-100}"
SCRIPT=$(basename "${0}")
if [ -z "$SCREEN_NAME" ]; then
echo "Usage: ${SCRIPT} <screen_name>"
exit 1
fi
screen -S "$SCREEN_NAME" -X hardcopy /tmp/screen-hardcopy
tail -n "${LIMIT}" /tmp/screen-hardcopy
rm /tmp/screen-hardcopy

View File

@ -1,5 +0,0 @@
##### Auto-Generated by the WireAdmin. Do not edit. #####
VirtualAddrNetwork 10.192.0.0/10
DNSPort {{INET_ADDRESS}}:53530
TransPort {{INET_ADDRESS}}:59040
ClientTransportPlugin obfs4 exec /usr/local/bin/obfs4proxy managed

34
docker-bake.hcl Normal file
View File

@ -0,0 +1,34 @@
variable "DEFAULT_TAG" {
default = "wireadmin:local"
}
// Special target: https://github.com/docker/metadata-action#bake-definition
target "docker-metadata-action" {
tags = ["${DEFAULT_TAG}"]
}
// Default target if none specified
group "default" {
targets = ["image-local"]
}
target "image" {
inherits = ["docker-metadata-action"]
}
target "image-local" {
inherits = ["image"]
output = ["type=docker"]
}
target "image-all" {
inherits = ["image"]
platforms = [
"linux/amd64",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/386",
"linux/s390x"
]
}

View File

@ -1,13 +1,15 @@
version: '3.8'
services:
wireadmin:
image: wireadmin
image: ghcr.io/wireadmin/wireadmin:dev
build:
context: .
dockerfile: Dockerfile-Dev
volumes:
- ./web/:/app/
ports:
- '5173:5173'
environment:
- WG_HOST=192.168.1.102
- UI_PASSWORD=password
- WG_HOST=192.168.88.252
- ADMIN_PASSWORD=password
extra_hosts:
- 'host.docker.internal:host-gateway'

18
docker-compose.yml Normal file → Executable file
View File

@ -1,4 +1,3 @@
version: '3.8'
services:
wireadmin:
environment:
@ -7,16 +6,21 @@ services:
- WG_HOST=localhost
# ⚠️ Required:
# You can use `openssl rand -base64 8` to generate a secure password
- UI_PASSWORD=super-secret-password
- ADMIN_PASSWORD=super-secret-password
image: wireadmin
image: ghcr.io/wireadmin/wireadmin
container_name: wireadmin
restart: unless-stopped
volumes:
- persist-data:/data
- wireadmin-data:/data
- tor-data:/var/lib/tor
ports:
- '51820:51820/udp'
- '3000:3000/tcp'
# Dnsmasq
#- '53:53/udp'
#- '53:53/tcp'
# WireGuard
- '51820:51820/udp'
cap_add:
- NET_ADMIN
- SYS_MODULE
@ -25,5 +29,5 @@ services:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
persist-data:
driver: local
wireadmin-data:
tor-data:

View File

@ -1,60 +1,10 @@
#!/usr/bin/env bash
set -e
source /etc/wireadmin/xscript.sh
ENV_FILE="/app/.env"
TOR_CONFIG="/etc/tor/torrc"
TOR_CONFIG_TEMPLATE="${TOR_CONFIG}.template"
log() {
local level=$1
local message=$2
echo "[$(date +"%Y-%m-%d %H:%M:%S")] [$level] $message"
}
to_camel_case() {
echo "${1}" | awk -F_ '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1' OFS=""
}
generate_tor_config() {
# Copying the torrc template to the torrc file
cp "${TOR_CONFIG_TEMPLATE}" "${TOR_CONFIG}"
# IP address of the container
local inet_address="$(hostname -i | awk '{print $1}')"
sed -i "s/{{INET_ADDRESS}}/$inet_address/g" "${TOR_CONFIG}"
# any other environment variables that start with TOR_ are added to the torrc
# file
env | grep ^TOR_ | sed -e 's/TOR_//' -e 's/=/ /' | while read -r line; do
key=$(echo "$line" | awk '{print $1}')
value=$(echo "$line" | awk '{print $2}')
key=$(to_camel_case "$key")
echo "$key $value" >>"${TOR_CONFIG}"
done
# Removing duplicated tor options
awk -F= '!a[tolower($1)]++' "${TOR_CONFIG}" >"/tmp/$(basename "${TOR_CONFIG}")" &&
mv "/tmp/$(basename "${TOR_CONFIG}")" "${TOR_CONFIG}"
# Checking if there is /etc/torrc.d folder and if there are use globbing to include all files
local torrc_files=$(find /etc/torrc.d -type f -name "*.conf")
if [ -n "${torrc_files}" ]; then
log "notice" "Found torrc.d folder with configuration files"
echo "%include /etc/torrc.d/*.conf" >>"${TOR_CONFIG}"
fi
# Remove comment line with single Hash
sed -i '/^#\([^#]\)/d' "${TOR_CONFIG}"
# Remove options with no value. (KEY[:space:]{...VALUE})
sed -i '/^[^ ]* $/d' "${TOR_CONFIG}"
# Remove double empty lines
sed -i '/^$/N;/^\n$/D' "${TOR_CONFIG}"
log "notice" "Tor configuration file has been generated"
}
echo " "
echo " _ ___ ___ __ _ "
echo "| | / (_)_______ / | ____/ /___ ___ (_)___ "
@ -63,55 +13,47 @@ echo "| |/ |/ / / / / __/ ___ / /_/ / / / / / / / / / /"
echo "|__/|__/_/_/ \___/_/ |_\__,_/_/ /_/ /_/_/_/ /_/ "
echo " "
mkdir -p /var/vlogs
touch "$ENV_FILE"
chmod 400 "$ENV_FILE"
touch "${ENV_FILE}"
chmod 400 "${ENV_FILE}"
if ! grep -q "AUTH_SECRET" "${ENV_FILE}"; then
tee -a "${ENV_FILE}" &>/dev/null <<EOF
AUTH_SECRET=$(openssl rand -base64 32)
EOF
fi
# Checking if there is `UI_PASSWORD` environment variable
# if there was, converting it to sha256 and storing it to
# the .env
if [ -n "$UI_PASSWORD" ]; then
sed -i '/^HASHED_PASSWORD/d' "${ENV_FILE}"
tee -a "${ENV_FILE}" &>/dev/null <<EOF
HASHED_PASSWORD=$(checksum hash -a sha256 -C "${UI_PASSWORD}")
EOF
unset UI_PASSWORD
else
log "error" "no password set for the UI"
exit 1
fi
if [ -z "$WG_HOST" ]; then
log "error" "the WG_HOST environment variable is not set"
exit 1
if [ -z "$ADMIN_PASSWORD" ]; then
log warn "No ADMIN_PASSWORD provided, using default password"
fi
# Remove duplicated envs
awk -F= '!a[$1]++' "${ENV_FILE}" >"/tmp/$(basename "${ENV_FILE}")" &&
mv "/tmp/$(basename "${ENV_FILE}")" "${ENV_FILE}"
awk -F= '!a[$1]++' "$ENV_FILE" > "/tmp/$(basename "$ENV_FILE")" \
&& mv "/tmp/$(basename "$ENV_FILE")" "$ENV_FILE"
if [ -z "$WG_HOST" ]; then
log "error" "the WG_HOST environment variable is not set"
exit 1
fi
# Generate Tor configuration
generate_tor_config
setup_logrotate
setup_dns
# Start Tor on the background
screen -dmS "tor" tor -f "${TOR_CONFIG}"
# Background services
crond
dnsmasq
# Start Tor
screen -L -Logfile /var/log/wireadmin/tor.log -dmS tor \
bash -c "screen -S tor -X wrap off; tor -f $TOR_CONFIG"
sleep 1
echo -e "\n======================== Versions ========================"
echo -e "Alpine Version: \c" && cat /etc/alpine-release
echo -e "WireGuard Version: \c" && wg -v | head -n 1 | awk '{print $1,$2}'
echo -e "Tor Version: \c" && tor --version | head -n 1
echo -e "Obfs4proxy Version: \c" && obfs4proxy -version
echo -e "\n========================= Torrc ========================="
cat "${TOR_CONFIG}"
echo -e "========================================================\n"
echo -e "Alpine: \c" && cat /etc/alpine-release
echo -e "WireGuard: \c" && wg -v | head -n 1 | awk '{print $2}'
echo -e "Tor: \c" && tor --version | head -n 1 | awk '{print $3}' | sed 's/.$//'
echo -e "Dnsmasq: \c" && dnsmasq -v | head -n 1 | cut -d ' ' -f3
echo -e "Lyrebird: \c" && lyrebird -version
echo -e "\n======================= Tor Config ======================="
grep -v "^#" "$TOR_CONFIG"
echo -e "====================== Dnsmasq Config ======================"
grep -v "^#" "$DNSMASQ_CONFIG"
echo -e "==========================================================\n"
sleep 1
exec "$@"

View File

@ -5,20 +5,22 @@
"private": true,
"packageManager": "pnpm@8.15.0",
"scripts": {
"dev": "pnpm docker:drop && docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
"dev:image": "docker buildx build --tag wireadmin -f Dockerfile-Dev .",
"build": "pnpm docker:build",
"start": "pnpm docker:drop && docker compose -f docker-compose.yml up",
"docker:build": "docker buildx build --tag wireadmin .",
"docker:drop": "docker compose rm -fsv",
"format": "prettier --write . && pnpm --if-present -r format",
"format:check": "prettier --check . && pnpm --if-present -r format:check"
"dev": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up --force-recreate",
"dev:image": "docker buildx build --tag ghcr.io/wireadmin/wireadmin:dev -f Dockerfile-Dev .",
"build": "docker buildx build --tag ghcr.io/wireadmin/wireadmin .",
"start": "docker compose -f docker-compose.yml up --force-recreate",
"format": "prettier --write .",
"format:check": "prettier --check . ",
"ci:publish": "pnpm build && changeset publish"
},
"keywords": [],
"license": "GPL-3.0",
"devDependencies": {
"dependencies": {
"@changesets/cli": "^2.27.1",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2"
"prettier-plugin-sh": "^0.14.0",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.5.14"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,4 +12,11 @@ Bridge obfs4 31.18.117.18:9899 13BD8D1786AB84231D2630840142E81B0DDDAD19 cert=E31
Bridge obfs4 86.88.234.28:50001 DE6145637D189CEBF7B052DFC111A511B2BE8072 cert=FXAneGUETzpaw5oxNqO1Wi3EWLBSgbeIN0Z8GVFxromPutq6JkduMpzzvbQpyfYcGYjyJw iat-mode=0
Bridge obfs4 65.108.214.170:23909 8ABD0C0130A37EB3F686F883BCE6D5E59F66C228 cert=mJZdHhaAk6VzaOjQA1UWGkVbDbGqLRuNSuBSk0evlfKRKVzb2EmNio2N0ja+JG1to8KWYw iat-mode=0
Bridge obfs4 92.243.27.238:46311 2E5DC5F2632535630E87883262F967DA376700E2 cert=1BAr2DKmCPxel2DTMXKyOQgoxHM2q6SqJ0tDrZdlyCCrBXhJhsGCICWWZpBEuVB6bdMVWA iat-mode=0
Bridge obfs4 129.213.132.232:50913 5F163F907B3CFCCA66639EE297C2CD27006F7235 cert=ojqfACdTxWZNEPZwfEbAbDMMumnxzwoRAVMRwjkVl5RDH1h1j38YALzRhFFVpzsu7ZthQw iat-mode=0
Bridge obfs4 79.215.99.47:9531 DC1A7B010A348F3A6BE0750D38428D1EAD976D69 cert=TX3XOj1SX3fAB9yoA4dCx8Geu325i564gwIBgnAMyhP6NBdd9dW90gJpWQXKL/VC2BlTNQ iat-mode=0
Bridge obfs4 167.235.71.161:25754 EED9A10892988E28ADCFDDF19AB4F8868C51892D cert=6q19P7O+Zcai7mCxDCVIjiQnrufsMO4X5Ky88dcNBI2H5+LUqNMIcr3kNV3Cd7sKcgUSeg iat-mode=0
Bridge obfs4 65.21.6.66:15751 4D0BEE93BABCFBCD837BB33344850B78FFECD9FF cert=29a0bbjME3mTxC5wcafYAS4v43DVyOtSQWx374De7R38ARiVQZZ3fORSwgGCtDMCFZyxcw iat-mode=0
Bridge obfs4 185.177.207.205:11205 084113B9A27A8087C26236EF67A16784DF58D7F0 cert=pzuLxMv5n+7nRqX2czUQGh8JZBCMEVUHlkciocGRpX2IsPlTqd1YyXFQxRwfsYEFuuBdBQ iat-mode=2
Bridge obfs4 51.75.74.245:8356 18C27C9850967FD4BF4188963C1AEBEC40807823 cert=y6cQEx4d/25KALeqJA+2uB+6rmzoD9KZ0FrQGNwxb10yVj3mDjHtOneqcqhRT+BADhCTYg iat-mode=0
Bridge obfs4 91.134.100.128:51106 ABB9F62BEC331EE5DE7B3C3BEA014F8910E0C6BD cert=bC5k/PWVu06cSPhSm6mrQDBevReEpdtpokmDibpK0MBxRaVnn0S3O6YvEi4BDUeasn71bA iat-mode=0
Bridge obfs4 51.83.252.216:45918 C2B7E51665111C9BE43894E90B9A65DD8A25490D cert=oQgHCdMhvfF44gwHJssSHXltUE4r8gddEQeZ4iy17XHZMP+ql2QTG9LziiEqNfNCqFDBSw iat-mode=0

View File

@ -0,0 +1,9 @@
##### Auto-Generated by the WireAdmin. Do not edit. #####
AutomapHostsOnResolve 1
VirtualAddrNetwork 10.192.0.0/10
DNSPort {{INET_ADDRESS}}:53530
User tor
DataDirectory /var/lib/tor
TransPort {{INET_ADDRESS}}:59040 IsolateClientAddr IsolateClientProtocol IsolateDestAddr IsolateDestPort
ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit exec /usr/local/bin/lyrebird
%include /etc/tor/torrc.d/*.conf

View File

@ -0,0 +1,54 @@
#!/bin/bash
DNSMASQ_CONFIG=/etc/dnsmasq.d/tor-dns.conf
setup_dns() {
local _TOR_DNS_PORT="$(get_torrc_option "DNSPort")"
local _TOR_DNS_HOST="127.0.0.1"
if [ -z "$_TOR_DNS_PORT" ]; then
log ERROR "DNSPort is not set in $TOR_CONFIG"
exit 1
fi
if echo "$_TOR_DNS_PORT" | grep -q ":"; then
_TOR_DNS_HOST="$(awk -F: '{print $1}' <<< "$_TOR_DNS_PORT")"
_TOR_DNS_PORT="$(awk -F: '{print $2}' <<< "$_TOR_DNS_PORT")"
fi
# DNS must be a number
if ! [[ "$_TOR_DNS_PORT" =~ ^[0-9]+$ ]]; then
log ERROR "DNSPort options is malformed."
exit 1
fi
log NOTICE "Setting up Dnsmasq to use Tor DNS on $_TOR_DNS_HOST:$_TOR_DNS_PORT"
_IFACE="$(ip route show default | awk '/default/ {print $5}')"
tee /etc/resolv.conf &> /dev/null << EOF
# Generated by WireAdmin; DO NOT EDIT
nameserver 127.0.0.1
option allow-domains *.onion
search .
EOF
tee "$DNSMASQ_CONFIG" &> /dev/null << EOF
pid-file=/var/run/dnsmasq.pid
interface=$_IFACE
user=dnsmasq
group=dnsmasq
bind-dynamic
no-resolv
no-poll
no-negcache
bogus-priv
log-queries
domain-needed
cache-size=1500
min-port=4096
server=$_TOR_DNS_HOST#$_TOR_DNS_PORT
log-facility=/var/log/dnsmasq/dnsmasq.log
EOF
mkdir -p /var/log/dnsmasq
uown dnsmasq /var/log/dnsmasq
}

View File

@ -0,0 +1,15 @@
#!/bin/bash
setup_logrotate() {
tee "/etc/logrotate.d/rotator" &> /dev/null << EOF
/var/log/dnsmasq/dnsmasq.log
/var/log/wireadmin/*.log {
size 512K
rotate 3
missingok
notifempty
create 0640 root adm
copytruncate
}
EOF
}

View File

@ -0,0 +1,95 @@
#!/bin/bash
TOR_CONFIG="/etc/tor/torrc"
TOR_CONFIG_TEMPLATE="$TOR_CONFIG.template"
_cleanse_config() {
# Remove comment line with single Hash
sed -i '/^#\([^#]\)/d' "$TOR_CONFIG"
# Remove options with no value. (KEY[:space:]{...VALUE})
sed -i '/^[^ ]* $/d' "$TOR_CONFIG"
# Remove duplicate lines
sed -i '/^$/N;/\n.*\n/d' "$TOR_CONFIG"
# Remove double empty lines
sed -i '/^$/N;/^\n$/D' "$TOR_CONFIG"
}
_fix_permissions() {
mkdir -p /var/lib/tor
uown tor /var/lib/tor
chmod +x /var/lib/tor
}
_load_from_env() {
local added_count=0
local updated_count=0
for _env_name in $(env | grep -o "^TOR_[^=]*"); do
# skip custom options
if [[ " ${CUSTOM_TOR_OPTIONS[*]} " == *" ${_env_name} "* ]]; then
continue
fi
local env_value="${!_env_name}"
# remove prefix and convert to camel case
local option=$(to_camel_case "${_env_name#TOR_}")
if [ -n "$env_value" ]; then
# Check if there is a corresponding option in the torrc file, and update it
if grep -i -q "^$option" "$TOR_CONFIG"; then
sed -i "s/^$option.*/$option $env_value/" "$TOR_CONFIG"
updated_count=$((updated_count + 1))
else
echo "$option $env_value" >> "$TOR_CONFIG"
added_count=$((added_count + 1))
fi
fi
done
# Add a blank line at the end of the file
echo "" >> "$TOR_CONFIG"
if [ "$added_count" -gt 0 ] || [ "$updated_count" -gt 0 ]; then
echo ""
log NOTICE "Added $added_count and updated $updated_count options from environment variables."
fi
}
generate_tor_config() {
# Copying the torrc template to the torrc file
cp "${TOR_CONFIG_TEMPLATE}" "$TOR_CONFIG"
# IP address of the container
local inet_address="$(hostname -i | awk '{print $1}')"
sed -i "s/{{INET_ADDRESS}}/$inet_address/g" "$TOR_CONFIG"
# any other environment variables that start with TOR_ are added to the torrc
# file
env | grep ^TOR_ | sed -e 's/TOR_//' -e 's/=/ /' | while read -r line; do
key=$(echo "$line" | awk '{print $1}')
value=$(echo "$line" | awk '{print $2}')
key=$(to_camel_case "$key")
echo "$key $value" >> "$TOR_CONFIG"
done
# Removing duplicated tor options
awk -F= '!a[tolower($1)]++' "$TOR_CONFIG" > "/tmp/$(basename "$TOR_CONFIG")" \
&& mv "/tmp/$(basename "$TOR_CONFIG")" "$TOR_CONFIG"
_load_from_env
_cleanse_config
_fix_permissions
log "notice" "Tor configuration file has been generated"
}
get_torrc_option() {
grep -i "^$1" "$TOR_CONFIG" | awk '{print $2}'
}

22
rootfs/etc/wireadmin/xscript.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
source /etc/wireadmin/internal/dns.sh
source /etc/wireadmin/internal/logrotate.sh
source /etc/wireadmin/internal/tor.sh
uppercase() {
echo "$1" | tr '[:lower:]' '[:upper:]'
}
log() {
echo -e "$(date +"%b %d %H:%M:%S %Z") [$(uppercase "$1")] $2"
}
to_camel_case() {
echo "$1" | awk -F_ '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1' OFS=""
}
uown() {
_UID="$(id -u "$1")"
chown -R "$_UID":"$_UID" "$2"
}

View File

@ -1,5 +0,0 @@
{
"$schema": "https://json.schemastore.org/mocharc.json",
"require": ["tsx", "chai/register-expect", "mocha.setup.js"],
"timeout": 10000
}

View File

@ -12,4 +12,4 @@ static
pnpm-lock.yaml
package-lock.json
yarn.lock
tsconfig.json
tsconfig.json

View File

@ -1,17 +0,0 @@
{
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"plugins": ["prettier-plugin-svelte"]
}
}
]
}

View File

@ -2,7 +2,7 @@
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "gray"
},

View File

@ -1 +0,0 @@
process.env.NODE_ENV = 'test';

View File

@ -1,17 +1,13 @@
{
"name": "web",
"version": "0.0.0-dev",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "mocha",
"check:format": "prettier --check .",
"format": "prettier --write .",
"start": "node ./build/index.js"
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"packageManager": "pnpm@8.15.0",
"engines": {
@ -19,50 +15,45 @@
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.5.5",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/chai": "^4.3.14",
"@sveltejs/kit": "^2.5.10",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.6",
"@types/node": "^20.12.2",
"@types/node": "^20.12.12",
"@types/qrcode": "^1.5.5",
"autoprefixer": "^10.4.19",
"chai": "^5.1.0",
"mocha": "^10.4.0",
"postcss": "^8.4.38",
"postcss-load-config": "^5.0.3",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"svelte": "^4.2.12",
"svelte-check": "^3.6.8",
"svelte-preprocess": "^5.1.3",
"sveltekit-superforms": "^2.12.2",
"postcss-load-config": "^5.1.0",
"svelte": "^4.2.17",
"svelte-check": "^3.7.1",
"svelte-preprocess": "^5.1.4",
"sveltekit-superforms": "^2.13.1",
"tailwindcss": "^3.4.3",
"tslib": "^2.6.2",
"tsx": "^4.7.1",
"typescript": "^5.4.3",
"vite": "^5.2.7",
"zod": "^3.22.4"
"tsx": "^4.10.5",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"zod": "^3.23.8"
},
"dependencies": {
"@litehex/storage-box": "^0.2.2-canary.0",
"@t3-oss/env-core": "0.7.3",
"bits-ui": "^0.18.0",
"clsx": "^2.1.0",
"bits-ui": "^0.21.9",
"clsx": "^2.1.1",
"deepmerge": "^4.3.1",
"dotenv": "^16.4.4",
"execa": "^8.0.1",
"dotenv": "^16.4.5",
"execa": "^9.1.0",
"formsnap": "^1.0.0",
"jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.330.0",
"lucide-svelte": "^0.379.0",
"mode-watcher": "^0.3.0",
"node-netkit": "0.1.0-canary.2",
"pino": "^8.18.0",
"pino-pretty": "^10.3.1",
"node-netkit": "0.1.0-canary.3",
"p-safe": "^1.0.0",
"pino": "^9.1.0",
"pino-pretty": "^11.0.0",
"pretty-bytes": "^6.1.1",
"qrcode": "^1.5.3",
"storage-box": "^1.0.0-canary.4",
"svelte-french-toast": "^1.2.0",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.2.0"
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -74,6 +74,9 @@
}
body {
@apply bg-background text-foreground;
font-feature-settings:
'rlig' 1,
'calt' 1;
}
}

View File

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<link rel="stylesheet" href="%sveltekit.assets%/fontawesome/all.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>

View File

@ -1,25 +1,25 @@
import { type Handle, redirect } from '@sveltejs/kit';
import { verifyToken } from '$lib/auth';
import { AUTH_COOKIE } from '$lib/constants';
import 'dotenv/config';
import { redirect, type Handle } from '@sveltejs/kit';
import { verifyToken } from '$lib/auth';
import { AUTH_COOKIE } from '$lib/constants';
import logger from '$lib/logger';
export const handle: Handle = async ({ event, resolve }) => {
if (!AUTH_EXCEPTION.includes(event.url.pathname)) {
const token = event.cookies.get(AUTH_COOKIE);
const token_valid = await verifyToken(token ?? '');
logger.debug(`-> ${event.request.method} ${event.url.pathname}`);
const is_login_page = event.url.pathname === '/login';
if (!token_valid && !is_login_page) {
// return redirect;
throw redirect(303, '/login');
}
const token = event.cookies.get(AUTH_COOKIE);
const token_valid = await verifyToken(token ?? '');
if (token_valid && is_login_page) {
throw redirect(303, '/');
}
const is_login_page = event.url.pathname === '/login';
if (!token_valid && !is_login_page) {
throw redirect(303, '/login');
}
if (token_valid && is_login_page) {
throw redirect(303, '/');
}
return resolve(event);
};
const AUTH_EXCEPTION = ['/api/health'];

View File

@ -1,34 +1,42 @@
import jwt from 'jsonwebtoken';
import { client } from '$lib/storage';
import { env } from '$lib/env';
export async function generateToken(): Promise<string> {
import { WG_AUTH_PATH } from '$lib/constants';
import { env } from '$lib/env';
import { storage } from '$lib/storage';
import { sha256 } from '$lib/utils/hash';
interface GenerateTokenParams {
expiresIn: number;
}
export async function generateToken(params: GenerateTokenParams): Promise<string> {
const now = Math.floor(Date.now() / 1000);
const oneHour = 60 * 60;
const token = jwt.sign(
{
ok: true,
iat: now,
exp: now + oneHour,
exp: now + params.expiresIn,
},
env.AUTH_SECRET,
env.AUTH_SECRET
);
client.setex(token, '1', oneHour);
await storage.lpushex(WG_AUTH_PATH, sha256(token), params.expiresIn);
return token;
}
export async function verifyToken(token: string): Promise<boolean> {
try {
const decode = jwt.verify(token, env.AUTH_SECRET);
if (!decode) return false;
if (!token || !(await storage.lexists(WG_AUTH_PATH, sha256(token)))) return false;
const exists = client.exists(token);
return exists;
return !!jwt.verify(token, env.AUTH_SECRET);
} catch (e) {
return false;
}
}
export async function revokeToken(token: string): Promise<void> {
client.del(token);
if (!token) return;
const index = await storage
.lgetall(WG_AUTH_PATH)
.then((l) => l.findIndex((t) => t === sha256(token)));
await storage.ldel(WG_AUTH_PATH, index);
}

View File

@ -1,5 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { ClipboardCopyIcon } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
export let showInHover: boolean = false;
export let rootClass: string | undefined = undefined;
@ -12,20 +14,23 @@
};
</script>
<div class={cn('group flex items-center', rootClass)}>
<div class={cn('group flex items-center gap-3', rootClass)}>
<slot />
<i
<Button
aria-roledescription="Copy to clipboard"
role="button"
tabindex="0"
class={cn(
'ml-2 mb-0.5 far fa-copy cursor-pointer text-gray-400/80 hover:text-primary',
showInHover && 'group-hover:opacity-100 opacity-0',
className,
)}
size="none"
variant="ghost"
on:click={handleCopy}
on:keydown={(e) => {
if (e.key === 'Enter') handleCopy();
}}
/>
>
<ClipboardCopyIcon
class={cn(
'h-4 w-4 cursor-pointer text-gray-400/80 hover:text-primary',
showInHover && 'group-hover:opacity-100 opacity-0',
className
)}
/>
</Button>
</div>

View File

@ -2,6 +2,8 @@
import { cn } from '$lib/utils';
import { createEventDispatcher } from 'svelte';
import type { ZodEffects, ZodString } from 'zod';
import { SquarePenIcon } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
export let editMode: boolean = false;
export let schema: ZodString | ZodEffects<any>;
@ -40,7 +42,7 @@
editMode ? 'block' : 'hidden',
'w-full ring-2 ring-neutral-800 ring-offset-2 rounded transition-colors duration-200 ease-in-out outline-transparent',
inputClass,
error && 'ring-red-500 rounded',
error && 'ring-red-500 rounded'
)}
{value}
on:keydown={(e) => {
@ -60,14 +62,16 @@
}}
/>
<i
class="fal fa-pen-to-square text-sm opacity-0 group-hover:opacity-100 text-neutral-400 hover:text-primary cursor-pointer"
role="button"
tabindex="0"
<Button
class="opacity-0 group-hover:opacity-100 text-gray-400/80 group-hover:text-primary"
aria-roledescription="Edit"
size="none"
variant="ghost"
on:click={handleEnterEditMode}
on:keydown={(e) => {
if (e.key === 'Enter') handleEnterEditMode();
}}
/>
>
<SquarePenIcon class="h-4 w-4" />
</Button>
</div>

View File

@ -1,7 +1,8 @@
import Root from './empty.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Description from './empty-description.svelte';
import SimpleImage from './empty-simple-img.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Root from './empty.svelte';
interface Props extends HTMLAttributes<HTMLDivElement> {
description?: string | null;

View File

@ -0,0 +1,14 @@
<script lang="ts">
import Icon from '$lib/components/iconset/icon.svelte';
import type { Props } from '$lib/components/iconset';
type $$Props = Props;
</script>
<Icon {...$$restProps} viewBox="0 0 56 30" fill="currentColor">
<path
d="M47.084 5.32128C42.17 2.30228 35.377 0.436279 27.878 0.436279C20.378 0.436279 13.588 2.30228 8.67402 5.32128C3.75902 8.33728 0.718018 12.5053 0.718018 17.1103C0.718018 21.7143 3.75902 25.8823 8.67402 28.8983C13.588 31.9183 20.378 24.6273 27.878 24.6273C35.377 24.6273 42.17 31.9183 47.084 28.8983C51.998 25.8823 55.039 21.7133 55.039 17.1103C55.039 12.5043 51.998 8.33728 47.084 5.32128ZM17.083 17.4883C14.385 17.4883 12.198 16.5663 12.198 15.4273C12.198 14.2873 14.385 13.3643 17.083 13.3643C19.78 13.3643 21.968 14.2883 21.968 15.4273C21.968 16.5663 19.78 17.4883 17.083 17.4883ZM38.584 17.6793C35.898 17.6793 33.723 16.8233 33.723 15.7673C33.723 14.7133 35.899 13.8563 38.584 13.8563C41.269 13.8563 43.447 14.7133 43.447 15.7673C43.447 16.8233 41.269 17.6793 38.584 17.6793Z"
fill="#6700AD"
aria-label="Dnsmasq"
/>
</Icon>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
export let name: string | undefined = undefined;
export let color = 'currentColor';
export let size: number | string = 24;
export let strokeWidth: number | string = 2;
export let absoluteStrokeWidth: boolean = false;
</script>
<svg
{...$$restProps}
width={size}
height={size}
stroke={color}
stroke-width={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
class={cn('lucide-icon', 'lucide', name ? `lucide-${name}` : '', $$props.class)}
>
<slot />
</svg>

View File

@ -0,0 +1,24 @@
import type { SVGAttributes } from 'svelte/elements';
import Dnsmasq from './dnsmasq-icon.svelte';
import Root from './icon.svelte';
import Onion from './onion-icon.svelte';
interface Props extends SVGAttributes<SVGSVGElement> {
color?: string;
size?: number | string;
strokeWidth?: number | string;
absoluteStrokeWidth?: boolean;
class?: string;
}
export {
Root,
Dnsmasq,
Onion,
type Props,
//
Root as Icon,
Dnsmasq as DnsmasqIcon,
Onion as OnionIcon,
};

View File

@ -0,0 +1,20 @@
<script lang="ts">
import Icon from '$lib/components/iconset/icon.svelte';
import type { Props } from '$lib/components/iconset';
type $$Props = Props;
</script>
<Icon {...$$restProps} viewBox="0 0 512 512" fill="currentColor">
<path
d="M256 502C391.862 502 502 391.862 502 256C502 120.138 391.862 10 256 10C120.138 10 10 120.138 10 256C10 391.862 120.138 502 256 502Z"
fill="#F2E4FF"
fill-opacity="0.5"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M256.525 465.44V434.407C354.826 434.123 434.421 354.365 434.421 255.993C434.421 157.628 354.826 77.8702 256.525 77.5863V46.5532C371.964 46.8442 465.447 140.49 465.447 255.993C465.447 371.503 371.964 465.156 256.525 465.44ZM256.525 356.82C311.97 356.529 356.849 311.516 356.849 255.993C356.849 200.477 311.97 155.464 256.525 155.173V124.147C329.115 124.43 387.882 183.339 387.882 255.993C387.882 328.654 329.115 387.562 256.525 387.846V356.82ZM256.525 201.719C286.267 202.003 310.303 226.18 310.303 255.993C310.303 285.812 286.267 309.99 256.525 310.274V201.719ZM0 255.993C0 397.384 114.609 512 256 512C397.384 512 512 397.384 512 255.993C512 114.609 397.384 0 256 0C114.609 0 0 114.609 0 255.993Z"
fill="#7D4698"
/>
</Icon>

View File

@ -12,7 +12,7 @@
</a>
<DotDivider className="font-bold text-gray-400" />
<a
href={'https://github.com/shahradelahi/wireadmin'}
href={'https://github.com/wireadmin/wireadmin'}
title={'Github'}
class={'px-2 font-medium text-gray-400/80 hover:text-gray-500 text-xs'}
>

View File

@ -4,7 +4,7 @@
import { toggleMode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
import { LogOutIcon } from 'lucide-svelte';
export let showLogout: boolean = false;
</script>
@ -18,12 +18,12 @@
<div class={'flex items-center gap-x-3'}>
<a
href={'https://github.com/shahradelahi/wireadmin'}
href={'https://github.com/wireadmin/wireadmin'}
title={'Giv me a star on Github'}
class="hidden md:block"
>
<img
src={'https://img.shields.io/github/stars/shahradelahi/wireadmin.svg?style=social&label=Star'}
src={'https://img.shields.io/github/stars/wireadmin/wireadmin.svg?style=social&label=Star'}
alt={'Gimme a Star'}
/>
</a>
@ -41,17 +41,8 @@
{#if showLogout}
<a href="/logout" rel="external" title="Logout">
<Button variant="ghost" class="group text-sm/2 gap-x-2 font-medium">
<i
class={cn(
'far fa-arrow-right-from-arc text-sm mr-0.5',
'text-neutral-500 group-hover:text-neutral-800',
'dark:text-neutral-400 dark:group-hover:text-neutral-100',
)}
></i>
<span
class="text-neutral-700 hover:text-neutral-800 dark:text-neutral-100 dark:hover:text-neutral-100"
>Logout</span
>
<LogOutIcon class={'w-4 h-4 mr-0.5'} />
Logout
</Button>
</a>
{/if}

View File

@ -1,4 +1,5 @@
import { tv, type VariantProps } from 'tailwind-variants';
export { default as Badge } from './badge.svelte';
export const badgeVariants = tv({

View File

@ -0,0 +1,24 @@
<script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn('flex h-9 w-9 items-center justify-center', className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLLiAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn('inline-flex items-center gap-1.5', className)}>
<slot />
</li>

View File

@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props['href'] = undefined;
export let el: $$Props['el'] = undefined;
export let asChild: $$Props['asChild'] = false;
let className: $$Props['class'] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn('transition-colors hover:text-foreground', className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}

View File

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className
)}
{...$$restProps}
>
<slot />
</ol>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props['el'] = undefined;
export let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn('font-normal text-foreground', className)}
{...$$restProps}
>
<slot />
</span>

View File

@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLLiAttributes } from 'svelte/elements';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn('[&>svg]:size-3.5', className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>

View File

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>

View File

@ -0,0 +1,25 @@
import Ellipsis from './breadcrumb-ellipsis.svelte';
import Item from './breadcrumb-item.svelte';
import Link from './breadcrumb-link.svelte';
import List from './breadcrumb-list.svelte';
import Page from './breadcrumb-page.svelte';
import Separator from './breadcrumb-separator.svelte';
import Root from './breadcrumb.svelte';
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Button as ButtonPrimitive } from 'bits-ui';
import { type Events, type Props, buttonVariants } from './index';
import { cn } from '$lib/utils';
import { type Events, type Props, buttonVariants } from '.';
type $$Props = Props;
type $$Events = Events;

View File

@ -1,5 +1,6 @@
import { type VariantProps, tv } from 'tailwind-variants';
import type { Button as ButtonPrimitive } from 'bits-ui';
import { tv, type VariantProps } from 'tailwind-variants';
import Root from './button.svelte';
const buttonVariants = tv({
@ -8,7 +9,6 @@ const buttonVariants = tv({
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
success: 'bg-green-500 text-white hover:bg-green-500/90 hover:text-gray-50',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
@ -19,6 +19,7 @@ const buttonVariants = tv({
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
none: '',
},
},
defaultVariants: {

View File

@ -1,9 +1,9 @@
import Root from './card.svelte';
import Content from './card-content.svelte';
import Description from './card-description.svelte';
import Footer from './card-footer.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
import Root from './card.svelte';
export {
Root,

View File

@ -15,7 +15,7 @@
<CheckboxPrimitive.Root
class={cn(
'peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50',
className,
className
)}
bind:checked
{...$$restProps}

View File

@ -1,4 +1,5 @@
import Root from './checkbox.svelte';
export {
Root,
//

View File

@ -1,4 +1,5 @@
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
import Content from './collapsible-content.svelte';
const Root = CollapsiblePrimitive.Root;

View File

@ -21,7 +21,7 @@
{transitionConfig}
class={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
className,
className
)}
{...$$restProps}
>

View File

@ -1,17 +1,17 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Portal from './dialog-portal.svelte';
import Title from './dialog-title.svelte';
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
import Title from './dialog-title.svelte';
import Portal from './dialog-portal.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
export {
Root,
Title,

View File

@ -1,27 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { Checkbox as CheckboxPrimitive } from 'bits-ui';
import { Checkbox } from '$lib/components/ui/checkbox';
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
const { name, setValue, attrStore, value } = getFormField();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name: nameAttr, value: valueAttr, ...rest } = $attrStore;
</script>
<Checkbox
{...rest}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v);
setValue(v);
}}
{...$$restProps}
on:click
on:keydown
/>
<input hidden {name} value={$value} />

View File

@ -1,7 +1,7 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;

View File

@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;

View File

@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;

View File

@ -1,13 +1,12 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldsetProps<T, U>;

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { HTMLInputAttributes } from 'svelte/elements';
import { Input, type InputEvents } from '$lib/components/ui/input';
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
const { attrStore, value } = getFormField();
</script>
<Input
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
/>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div class={cn('space-y-2', className)} {...$$restProps}>
<slot />
</div>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils';
import { Label } from '$lib/components/ui/label';
import { cn } from '$lib/utils.js';
import { Label } from '$lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.LegendProps;

View File

@ -1,24 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap';
import { buttonVariants } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
import { ChevronDown } from 'lucide-svelte';
import type { HTMLSelectAttributes } from 'svelte/elements';
type $$Props = HTMLSelectAttributes;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Select
class={cn(
buttonVariants({ variant: 'outline' }),
'appearance-none bg-transparent font-normal',
className,
)}
{...$$restProps}
>
<slot />
</FormPrimitive.Select>
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />

View File

@ -1,22 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import * as RadioGroup from '$lib/components/ui/radio-group';
type $$Props = RadioGroupPrimitive.Props;
const { attrStore, setValue, name, value } = getFormField();
export let onValueChange: $$Props['onValueChange'] = undefined;
</script>
<RadioGroup.Root
{...$attrStore}
onValueChange={(v) => {
onValueChange?.(v);
setValue(v);
}}
{...$$restProps}
>
<slot />
<input hidden {name} value={$value} />
</RadioGroup.Root>

View File

@ -1,17 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import type { Select as SelectPrimitive } from 'bits-ui';
import { getFormField } from 'formsnap';
type $$Props = SelectPrimitive.TriggerProps & {
placeholder?: string;
};
type $$Events = SelectPrimitive.TriggerEvents;
const { attrStore } = getFormField();
export let placeholder = '';
</script>
<Select.Trigger {...$$restProps} {...$attrStore} on:click on:keydown>
<Select.Value {placeholder} />
<slot />
</Select.Trigger>

View File

@ -1,20 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { getFormField } from 'formsnap';
import type { Select as SelectPrimitive } from 'bits-ui';
type $$Props = SelectPrimitive.Props;
const { setValue, name, value } = getFormField();
export let onSelectedChange: $$Props['onSelectedChange'] = undefined;
</script>
<Select.Root
onSelectedChange={(v) => {
onSelectedChange?.(v);
setValue(v ? v.value : undefined);
}}
{...$$restProps}
>
<slot />
<input hidden {name} value={$value} />
</Select.Root>

View File

@ -1,25 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { Switch as SwitchPrimitive } from 'bits-ui';
import { Switch } from '$lib/components/ui/switch';
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
const { name, setValue, attrStore, value } = getFormField();
</script>
<Switch
{...$attrStore}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v);
setValue(v);
}}
{...$$restProps}
on:click
on:keydown
/>
<input hidden {name} value={$value} />

View File

@ -1,29 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { HTMLTextareaAttributes } from 'svelte/elements';
import type { TextareaGetFormField } from './index';
import { Textarea, type TextareaEvents } from '$lib/components/ui/textarea';
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
const { attrStore, value } = getFormField() as TextareaGetFormField;
</script>
<Textarea
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
/>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap';
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Validation
class={cn('text-sm font-medium text-destructive', className)}
{...$$restProps}
/>

View File

@ -1,12 +1,13 @@
import * as FormPrimitive from 'formsnap';
import Button from './form-button.svelte';
import Description from './form-description.svelte';
import Label from './form-label.svelte';
import ElementField from './form-element-field.svelte';
import FieldErrors from './form-field-errors.svelte';
import Field from './form-field.svelte';
import Fieldset from './form-fieldset.svelte';
import Label from './form-label.svelte';
import Legend from './form-legend.svelte';
import ElementField from './form-element-field.svelte';
import Button from './form-button.svelte';
const Control = FormPrimitive.Control;

View File

@ -14,7 +14,7 @@
<input
class={cn(
'flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-foreground file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
bind:value
on:blur

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
@ -12,7 +12,7 @@
<LabelPrimitive.Root
class={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className,
className
)}
{...$$restProps}
on:mousedown

View File

@ -1,7 +1,8 @@
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import Root from './radio-group.svelte';
import Item from './radio-group-item.svelte';
import Root from './radio-group.svelte';
const Input = RadioGroupPrimitive.Input;
export {

View File

@ -15,7 +15,7 @@
{value}
class={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
{...$$restProps}
on:click

View File

@ -1,10 +1,10 @@
import { Select as SelectPrimitive } from 'bits-ui';
import Label from './select-label.svelte';
import Item from './select-item.svelte';
import Content from './select-content.svelte';
import Trigger from './select-trigger.svelte';
import Item from './select-item.svelte';
import Label from './select-label.svelte';
import Separator from './select-separator.svelte';
import Trigger from './select-trigger.svelte';
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;

View File

@ -28,7 +28,7 @@
{sideOffset}
class={cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none',
className,
className
)}
{...$$restProps}
on:keydown

View File

@ -19,7 +19,7 @@
{label}
class={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
className,
className
)}
{...$$restProps}
on:click

View File

@ -13,7 +13,7 @@
<SelectPrimitive.Trigger
class={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
className
)}
{...$$restProps}
let:builder

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '$lib/utils.js';
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
@ -14,7 +14,7 @@
bind:checked
class={cn(
'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className,
className
)}
{...$$restProps}
on:click
@ -22,7 +22,7 @@
>
<SwitchPrimitive.Thumb
class={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>

View File

@ -12,7 +12,7 @@
<textarea
class={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
bind:value
on:blur

View File

@ -1,5 +1,6 @@
export const WG_PATH = '/etc/wireguard';
export const WG_SEVER_PATH = `WG::SERVERS`;
export const WG_SEVER_PATH = `WG::SERVER`;
export const WG_AUTH_PATH = `WG::AUTH`;
export const AUTH_COOKIE = 'authorization';

View File

@ -1,18 +1,26 @@
import 'dotenv/config';
import { randomUUID } from 'node:crypto';
import { createEnv } from '@t3-oss/env-core';
import { z } from 'zod';
import { sha256 } from '$lib/hash';
import { randomUUID } from 'node:crypto';
import 'dotenv/config';
import { sha256 } from '$lib/utils/hash';
export const env = createEnv({
runtimeEnv: process.env,
emptyStringAsUndefined: true,
server: {
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
STORAGE_PATH: z.string().default('/data/storage.pack'),
AUTH_SECRET: z.string().default(sha256(randomUUID())),
HASHED_PASSWORD: z.string().default(sha256('insecure-password')),
ADMIN_PASSWORD: z.string().default('insecure-password'),
// -----
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
ORIGIN: z.string().optional(),
PORT: z.string().optional(),
HOST: z.string().optional(),
// -----
LOG_LEVEL: z.string().default('trace'),
LOG_FILE_PATH: z.string().default('/var/log/wireadmin/web.log'),
LOG_COLORS: z.string().default('true'),
},
});

View File

@ -1,4 +1,4 @@
export default class ServerError extends Error {
export default class HTTPError extends Error {
statusCode;
constructor(message: string, statusCode: number = 500) {

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