v2 (#11)
@ -7,5 +7,9 @@
|
||||
"access": "restricted",
|
||||
"baseBranch": "canary",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"ignore": [],
|
||||
"privatePackages": {
|
||||
"version": true,
|
||||
"tag": true
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
'wireadmin': patch
|
||||
"wireadmin": patch
|
||||
---
|
||||
|
||||
fix: Improve password hashing method and env loader
|
||||
|
5
.changeset/mighty-kangaroos-talk.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"wireadmin": major
|
||||
---
|
||||
|
||||
BREAKING: `UI_PASSWORD` has been removed. Please use `ADMIN_PASSWORD` instead.
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
'wireadmin': patch
|
||||
"wireadmin": patch
|
||||
---
|
||||
|
||||
fix: tor config generation when container restarts
|
||||
|
5
.changeset/thick-moles-kick.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"wireadmin": major
|
||||
---
|
||||
|
||||
feat: Creates a Dnsmasq server on port 53 and forwards DNS queries through the Tor network.
|
@ -9,4 +9,4 @@ Dockerfile
|
||||
*.md
|
||||
tests/
|
||||
*.log
|
||||
tmp/
|
||||
tmp/
|
||||
|
32
.github/workflows/ci.yml
vendored
@ -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 }}'
|
||||
|
75
.github/workflows/docker-image.yaml
vendored
@ -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 }}
|
123
.github/workflows/release.yaml
vendored
@ -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 }}
|
||||
|
@ -1,7 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
web
|
||||
build
|
||||
dist
|
||||
.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
|
23
.prettierrc
@ -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"
|
||||
]
|
||||
}
|
||||
|
123
Dockerfile
@ -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" ]
|
||||
|
@ -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
@ -1,6 +1,6 @@
|
||||
# WireGuard (Easy Admin UI)
|
||||
|
||||
[](https://github.com/shahradelahi/wireadmin/actions/workflows/ci.yml)
|
||||
[](https://github.com/wireadmin/wireadmin/actions/workflows/ci.yml)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
|
||||

|
||||
@ -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
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 65 KiB |
BIN
assets/screenshot-2.png
Normal file → Executable file
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 77 KiB |
BIN
assets/screenshot-3.png
Normal file → Executable file
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
BIN
assets/screenshot-4.png
Normal file → Executable file
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
16
bin/logs
@ -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
|
@ -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
@ -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"
|
||||
]
|
||||
}
|
@ -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
@ -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:
|
||||
|
@ -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 "$@"
|
||||
|
22
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
983
pnpm-lock.yaml
@ -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
|
9
rootfs/etc/tor/torrc.template
Normal 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
|
54
rootfs/etc/wireadmin/internal/dns.sh
Normal 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
|
||||
}
|
15
rootfs/etc/wireadmin/internal/logrotate.sh
Executable 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
|
||||
}
|
95
rootfs/etc/wireadmin/internal/tor.sh
Executable 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
@ -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"
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/mocharc.json",
|
||||
"require": ["tsx", "chai/register-expect", "mocha.setup.js"],
|
||||
"timeout": 10000
|
||||
}
|
@ -12,4 +12,4 @@ static
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
tsconfig.json
|
||||
tsconfig.json
|
||||
|
@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
},
|
||||
|
@ -1 +0,0 @@
|
||||
process.env.NODE_ENV = 'test';
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
1129
web/pnpm-lock.yaml
@ -74,6 +74,9 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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'];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
14
web/src/lib/components/iconset/dnsmasq-icon.svelte
Normal 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>
|
20
web/src/lib/components/iconset/icon.svelte
Normal 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>
|
24
web/src/lib/components/iconset/index.ts
Normal 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,
|
||||
};
|
20
web/src/lib/components/iconset/onion-icon.svelte
Normal 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>
|
@ -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'}
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export { default as Badge } from './badge.svelte';
|
||||
|
||||
export const badgeVariants = tv({
|
||||
|
@ -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>
|
16
web/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal 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>
|
31
web/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal 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}
|
23
web/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal 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>
|
23
web/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal 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>
|
@ -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>
|
15
web/src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal 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>
|
25
web/src/lib/components/ui/breadcrumb/index.ts
Normal 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,
|
||||
};
|
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Root from './checkbox.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
import Content from './collapsible-content.svelte';
|
||||
|
||||
const Root = CollapsiblePrimitive.Root;
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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,
|
||||
|
@ -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} />
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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
|
||||
/>
|
@ -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>
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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" />
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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} />
|
@ -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
|
||||
/>
|
@ -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}
|
||||
/>
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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'),
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
export default class ServerError extends Error {
|
||||
export default class HTTPError extends Error {
|
||||
statusCode;
|
||||
|
||||
constructor(message: string, statusCode: number = 500) {
|