v2 (#11)
@@ -7,5 +7,9 @@
|
|||||||
"access": "restricted",
|
"access": "restricted",
|
||||||
"baseBranch": "canary",
|
"baseBranch": "canary",
|
||||||
"updateInternalDependencies": "patch",
|
"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
|
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
|
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.
|
||||||
32
.github/workflows/ci.yml
vendored
@@ -10,6 +10,12 @@ concurrency:
|
|||||||
group: '${{ github.workflow }}-${{ github.event.number || github.sha }}'
|
group: '${{ github.workflow }}-${{ github.event.number || github.sha }}'
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_PLATFORMS: linux/amd64,linux/arm64
|
||||||
|
DOCKERHUB_SLUG: shahradel/wireadmin
|
||||||
|
GHCR_SLUG: ghcr.io/wireadmin/wireadmin
|
||||||
|
TAG: dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -24,25 +30,37 @@ jobs:
|
|||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- run: pnpm -r install --frozen-lockfile
|
- run: pnpm -r install --frozen-lockfile
|
||||||
- run: pnpm --if-present format:check
|
- run: pnpm format:check
|
||||||
- run: pnpm --if-present lint
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
|
if: github.repository == 'wireadmin/wireadmin'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: docker/setup-buildx-action@v3
|
- 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
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.PRIVATE_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build image
|
- name: Build & Publish
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
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
|
name: Release Package
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
branches:
|
||||||
tag:
|
- canary
|
||||||
description: 'Tag to build'
|
|
||||||
required: true
|
concurrency: '${{ github.workflow }}-${{ github.ref }}'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_PLATFORMS: linux/amd64,linux/arm64
|
DOCKERHUB_SLUG: shahradel/wireadmin
|
||||||
IMAGE_TAG: ${{ github.event.inputs.tag }}
|
GHCR_SLUG: ghcr.io/wireadmin/wireadmin
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
needs: [github-registry, docker-hub]
|
if: github.repository == 'wireadmin/wireadmin'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
published: ${{ steps.changesets.outputs.published }}
|
||||||
|
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
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
|
- name: Create Release Pull Request or Publish
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
commit: 'chore(release): version package'
|
commit: 'chore(release): version package'
|
||||||
title: 'chore(release): version package'
|
title: 'chore(release): version package'
|
||||||
publish: changeset publish
|
publish: pnpm ci:publish
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
github-registry:
|
build:
|
||||||
name: Push to GitHub Container Registry
|
name: Build & Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
needs: release
|
||||||
IMAGE_NAME: shahradelahi/wireadmin
|
if: needs.release.outputs.published == 'true'
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
package: ${{ fromJson(needs.release.outputs.publishedPackages) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: docker/setup-buildx-action@v3
|
- 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
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.PRIVATE_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Push to GitHub Container Registry
|
- name: Docker meta
|
||||||
uses: docker/build-push-action@v5
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
images: |
|
||||||
file: ./Dockerfile
|
${{ 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
|
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: Check manifest
|
||||||
name: Push to DockerHub
|
run: |
|
||||||
runs-on: ubuntu-latest
|
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
|
||||||
env:
|
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
|
||||||
IMAGE_NAME: litehex/wireadmin
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Inspect image
|
||||||
uses: docker/login-action@v1
|
run: |
|
||||||
with:
|
docker pull ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
|
||||||
registry: docker.io
|
docker image inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
docker pull ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
docker image inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/build
|
build
|
||||||
web
|
dist
|
||||||
|
.svelte-kit
|
||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|||||||
23
.prettierrc
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": true,
|
"jsxSingleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "es5",
|
||||||
|
"endOfLine": "lf",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@@ -12,6 +14,25 @@
|
|||||||
"parser": "markdown",
|
"parser": "markdown",
|
||||||
"printWidth": 79
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
115
Dockerfile
@@ -1,95 +1,90 @@
|
|||||||
ARG ALPINE_VERSION=3.19
|
ARG ALPINE_VERSION=3.19
|
||||||
|
ARG LYREBIRD_VERSION=0.2.0
|
||||||
ARG NODE_VERSION=20
|
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
|
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 \
|
||||||
COPY --from=tor /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy
|
&& apk upgrade \
|
||||||
COPY --from=tor /usr/local/bin/meek-server /usr/local/bin/meek-server
|
&& apk add -U --no-cache \
|
||||||
|
iptables net-tools \
|
||||||
# Install required packages
|
screen logrotate bash \
|
||||||
RUN apk add -U --no-cache \
|
|
||||||
iproute2 iptables net-tools \
|
|
||||||
screen curl bash \
|
|
||||||
wireguard-tools \
|
wireguard-tools \
|
||||||
tor &&\
|
dnsmasq \
|
||||||
# NPM packages
|
tor \
|
||||||
npm install -g @litehex/node-checksum@0.2 &&\
|
&& rm -rf /var/cache/apk/*
|
||||||
# Clear APK cache
|
|
||||||
rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
COPY /config/torrc.template /etc/tor/torrc.template
|
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
|
||||||
|
|
||||||
# Copy user scripts
|
# Lyrebird - https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird
|
||||||
COPY /bin /usr/local/bin
|
wget "https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird/-/archive/lyrebird-$LYREBIRD_VERSION/lyrebird-lyrebird-$LYREBIRD_VERSION.tar.gz"
|
||||||
RUN chmod -R +x /usr/local/bin
|
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 web/package.json web/pnpm-lock.yaml ./
|
cp -rv /go/bin /usr/local/bin
|
||||||
|
rm -rf /go
|
||||||
|
rm -rf /tmp/*
|
||||||
|
EOT
|
||||||
|
|
||||||
# Base env
|
FROM node AS build
|
||||||
ENV PROTOCOL_HEADER=x-forwarded-proto
|
WORKDIR /app
|
||||||
ENV HOST_HEADER=x-forwarded-host
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS build
|
|
||||||
|
|
||||||
# Setup Pnpm - Pnpm only used for build stage
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
COPY web .
|
COPY web .
|
||||||
|
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile \
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile \
|
||||||
# build
|
&& NODE_ENV=production pnpm build \
|
||||||
&& mkdir -p /data \
|
|
||||||
&& echo gA== > /data/storage.b64 \
|
|
||||||
&& NODE_ENV=production pnpm run build \
|
|
||||||
# Omit devDependencies
|
|
||||||
&& pnpm prune --prod \
|
&& pnpm prune --prod \
|
||||||
# Move the goods to a temporary location
|
&& cp -R node_modules build package.json /tmp \
|
||||||
&& mv node_modules /tmp/node_modules \
|
|
||||||
&& mv build /tmp/build \
|
|
||||||
# Remove everything else
|
|
||||||
&& rm -rf ./*
|
&& 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/node_modules node_modules
|
||||||
COPY --from=build /tmp/build build
|
COPY --from=build /tmp/build build
|
||||||
|
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
RUN mkdir -p /data && chmod 700 /data
|
RUN mkdir -p /data/ /etc/tor/torrc.d/ /var/log/wireadmin/ \
|
||||||
RUN mkdir -p /etc/torrc.d && chmod -R 400 /etc/torrc.d
|
&& chmod 700 /data/ \
|
||||||
RUN mkdir -p /var/vlogs && touch /var/vlogs/web && chmod -R 600 /var/vlogs
|
&& chmod -R 400 /etc/tor/ \
|
||||||
|
&& touch /var/log/wireadmin/web.log
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
RUN echo '* * * * * /usr/bin/env logrotate /etc/logrotate.d/rotator' >/etc/crontabs/root
|
||||||
ENV LOG_LEVEL=error
|
|
||||||
|
|
||||||
# Setup entrypoint
|
# Setup entrypoint
|
||||||
COPY docker-entrypoint.sh /entrypoint.sh
|
COPY docker-entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
ENTRYPOINT ["/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
|
# Volumes
|
||||||
VOLUME ["/etc/torrc.d", "/data", "/var/vlogs"]
|
VOLUME ["/etc/tor", "/var/lib/tor", "/data"]
|
||||||
|
|
||||||
# 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));"
|
|
||||||
|
|
||||||
# Run the app
|
# Run the app
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
CMD [ "npm", "run", "start" ]
|
CMD [ "node", "/app/build/index.js" ]
|
||||||
|
|||||||
@@ -1,57 +1,42 @@
|
|||||||
ARG ALPINE_VERSION=3.19
|
ARG ALPINE_VERSION=3.19
|
||||||
ARG NODE_VERSION=20
|
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
|
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 \
|
||||||
COPY --from=tor /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy
|
&& apk upgrade \
|
||||||
COPY --from=tor /usr/local/bin/meek-server /usr/local/bin/meek-server
|
&& apk add -U --no-cache \
|
||||||
|
iptables net-tools \
|
||||||
# Install required packages
|
screen logrotate bash \
|
||||||
RUN apk add -U --no-cache \
|
|
||||||
iproute2 iptables net-tools \
|
|
||||||
screen vim curl bash \
|
|
||||||
wireguard-tools \
|
wireguard-tools \
|
||||||
tor &&\
|
dnsmasq \
|
||||||
# NPM packages
|
tor \
|
||||||
npm install -g @litehex/node-checksum@0.2 &&\
|
&& rm -rf /var/cache/apk/*
|
||||||
# Clear APK cache
|
|
||||||
rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
# Copy Tor Configs
|
FROM node
|
||||||
COPY /config/torrc.template /etc/tor/torrc.template
|
WORKDIR /app
|
||||||
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
|
|
||||||
|
|
||||||
# Setup Pnpm
|
# Setup Pnpm
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Base env
|
ENV NODE_ENV=development
|
||||||
|
ENV LOG_LEVEL=debug
|
||||||
ENV PROTOCOL_HEADER=x-forwarded-proto
|
ENV PROTOCOL_HEADER=x-forwarded-proto
|
||||||
ENV HOST_HEADER=x-forwarded-host
|
ENV HOST_HEADER=x-forwarded-host
|
||||||
|
|
||||||
|
COPY --from=tor /usr/local/bin/lyrebird /usr/local/bin/lyrebird
|
||||||
FROM base AS runner
|
COPY rootfs /
|
||||||
|
|
||||||
ENV NODE_ENV=development
|
|
||||||
ENV LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
RUN mkdir -p /data && chmod 700 /data
|
RUN mkdir -p /data/ && chmod 700 /data/
|
||||||
RUN mkdir -p /etc/torrc.d && chmod -R 400 /etc/torrc.d
|
RUN mkdir -p /etc/tor/torrc.d/ && chmod -R 400 /etc/tor/
|
||||||
RUN mkdir -p /var/vlogs && touch /var/vlogs/web && chmod -R 600 /var/vlogs
|
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
|
# Setup entrypoint
|
||||||
COPY docker-entrypoint.sh /entrypoint.sh
|
COPY docker-entrypoint.sh /entrypoint.sh
|
||||||
@@ -59,7 +44,7 @@ RUN chmod +x /entrypoint.sh
|
|||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
# Volumes
|
# Volumes
|
||||||
VOLUME ["/etc/torrc.d", "/data", "/var/vlogs"]
|
VOLUME ["/etc/tor", "/var/lib/tor", "/data"]
|
||||||
|
|
||||||
# Run the app
|
# Run the app
|
||||||
EXPOSE 5173/tcp
|
EXPOSE 5173/tcp
|
||||||
|
|||||||
129
README.md
@@ -1,6 +1,6 @@
|
|||||||
# WireGuard (Easy Admin UI)
|
# 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)
|
[](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;"/> |
|
| <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
|
## Features
|
||||||
|
|
||||||
- Simple and friendly UI
|
- Simple and friendly UI
|
||||||
@@ -20,61 +35,46 @@
|
|||||||
- Easily download the client configurations.
|
- Easily download the client configurations.
|
||||||
- Automatic Light/Dark Mode
|
- 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
|
```bash
|
||||||
git clone https://github.com/shahradelahi/wireadmin
|
docker compose up -d
|
||||||
docker buildx build --tag litehex/wireadmin ./wireadmin
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Pull from Docker Hub
|
### Command line
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run --detach \
|
$ docker run -d \
|
||||||
--name wireadmin \
|
--name wireadmin \
|
||||||
-e WG_HOST=<YOUR_SERVER_IP> \
|
-e WG_HOST="<YOUR_SERVER_IP>" \
|
||||||
-e UI_PASSWORD=<ADMIN_PASSWORD> \
|
-e ADMIN_PASSWORD="<ADMIN_PASSWORD>" \
|
||||||
-p "3000:3000/tcp" \
|
-p "3000:3000/tcp" \
|
||||||
-p "51820:51820/udp" \
|
-p "51820:51820/udp" \
|
||||||
-v "wireadmin-data:/data" \
|
|
||||||
--cap-add=NET_ADMIN \
|
--cap-add=NET_ADMIN \
|
||||||
--cap-add=SYS_MODULE \
|
--cap-add=SYS_MODULE \
|
||||||
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
|
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
|
||||||
--sysctl="net.ipv4.ip_forward=1" \
|
--sysctl="net.ipv4.ip_forward=1" \
|
||||||
litehex/wireadmin
|
ghcr.io/wireadmin/wireadmin
|
||||||
```
|
```
|
||||||
|
|
||||||
> 💡 Replace `<YOUR_SERVER_IP>` with the IP address of your server.
|
> 💡 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`.
|
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.
|
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
|
||||||
|
|
||||||
| Option | Description | Default | Optional |
|
| Option | Description | Default | Optional |
|
||||||
| ----------------- | ------------------------------------------------------------------------------- | ------------------- | -------- |
|
| ----------------- | ----------------------------------------------------------------------------------- | ------------------- | -------- |
|
||||||
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
|
| `WG_HOST` | The public IP address of the WireGuard server. | - | |
|
||||||
| `UI_PASSWORD` | The password for the admin UI. | `insecure-password` | |
|
| `ADMIN_PASSWORD` | The password for the web UI. | `insecure-password` | |
|
||||||
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
|
| `HOST` | The hostname for the WebUI. | `127.0.0.1` | ✔️ |
|
||||||
| `PORT` | The port for the WebUI. | `3000` | ✔️ |
|
| `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_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"`) | - | ✔️ |
|
| `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
|
Recreate the container whenever I push an update:
|
||||||
on [GitHub](https://github.com/shahradelahi/wireadmin/issues).
|
|
||||||
|
```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
|
## 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:
|
services:
|
||||||
wireadmin:
|
wireadmin:
|
||||||
image: wireadmin
|
image: ghcr.io/wireadmin/wireadmin:dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile-Dev
|
||||||
volumes:
|
volumes:
|
||||||
- ./web/:/app/
|
- ./web/:/app/
|
||||||
ports:
|
ports:
|
||||||
- '5173:5173'
|
- '5173:5173'
|
||||||
environment:
|
environment:
|
||||||
- WG_HOST=192.168.1.102
|
- WG_HOST=192.168.88.252
|
||||||
- UI_PASSWORD=password
|
- ADMIN_PASSWORD=password
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
|
|||||||
18
docker-compose.yml
Normal file → Executable file
@@ -1,4 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
services:
|
services:
|
||||||
wireadmin:
|
wireadmin:
|
||||||
environment:
|
environment:
|
||||||
@@ -7,16 +6,21 @@ services:
|
|||||||
- WG_HOST=localhost
|
- WG_HOST=localhost
|
||||||
# ⚠️ Required:
|
# ⚠️ Required:
|
||||||
# You can use `openssl rand -base64 8` to generate a secure password
|
# 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
|
container_name: wireadmin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- persist-data:/data
|
- wireadmin-data:/data
|
||||||
|
- tor-data:/var/lib/tor
|
||||||
ports:
|
ports:
|
||||||
- '51820:51820/udp'
|
|
||||||
- '3000:3000/tcp'
|
- '3000:3000/tcp'
|
||||||
|
# Dnsmasq
|
||||||
|
#- '53:53/udp'
|
||||||
|
#- '53:53/tcp'
|
||||||
|
# WireGuard
|
||||||
|
- '51820:51820/udp'
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
@@ -25,5 +29,5 @@ services:
|
|||||||
- net.ipv4.conf.all.src_valid_mark=1
|
- net.ipv4.conf.all.src_valid_mark=1
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
persist-data:
|
wireadmin-data:
|
||||||
driver: local
|
tor-data:
|
||||||
|
|||||||
@@ -1,60 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
source /etc/wireadmin/xscript.sh
|
||||||
|
|
||||||
ENV_FILE="/app/.env"
|
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 " _ ___ ___ __ _ "
|
echo " _ ___ ___ __ _ "
|
||||||
echo "| | / (_)_______ / | ____/ /___ ___ (_)___ "
|
echo "| | / (_)_______ / | ____/ /___ ___ (_)___ "
|
||||||
@@ -63,55 +13,47 @@ echo "| |/ |/ / / / / __/ ___ / /_/ / / / / / / / / / /"
|
|||||||
echo "|__/|__/_/_/ \___/_/ |_\__,_/_/ /_/ /_/_/_/ /_/ "
|
echo "|__/|__/_/_/ \___/_/ |_\__,_/_/ /_/ /_/_/_/ /_/ "
|
||||||
echo " "
|
echo " "
|
||||||
|
|
||||||
mkdir -p /var/vlogs
|
touch "$ENV_FILE"
|
||||||
|
chmod 400 "$ENV_FILE"
|
||||||
|
|
||||||
touch "${ENV_FILE}"
|
if [ -z "$ADMIN_PASSWORD" ]; then
|
||||||
chmod 400 "${ENV_FILE}"
|
log warn "No ADMIN_PASSWORD provided, using default password"
|
||||||
|
|
||||||
if ! grep -q "AUTH_SECRET" "${ENV_FILE}"; then
|
|
||||||
tee -a "${ENV_FILE}" &>/dev/null <<EOF
|
|
||||||
AUTH_SECRET=$(openssl rand -base64 32)
|
|
||||||
EOF
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Checking if there is `UI_PASSWORD` environment variable
|
# Remove duplicated envs
|
||||||
# if there was, converting it to sha256 and storing it to
|
awk -F= '!a[$1]++' "$ENV_FILE" > "/tmp/$(basename "$ENV_FILE")" \
|
||||||
# the .env
|
&& mv "/tmp/$(basename "$ENV_FILE")" "$ENV_FILE"
|
||||||
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
|
if [ -z "$WG_HOST" ]; then
|
||||||
log "error" "the WG_HOST environment variable is not set"
|
log "error" "the WG_HOST environment variable is not set"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove duplicated envs
|
|
||||||
awk -F= '!a[$1]++' "${ENV_FILE}" >"/tmp/$(basename "${ENV_FILE}")" &&
|
|
||||||
mv "/tmp/$(basename "${ENV_FILE}")" "${ENV_FILE}"
|
|
||||||
|
|
||||||
# Generate Tor configuration
|
# Generate Tor configuration
|
||||||
generate_tor_config
|
generate_tor_config
|
||||||
|
setup_logrotate
|
||||||
|
setup_dns
|
||||||
|
|
||||||
# Start Tor on the background
|
# Background services
|
||||||
screen -dmS "tor" tor -f "${TOR_CONFIG}"
|
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
|
sleep 1
|
||||||
echo -e "\n======================== Versions ========================"
|
echo -e "\n======================== Versions ========================"
|
||||||
echo -e "Alpine Version: \c" && cat /etc/alpine-release
|
echo -e "Alpine: \c" && cat /etc/alpine-release
|
||||||
echo -e "WireGuard Version: \c" && wg -v | head -n 1 | awk '{print $1,$2}'
|
echo -e "WireGuard: \c" && wg -v | head -n 1 | awk '{print $2}'
|
||||||
echo -e "Tor Version: \c" && tor --version | head -n 1
|
echo -e "Tor: \c" && tor --version | head -n 1 | awk '{print $3}' | sed 's/.$//'
|
||||||
echo -e "Obfs4proxy Version: \c" && obfs4proxy -version
|
echo -e "Dnsmasq: \c" && dnsmasq -v | head -n 1 | cut -d ' ' -f3
|
||||||
echo -e "\n========================= Torrc ========================="
|
echo -e "Lyrebird: \c" && lyrebird -version
|
||||||
cat "${TOR_CONFIG}"
|
echo -e "\n======================= Tor Config ======================="
|
||||||
echo -e "========================================================\n"
|
grep -v "^#" "$TOR_CONFIG"
|
||||||
|
echo -e "====================== Dnsmasq Config ======================"
|
||||||
|
grep -v "^#" "$DNSMASQ_CONFIG"
|
||||||
|
echo -e "==========================================================\n"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
22
package.json
@@ -5,20 +5,22 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@8.15.0",
|
"packageManager": "pnpm@8.15.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm docker:drop && docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
"dev": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up --force-recreate",
|
||||||
"dev:image": "docker buildx build --tag wireadmin -f Dockerfile-Dev .",
|
"dev:image": "docker buildx build --tag ghcr.io/wireadmin/wireadmin:dev -f Dockerfile-Dev .",
|
||||||
"build": "pnpm docker:build",
|
"build": "docker buildx build --tag ghcr.io/wireadmin/wireadmin .",
|
||||||
"start": "pnpm docker:drop && docker compose -f docker-compose.yml up",
|
"start": "docker compose -f docker-compose.yml up --force-recreate",
|
||||||
"docker:build": "docker buildx build --tag wireadmin .",
|
"format": "prettier --write .",
|
||||||
"docker:drop": "docker compose rm -fsv",
|
"format:check": "prettier --check . ",
|
||||||
"format": "prettier --write . && pnpm --if-present -r format",
|
"ci:publish": "pnpm build && changeset publish"
|
||||||
"format:check": "prettier --check . && pnpm --if-present -r format:check"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@changesets/cli": "^2.27.1",
|
"@changesets/cli": "^2.27.1",
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||||
"prettier": "^3.2.5",
|
"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
generated
@@ -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 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 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 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
|
|
||||||
}
|
|
||||||
@@ -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",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "default",
|
"style": "default",
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.ts",
|
||||||
"css": "src/app.css",
|
"css": "src/app.css",
|
||||||
"baseColor": "gray"
|
"baseColor": "gray"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
process.env.NODE_ENV = 'test';
|
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0-dev",
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"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"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.15.0",
|
"packageManager": "pnpm@8.15.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -19,50 +15,45 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.0.1",
|
"@sveltejs/adapter-node": "^5.0.1",
|
||||||
"@sveltejs/kit": "^2.5.5",
|
"@sveltejs/kit": "^2.5.10",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||||
"@types/chai": "^4.3.14",
|
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/node": "^20.12.12",
|
||||||
"@types/node": "^20.12.2",
|
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"chai": "^5.1.0",
|
|
||||||
"mocha": "^10.4.0",
|
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-load-config": "^5.0.3",
|
"postcss-load-config": "^5.1.0",
|
||||||
"prettier": "^3.2.5",
|
"svelte": "^4.2.17",
|
||||||
"prettier-plugin-svelte": "^3.2.2",
|
"svelte-check": "^3.7.1",
|
||||||
"svelte": "^4.2.12",
|
"svelte-preprocess": "^5.1.4",
|
||||||
"svelte-check": "^3.6.8",
|
"sveltekit-superforms": "^2.13.1",
|
||||||
"svelte-preprocess": "^5.1.3",
|
|
||||||
"sveltekit-superforms": "^2.12.2",
|
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.10.5",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.7",
|
"vite": "^5.2.11",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@litehex/storage-box": "^0.2.2-canary.0",
|
|
||||||
"@t3-oss/env-core": "0.7.3",
|
"@t3-oss/env-core": "0.7.3",
|
||||||
"bits-ui": "^0.18.0",
|
"bits-ui": "^0.21.9",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.1",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.5",
|
||||||
"execa": "^8.0.1",
|
"execa": "^9.1.0",
|
||||||
"formsnap": "^1.0.0",
|
"formsnap": "^1.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-svelte": "^0.330.0",
|
"lucide-svelte": "^0.379.0",
|
||||||
"mode-watcher": "^0.3.0",
|
"mode-watcher": "^0.3.0",
|
||||||
"node-netkit": "0.1.0-canary.2",
|
"node-netkit": "0.1.0-canary.3",
|
||||||
"pino": "^8.18.0",
|
"p-safe": "^1.0.0",
|
||||||
"pino-pretty": "^10.3.1",
|
"pino": "^9.1.0",
|
||||||
|
"pino-pretty": "^11.0.0",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
|
"storage-box": "^1.0.0-canary.4",
|
||||||
"svelte-french-toast": "^1.2.0",
|
"svelte-french-toast": "^1.2.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwind-variants": "^0.2.0"
|
"tailwind-variants": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1129
web/pnpm-lock.yaml
generated
@@ -74,6 +74,9 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
font-feature-settings:
|
||||||
|
'rlig' 1,
|
||||||
|
'calt' 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</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 '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 }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
if (!AUTH_EXCEPTION.includes(event.url.pathname)) {
|
logger.debug(`-> ${event.request.method} ${event.url.pathname}`);
|
||||||
|
|
||||||
const token = event.cookies.get(AUTH_COOKIE);
|
const token = event.cookies.get(AUTH_COOKIE);
|
||||||
const token_valid = await verifyToken(token ?? '');
|
const token_valid = await verifyToken(token ?? '');
|
||||||
|
|
||||||
const is_login_page = event.url.pathname === '/login';
|
const is_login_page = event.url.pathname === '/login';
|
||||||
if (!token_valid && !is_login_page) {
|
if (!token_valid && !is_login_page) {
|
||||||
// return redirect;
|
|
||||||
throw redirect(303, '/login');
|
throw redirect(303, '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token_valid && is_login_page) {
|
if (token_valid && is_login_page) {
|
||||||
throw redirect(303, '/');
|
throw redirect(303, '/');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTH_EXCEPTION = ['/api/health'];
|
|
||||||
|
|||||||
@@ -1,34 +1,42 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
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 now = Math.floor(Date.now() / 1000);
|
||||||
const oneHour = 60 * 60;
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
iat: now,
|
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;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyToken(token: string): Promise<boolean> {
|
export async function verifyToken(token: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const decode = jwt.verify(token, env.AUTH_SECRET);
|
if (!token || !(await storage.lexists(WG_AUTH_PATH, sha256(token)))) return false;
|
||||||
if (!decode) return false;
|
|
||||||
|
|
||||||
const exists = client.exists(token);
|
return !!jwt.verify(token, env.AUTH_SECRET);
|
||||||
return exists;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revokeToken(token: string): Promise<void> {
|
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">
|
<script lang="ts">
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
|
import { ClipboardCopyIcon } from 'lucide-svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
|
||||||
export let showInHover: boolean = false;
|
export let showInHover: boolean = false;
|
||||||
export let rootClass: string | undefined = undefined;
|
export let rootClass: string | undefined = undefined;
|
||||||
@@ -12,20 +14,23 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn('group flex items-center', rootClass)}>
|
<div class={cn('group flex items-center gap-3', rootClass)}>
|
||||||
<slot />
|
<slot />
|
||||||
<i
|
<Button
|
||||||
aria-roledescription="Copy to clipboard"
|
aria-roledescription="Copy to clipboard"
|
||||||
role="button"
|
size="none"
|
||||||
tabindex="0"
|
variant="ghost"
|
||||||
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,
|
|
||||||
)}
|
|
||||||
on:click={handleCopy}
|
on:click={handleCopy}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'Enter') handleCopy();
|
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>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type { ZodEffects, ZodString } from 'zod';
|
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 editMode: boolean = false;
|
||||||
export let schema: ZodString | ZodEffects<any>;
|
export let schema: ZodString | ZodEffects<any>;
|
||||||
@@ -40,7 +42,7 @@
|
|||||||
editMode ? 'block' : 'hidden',
|
editMode ? 'block' : 'hidden',
|
||||||
'w-full ring-2 ring-neutral-800 ring-offset-2 rounded transition-colors duration-200 ease-in-out outline-transparent',
|
'w-full ring-2 ring-neutral-800 ring-offset-2 rounded transition-colors duration-200 ease-in-out outline-transparent',
|
||||||
inputClass,
|
inputClass,
|
||||||
error && 'ring-red-500 rounded',
|
error && 'ring-red-500 rounded'
|
||||||
)}
|
)}
|
||||||
{value}
|
{value}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
@@ -60,14 +62,16 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<i
|
<Button
|
||||||
class="fal fa-pen-to-square text-sm opacity-0 group-hover:opacity-100 text-neutral-400 hover:text-primary cursor-pointer"
|
class="opacity-0 group-hover:opacity-100 text-gray-400/80 group-hover:text-primary"
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-roledescription="Edit"
|
aria-roledescription="Edit"
|
||||||
|
size="none"
|
||||||
|
variant="ghost"
|
||||||
on:click={handleEnterEditMode}
|
on:click={handleEnterEditMode}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'Enter') handleEnterEditMode();
|
if (e.key === 'Enter') handleEnterEditMode();
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<SquarePenIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Root from './empty.svelte';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
import Description from './empty-description.svelte';
|
import Description from './empty-description.svelte';
|
||||||
import SimpleImage from './empty-simple-img.svelte';
|
import SimpleImage from './empty-simple-img.svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import Root from './empty.svelte';
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
description?: string | null;
|
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>
|
</a>
|
||||||
<DotDivider className="font-bold text-gray-400" />
|
<DotDivider className="font-bold text-gray-400" />
|
||||||
<a
|
<a
|
||||||
href={'https://github.com/shahradelahi/wireadmin'}
|
href={'https://github.com/wireadmin/wireadmin'}
|
||||||
title={'Github'}
|
title={'Github'}
|
||||||
class={'px-2 font-medium text-gray-400/80 hover:text-gray-500 text-xs'}
|
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 { toggleMode } from 'mode-watcher';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { cn } from '$lib/utils';
|
import { LogOutIcon } from 'lucide-svelte';
|
||||||
|
|
||||||
export let showLogout: boolean = false;
|
export let showLogout: boolean = false;
|
||||||
</script>
|
</script>
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
|
|
||||||
<div class={'flex items-center gap-x-3'}>
|
<div class={'flex items-center gap-x-3'}>
|
||||||
<a
|
<a
|
||||||
href={'https://github.com/shahradelahi/wireadmin'}
|
href={'https://github.com/wireadmin/wireadmin'}
|
||||||
title={'Giv me a star on Github'}
|
title={'Giv me a star on Github'}
|
||||||
class="hidden md:block"
|
class="hidden md:block"
|
||||||
>
|
>
|
||||||
<img
|
<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'}
|
alt={'Gimme a Star'}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -41,17 +41,8 @@
|
|||||||
{#if showLogout}
|
{#if showLogout}
|
||||||
<a href="/logout" rel="external" title="Logout">
|
<a href="/logout" rel="external" title="Logout">
|
||||||
<Button variant="ghost" class="group text-sm/2 gap-x-2 font-medium">
|
<Button variant="ghost" class="group text-sm/2 gap-x-2 font-medium">
|
||||||
<i
|
<LogOutIcon class={'w-4 h-4 mr-0.5'} />
|
||||||
class={cn(
|
Logout
|
||||||
'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
|
|
||||||
>
|
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { tv, type VariantProps } from 'tailwind-variants';
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
export { default as Badge } from './badge.svelte';
|
export { default as Badge } from './badge.svelte';
|
||||||
|
|
||||||
export const badgeVariants = tv({
|
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">
|
<script lang="ts">
|
||||||
import { Button as ButtonPrimitive } from 'bits-ui';
|
import { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
|
import { type Events, type Props, buttonVariants } from './index';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
import { type Events, type Props, buttonVariants } from '.';
|
|
||||||
|
|
||||||
type $$Props = Props;
|
type $$Props = Props;
|
||||||
type $$Events = Events;
|
type $$Events = Events;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type VariantProps, tv } from 'tailwind-variants';
|
|
||||||
import type { Button as ButtonPrimitive } from 'bits-ui';
|
import type { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
import Root from './button.svelte';
|
import Root from './button.svelte';
|
||||||
|
|
||||||
const buttonVariants = tv({
|
const buttonVariants = tv({
|
||||||
@@ -8,7 +9,6 @@ const buttonVariants = tv({
|
|||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/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',
|
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
@@ -19,6 +19,7 @@ const buttonVariants = tv({
|
|||||||
sm: 'h-9 rounded-md px-3',
|
sm: 'h-9 rounded-md px-3',
|
||||||
lg: 'h-11 rounded-md px-8',
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: 'h-10 w-10',
|
icon: 'h-10 w-10',
|
||||||
|
none: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Root from './card.svelte';
|
|
||||||
import Content from './card-content.svelte';
|
import Content from './card-content.svelte';
|
||||||
import Description from './card-description.svelte';
|
import Description from './card-description.svelte';
|
||||||
import Footer from './card-footer.svelte';
|
import Footer from './card-footer.svelte';
|
||||||
import Header from './card-header.svelte';
|
import Header from './card-header.svelte';
|
||||||
import Title from './card-title.svelte';
|
import Title from './card-title.svelte';
|
||||||
|
import Root from './card.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
class={cn(
|
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',
|
'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
|
bind:checked
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Root from './checkbox.svelte';
|
import Root from './checkbox.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||||
|
|
||||||
import Content from './collapsible-content.svelte';
|
import Content from './collapsible-content.svelte';
|
||||||
|
|
||||||
const Root = CollapsiblePrimitive.Root;
|
const Root = CollapsiblePrimitive.Root;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{transitionConfig}
|
{transitionConfig}
|
||||||
class={cn(
|
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',
|
'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}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
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 Root = DialogPrimitive.Root;
|
||||||
const Trigger = DialogPrimitive.Trigger;
|
const Trigger = DialogPrimitive.Trigger;
|
||||||
const Close = DialogPrimitive.Close;
|
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 {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Title,
|
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">
|
<script lang="ts">
|
||||||
import * as FormPrimitive from 'formsnap';
|
import * as FormPrimitive from 'formsnap';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||||
let className: string | undefined | null = undefined;
|
let className: string | undefined | null = undefined;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
|
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
|
||||||
|
|
||||||
type T = Record<string, unknown>;
|
type T = Record<string, unknown>;
|
||||||
type U = FormPathLeaves<T>;
|
type U = FormPathLeaves<T>;
|
||||||
</script>
|
</script>
|
||||||
@@ -8,7 +7,7 @@
|
|||||||
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import * as FormPrimitive from 'formsnap';
|
import * as FormPrimitive from 'formsnap';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
|
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as FormPrimitive from 'formsnap';
|
import * as FormPrimitive from 'formsnap';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = FormPrimitive.FieldErrorsProps & {
|
type $$Props = FormPrimitive.FieldErrorsProps & {
|
||||||
errorClasses?: string | undefined | null;
|
errorClasses?: string | undefined | null;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import type { FormPath, SuperForm } from 'sveltekit-superforms';
|
import type { FormPath, SuperForm } from 'sveltekit-superforms';
|
||||||
|
|
||||||
type T = Record<string, unknown>;
|
type T = Record<string, unknown>;
|
||||||
type U = FormPath<T>;
|
type U = FormPath<T>;
|
||||||
</script>
|
</script>
|
||||||
@@ -8,7 +7,7 @@
|
|||||||
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import * as FormPrimitive from 'formsnap';
|
import * as FormPrimitive from 'formsnap';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
|
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import type { FormPath, SuperForm } from 'sveltekit-superforms';
|
import type { FormPath, SuperForm } from 'sveltekit-superforms';
|
||||||
|
|
||||||
type T = Record<string, unknown>;
|
type T = Record<string, unknown>;
|
||||||
type U = FormPath<T>;
|
type U = FormPath<T>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
|
||||||
import * as FormPrimitive from 'formsnap';
|
import * as FormPrimitive from 'formsnap';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = FormPrimitive.FieldsetProps<T, U>;
|
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">
|
<script lang="ts">
|
||||||
import type { Label as LabelPrimitive } from 'bits-ui';
|
import type { Label as LabelPrimitive } from 'bits-ui';
|
||||||
import { getFormControl } from 'formsnap';
|
import { getFormControl } from 'formsnap';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
|
||||||
type $$Props = LabelPrimitive.Props;
|
type $$Props = LabelPrimitive.Props;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as FormPrimitive from 'formsnap';
|
import * as FormPrimitive from 'formsnap';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = FormPrimitive.LegendProps;
|
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 * as FormPrimitive from 'formsnap';
|
||||||
|
|
||||||
|
import Button from './form-button.svelte';
|
||||||
import Description from './form-description.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 FieldErrors from './form-field-errors.svelte';
|
||||||
import Field from './form-field.svelte';
|
import Field from './form-field.svelte';
|
||||||
import Fieldset from './form-fieldset.svelte';
|
import Fieldset from './form-fieldset.svelte';
|
||||||
|
import Label from './form-label.svelte';
|
||||||
import Legend from './form-legend.svelte';
|
import Legend from './form-legend.svelte';
|
||||||
import ElementField from './form-element-field.svelte';
|
|
||||||
import Button from './form-button.svelte';
|
|
||||||
|
|
||||||
const Control = FormPrimitive.Control;
|
const Control = FormPrimitive.Control;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<input
|
<input
|
||||||
class={cn(
|
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',
|
'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
|
bind:value
|
||||||
on:blur
|
on:blur
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label as LabelPrimitive } from 'bits-ui';
|
import { Label as LabelPrimitive } from 'bits-ui';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = LabelPrimitive.Props;
|
type $$Props = LabelPrimitive.Props;
|
||||||
type $$Events = LabelPrimitive.Events;
|
type $$Events = LabelPrimitive.Events;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
class={cn(
|
class={cn(
|
||||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
on:mousedown
|
on:mousedown
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
import Root from './radio-group.svelte';
|
|
||||||
import Item from './radio-group-item.svelte';
|
import Item from './radio-group-item.svelte';
|
||||||
|
import Root from './radio-group.svelte';
|
||||||
|
|
||||||
const Input = RadioGroupPrimitive.Input;
|
const Input = RadioGroupPrimitive.Input;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{value}
|
{value}
|
||||||
class={cn(
|
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',
|
'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}
|
{...$$restProps}
|
||||||
on:click
|
on:click
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Select as SelectPrimitive } from 'bits-ui';
|
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 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 Separator from './select-separator.svelte';
|
||||||
|
import Trigger from './select-trigger.svelte';
|
||||||
|
|
||||||
const Root = SelectPrimitive.Root;
|
const Root = SelectPrimitive.Root;
|
||||||
const Group = SelectPrimitive.Group;
|
const Group = SelectPrimitive.Group;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{sideOffset}
|
{sideOffset}
|
||||||
class={cn(
|
class={cn(
|
||||||
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none',
|
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none',
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
on:keydown
|
on:keydown
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
{label}
|
{label}
|
||||||
class={cn(
|
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',
|
'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}
|
{...$$restProps}
|
||||||
on:click
|
on:click
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
class={cn(
|
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',
|
'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}
|
{...$$restProps}
|
||||||
let:builder
|
let:builder
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Switch as SwitchPrimitive } from 'bits-ui';
|
import { Switch as SwitchPrimitive } from 'bits-ui';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = SwitchPrimitive.Props;
|
type $$Props = SwitchPrimitive.Props;
|
||||||
type $$Events = SwitchPrimitive.Events;
|
type $$Events = SwitchPrimitive.Events;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
bind:checked
|
bind:checked
|
||||||
class={cn(
|
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',
|
'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}
|
{...$$restProps}
|
||||||
on:click
|
on:click
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
class={cn(
|
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>
|
</SwitchPrimitive.Root>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
class={cn(
|
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',
|
'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
|
bind:value
|
||||||
on:blur
|
on:blur
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const WG_PATH = '/etc/wireguard';
|
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';
|
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 { createEnv } from '@t3-oss/env-core';
|
||||||
import { z } from 'zod';
|
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({
|
export const env = createEnv({
|
||||||
runtimeEnv: process.env,
|
runtimeEnv: process.env,
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
server: {
|
server: {
|
||||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
||||||
STORAGE_PATH: z.string().default('/data/storage.pack'),
|
STORAGE_PATH: z.string().default('/data/storage.pack'),
|
||||||
AUTH_SECRET: z.string().default(sha256(randomUUID())),
|
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(),
|
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;
|
statusCode;
|
||||||
|
|
||||||
constructor(message: string, statusCode: number = 500) {
|
constructor(message: string, statusCode: number = 500) {
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
|
import { createWriteStream, promises } from 'node:fs';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
|
import { trySafe } from 'p-safe';
|
||||||
import pino, { type Logger, type LoggerOptions } from 'pino';
|
import pino, { type Logger, type LoggerOptions } from 'pino';
|
||||||
import pretty from 'pino-pretty';
|
import pretty from 'pino-pretty';
|
||||||
import { createWriteStream } from 'node:fs';
|
|
||||||
import { resolve } from 'node:path';
|
|
||||||
import { fsAccess, fsTouch } from '$lib/fs-extra';
|
|
||||||
|
|
||||||
const LOG_LEVEL = process.env.LOG_LEVEL || 'trace';
|
import { env } from '$lib/env';
|
||||||
const LOG_FILE_PATH = process.env.LOG_FILE_PATH || '/var/vlogs/web';
|
import { fsAccess } from '$lib/utils/fs-extra';
|
||||||
const LOG_COLORS = process.env.LOG_COLORS || 'true';
|
|
||||||
|
|
||||||
const options: LoggerOptions = {
|
const options: LoggerOptions = {
|
||||||
level: LOG_LEVEL,
|
level: env.LOG_LEVEL,
|
||||||
customLevels: {
|
customLevels: {
|
||||||
trace: 10,
|
trace: 10,
|
||||||
info: 30,
|
info: 30,
|
||||||
@@ -25,25 +24,41 @@ const jsonLevels = JSON.stringify(options.customLevels);
|
|||||||
const levelsInString = jsonLevels.replaceAll('"', '').slice(0, -1).slice(1);
|
const levelsInString = jsonLevels.replaceAll('"', '').slice(0, -1).slice(1);
|
||||||
|
|
||||||
const prettyStream = pretty({
|
const prettyStream = pretty({
|
||||||
colorize: LOG_COLORS === 'true',
|
colorize: env.LOG_COLORS === 'true',
|
||||||
customLevels: levelsInString,
|
customLevels: levelsInString,
|
||||||
});
|
});
|
||||||
|
|
||||||
let logger: Logger = pino(options, pino.multistream([prettyStream]));
|
let logger: Logger = pino(options, pino.multistream([prettyStream]));
|
||||||
|
|
||||||
if (fsAccess(LOG_FILE_PATH)) {
|
export function errorBox<T = Error>(e: T) {
|
||||||
fsTouch(LOG_FILE_PATH).then(() => {
|
console.error('');
|
||||||
|
console.error('---------------- ERROR ----------------');
|
||||||
|
logger.error(e);
|
||||||
|
console.error('---------------- ERROR ----------------');
|
||||||
|
console.error('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await trySafe(async () => {
|
||||||
|
const logDir = dirname(env.LOG_FILE_PATH);
|
||||||
|
if (!fsAccess(logDir)) {
|
||||||
|
await promises.mkdir(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fsAccess(env.LOG_FILE_PATH)) {
|
||||||
|
await promises.writeFile(env.LOG_FILE_PATH, '', { encoding: 'utf-8' });
|
||||||
|
}
|
||||||
logger = pino(
|
logger = pino(
|
||||||
options,
|
options,
|
||||||
pino.multistream([
|
pino.multistream([
|
||||||
prettyStream,
|
prettyStream,
|
||||||
createWriteStream(resolve(LOG_FILE_PATH), {
|
createWriteStream(env.LOG_FILE_PATH, {
|
||||||
flags: 'a',
|
flags: 'a',
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
|
if (error) {
|
||||||
logger.warn('Log file is not accessible');
|
logger.warn('Log file is not accessible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { ip } from 'node-netkit';
|
|
||||||
|
|
||||||
export default class Network {
|
export default class Network {
|
||||||
public static async dropInterface(inet: string) {
|
|
||||||
await execa(`ip link delete dev ${inet}`, { shell: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async defaultInterface(): Promise<string> {
|
|
||||||
const route = await ip.route.defaultRoute();
|
|
||||||
if (!route) throw new Error('No default route found');
|
|
||||||
return route.dev;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async interfaceExists(inet: string): Promise<boolean> {
|
public static async interfaceExists(inet: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { stdout: o } = await execa(`ip link show | grep ${inet}`, { shell: true });
|
const { stdout: o } = await execa(`ip link show | grep ${inet}`, { shell: true });
|
||||||
@@ -25,7 +14,7 @@ export default class Network {
|
|||||||
const ports = [];
|
const ports = [];
|
||||||
const { stdout: output } = await execa(
|
const { stdout: output } = await execa(
|
||||||
`netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`,
|
`netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`,
|
||||||
{ shell: true },
|
{ shell: true }
|
||||||
);
|
);
|
||||||
for (const line of output.split('\n')) {
|
for (const line of output.split('\n')) {
|
||||||
const clean = Number(line.trim());
|
const clean = Number(line.trim());
|
||||||
|
|||||||