Release fixes (#12)

* Release fixes

* fixes

* test
This commit is contained in:
Günter Grodotzki 2026-04-23 16:13:25 +02:00 committed by GitHub
parent 929034a0b1
commit 067715f48a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 216 additions and 224 deletions

View File

@ -6,9 +6,13 @@ on:
pull_request:
branches: [main, master]
permissions: {}
jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
@ -66,6 +70,8 @@ jobs:
docker:
runs-on: ubuntu-latest
needs: build-and-test
permissions:
contents: read
steps:
- uses: actions/checkout@v6
@ -84,6 +90,7 @@ jobs:
needs: build-and-test
if: always()
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Finalize Coveralls
uses: coverallsapp/github-action@v2

View File

@ -5,9 +5,7 @@ on:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
permissions:
contents: write
packages: write
permissions: {}
env:
REGISTRY: ghcr.io
@ -16,6 +14,8 @@ env:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
@ -48,6 +48,9 @@ jobs:
docker:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
@ -87,7 +90,7 @@ jobs:
with:
push: true
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
@ -100,6 +103,8 @@ jobs:
binaries:
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
fail-fast: false
matrix:
@ -108,17 +113,10 @@ jobs:
goarch: amd64
- goos: linux
goarch: arm64
- goos: linux
goarch: arm
goarm: "7"
- goos: linux
goarch: "386"
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: freebsd
goarch: amd64
steps:
- uses: actions/checkout@v6
@ -142,17 +140,17 @@ jobs:
CGO_ENABLED: "0"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
run: |
BINARY_NAME="wireguard-ui-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ -n "${{ matrix.goarm }}" ]; then
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
fi
go build -ldflags="-X 'main.appVersion=${{ github.ref_name }}' -X 'main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.gitCommit=${{ github.sha }}'" \
-o "${BINARY_NAME}" .
tar czf "${BINARY_NAME}.tar.gz" "${BINARY_NAME}" init.sh
ARCHIVE="wireguard-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz"
go build -trimpath \
-ldflags="-s -w -X 'main.appVersion=${{ github.ref_name }}' -X 'main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.gitCommit=${{ github.sha }}'" \
-o wireguard-ui .
tar czf "${ARCHIVE}" wireguard-ui
sha256sum "${ARCHIVE}" >> checksums.txt
- name: Upload release asset
- name: Upload release assets
uses: softprops/action-gh-release@v3
with:
files: wireguard-ui-*.tar.gz
files: |
wireguard-ui-*.tar.gz
checksums.txt

View File

@ -32,8 +32,9 @@ COPY . .
COPY --from=frontend /src/assets ./assets/
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-trimpath \
-ldflags="-s -w -X 'main.appVersion=${APP_VERSION}' -X 'main.buildTime=${BUILD_TIME}' -X 'main.gitCommit=${GIT_COMMIT}'" \
-a -o wg-ui .
-o wg-ui .
# Stage 3: Runtime
FROM alpine:3.23

View File

@ -5,7 +5,7 @@ GO_PACKAGES := $(shell go list ./... | grep -v 'wireguard-ui$$' | grep -v node_m
VERSION ?= dev
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "N/A")
BUILD_TIME := $(shell date -u '+%Y-%m-%d %H:%M:%S')
LDFLAGS := -X 'main.appVersion=$(VERSION)' -X 'main.buildTime=$(BUILD_TIME)' -X 'main.gitCommit=$(GIT_COMMIT)'
LDFLAGS := -s -w -X 'main.appVersion=$(VERSION)' -X 'main.buildTime=$(BUILD_TIME)' -X 'main.gitCommit=$(GIT_COMMIT)'
.PHONY: help build build-frontend build-backend test test-verbose coverage lint lint-go lint-frontend fmt vet clean dev
@ -20,7 +20,7 @@ build-frontend: ## Build the React frontend
npm ci && npm run build
build-backend: ## Build the Go binary
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o $(APP_NAME) .
CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o $(APP_NAME) .
## ---- Test ----

169
README.md
View File

@ -1,28 +1,26 @@
# digitaltolk/wireguard-ui
[![CI](https://github.com/DigitalTolk/wireguard-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/DigitalTolk/wireguard-ui/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/DigitalTolk/wireguard-ui/badge.svg)](https://coveralls.io/github/DigitalTolk/wireguard-ui)
A web user interface to manage your WireGuard setup.
A modern web interface to manage your WireGuard VPN setup.
Fork of [ngoduykhanh/wireguard-ui](https://github.com/ngoduykhanh/wireguard-ui).
Fork of [ngoduykhanh/wireguard-ui](https://github.com/ngoduykhanh/wireguard-ui) by [Khanh Ngo](https://github.com/ngoduykhanh).
## Features
- Modern React frontend built with shadcn/ui components (WCAG accessible)
- Single Sign-On via OpenID Connect (OIDC)
- Audit logging of all administrative actions
- SQLite database backend
- Client management with QR codes, email delivery, and Excel export
- Multi-platform Docker images (amd64, arm64, armv7)
- Modern React frontend with [shadcn/ui](https://ui.shadcn.com/) components
- WCAG accessible, auto dark mode, mobile-friendly
- Single Sign-On via OpenID Connect (Microsoft Entra ID / any OIDC provider)
- Audit logging with Excel export (ISO 27001 evidence)
- SQLite database (pure Go, no CGO)
- Client management: QR codes, config download, email delivery
- Server-side search and filtering with bookmarkable URLs
- Input validation (frontend + backend)
- Multi-platform Docker images (linux/amd64, linux/arm64)
## Quick Start
### Docker
```sh
docker pull digitaltolk/wireguard-ui
```
### Docker Compose
```yaml
@ -34,51 +32,156 @@ services:
- NET_ADMIN
network_mode: host
environment:
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
- OIDC_CLIENT_ID=your-app-client-id
- OIDC_CLIENT_SECRET=your-app-client-secret
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
- SESSION_SECRET=change-me-to-a-random-string
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
volumes:
- ./db:/app/db
- /etc/wireguard:/etc/wireguard
restart: unless-stopped
```
> The default username and password are `admin`. Change them to secure your setup.
### Docker
See [`examples/docker-compose`](examples/docker-compose) for more configurations
(LinuxServer, BoringTun, host networking).
```sh
docker pull digitaltolk/wireguard-ui:latest
# or from GitHub Container Registry:
docker pull ghcr.io/digitaltolk/wireguard-ui:latest
```
### Binary
Download the binary from the [Releases](https://github.com/DigitalTolk/wireguard-ui/releases) page:
Download from [Releases](https://github.com/DigitalTolk/wireguard-ui/releases):
```sh
./wireguard-ui
tar xzf wireguard-ui-linux-amd64.tar.gz -C /usr/local/bin
wireguard-ui
```
## Environment Variables
Refer to the environment variable tables in the upstream documentation or inspect
the source for a full list. Key variables:
### Application
| Variable | Description | Default |
|---|---|---|
| `BIND_ADDRESS` | Listen address | `0.0.0.0:80` |
| `SESSION_SECRET` | Secret for session cookies | N/A |
| `WGUI_USERNAME` | Initial admin username | `admin` |
| `WGUI_PASSWORD` | Initial admin password | `admin` |
| `WGUI_CONFIG_FILE_PATH` | WireGuard config path | `/etc/wireguard/wg0.conf` |
| `WGUI_MANAGE_START` | Start WireGuard with container | `false` |
| `WGUI_MANAGE_RESTART` | Restart WireGuard on config apply | `false` |
| `BIND_ADDRESS` | Listen address and port | `0.0.0.0:5000` |
| `BASE_PATH` | URL base path (for reverse proxy) | `` |
| `SESSION_SECRET` | Secret key for session cookies | random |
| `SESSION_SECRET_FILE` | File containing session secret | |
| `SESSION_MAX_DURATION` | Max session lifetime in days | `90` |
| `DISABLE_LOGIN` | Disable authentication (development only) | `false` |
| `WGUI_LOG_LEVEL` | Log level: DEBUG, INFO, WARN, ERROR, OFF | `INFO` |
| `WGUI_FAVICON_FILE_PATH` | Custom favicon file path | |
## Build
### OIDC / SSO (required for production)
| Variable | Description | Default |
|---|---|---|
| `OIDC_ISSUER_URL` | OIDC provider URL (e.g. `https://login.microsoftonline.com/{tenant}/v2.0`) | |
| `OIDC_CLIENT_ID` | OAuth2 client ID | |
| `OIDC_CLIENT_SECRET` | OAuth2 client secret | |
| `OIDC_CLIENT_SECRET_FILE` | File containing client secret | |
| `OIDC_REDIRECT_URL` | Callback URL (e.g. `https://vpn.example.com/api/v1/auth/oidc/callback`) | |
| `OIDC_SCOPES` | Comma-separated scopes | `openid,profile,email` |
| `OIDC_AUTO_PROVISION` | Auto-create users on first OIDC login | `true` |
| `OIDC_ADMIN_GROUPS` | Comma-separated group UUIDs for auto-admin | |
### WireGuard Server
| Variable | Description | Default |
|---|---|---|
| `WGUI_ENDPOINT_ADDRESS` | Public endpoint address for clients | auto-detected |
| `WGUI_SERVER_INTERFACE_ADDRESSES` | Server interface CIDR addresses | `10.252.1.0/24` |
| `WGUI_SERVER_LISTEN_PORT` | WireGuard listen port | `51820` |
| `WGUI_SERVER_POST_UP_SCRIPT` | Post-up script | |
| `WGUI_SERVER_POST_DOWN_SCRIPT` | Post-down script | |
| `WGUI_DNS` | DNS servers pushed to clients (comma-separated) | `1.1.1.1` |
| `WGUI_MTU` | MTU size | `1450` |
| `WGUI_PERSISTENT_KEEPALIVE` | Keepalive interval in seconds | `15` |
| `WGUI_FIREWALL_MARK` | Firewall mark (hex) | `0xca6c` |
| `WGUI_TABLE` | Routing table | `auto` |
| `WGUI_CONFIG_FILE_PATH` | Path to write wg0.conf | `/etc/wireguard/wg0.conf` |
| `WG_CONF_TEMPLATE` | Custom wg.conf template path | built-in |
| `SUBNET_RANGES` | Named subnet ranges (e.g. `LAN:10.0.0.0/24;REMOTE:192.168.0.0/24`) | |
### Client Defaults
| Variable | Description | Default |
|---|---|---|
| `WGUI_DEFAULT_CLIENT_ALLOWED_IPS` | Default allowed IPs for new clients | `0.0.0.0/0` |
| `WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS` | Default extra allowed IPs | |
| `WGUI_DEFAULT_CLIENT_USE_SERVER_DNS` | Use server DNS by default | `true` |
| `WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION` | Enable client after creation | `true` |
### Email (SMTP)
| Variable | Description | Default |
|---|---|---|
| `SMTP_HOSTNAME` | SMTP server hostname | `127.0.0.1` |
| `SMTP_PORT` | SMTP server port | `25` |
| `SMTP_USERNAME` | SMTP username | |
| `SMTP_PASSWORD` | SMTP password | |
| `SMTP_PASSWORD_FILE` | File containing SMTP password | |
| `SMTP_HELO` | SMTP HELO hostname | `localhost` |
| `SMTP_ENCRYPTION` | NONE, SSL, SSLTLS, TLS, STARTTLS | `STARTTLS` |
| `SMTP_AUTH_TYPE` | PLAIN, LOGIN, NONE | `NONE` |
| `EMAIL_FROM_ADDRESS` | Sender email address | |
| `EMAIL_FROM_NAME` | Sender display name | `WireGuard UI` |
### Email (SendGrid)
| Variable | Description | Default |
|---|---|---|
| `SENDGRID_API_KEY` | SendGrid API key | |
| `SENDGRID_API_KEY_FILE` | File containing SendGrid API key | |
### Container Management
| Variable | Description | Default |
|---|---|---|
| `WGUI_MANAGE_START` | Start WireGuard when container starts | `false` |
| `WGUI_MANAGE_RESTART` | Restart WireGuard when config changes | `false` |
## Development
```sh
# Build everything (frontend + Go binary)
# Install dependencies
make deps && make deps-frontend
# Start Go backend (port 5000)
make dev
# Start frontend dev server with hot reload (port 5173)
make dev-frontend
# Run all tests
make test
# Run linters
make lint
# Build everything
make build
# Build Docker image
docker build -t wireguard-ui .
make docker-build
```
### Available Make Targets
```
make help Show all commands
make build Build frontend + Go binary
make test Run all tests with coverage
make lint Run Go + frontend linters
make dev Start Go backend
make dev-frontend Start Vite dev server
make docker-build Build Docker image
make coverage-html Open coverage report in browser
```
## License

View File

@ -80,14 +80,13 @@ func TestAddressField_WithoutName(t *testing.T) {
// --- NewSmtpMail tests ---
func TestNewSmtpMail(t *testing.T) {
s := NewSmtpMail("smtp.example.com", 587, "user", "pass", "helo.example.com", true, "PLAIN", "Sender", "sender@example.com", "TLS")
s := NewSmtpMail("smtp.example.com", 587, "user", "pass", "helo.example.com", "PLAIN", "Sender", "sender@example.com", "TLS")
assert.Equal(t, "smtp.example.com", s.hostname)
assert.Equal(t, 587, s.port)
assert.Equal(t, "user", s.username)
assert.Equal(t, "pass", s.password)
assert.Equal(t, "helo.example.com", s.smtpHelo)
assert.True(t, s.noTLSCheck)
assert.Equal(t, mail.AuthPlain, s.authType)
assert.Equal(t, mail.EncryptionTLS, s.encryption)
assert.Equal(t, "Sender", s.fromName)
@ -95,11 +94,10 @@ func TestNewSmtpMail(t *testing.T) {
}
func TestNewSmtpMail_Defaults(t *testing.T) {
s := NewSmtpMail("host", 25, "", "", "", false, "", "", "from@test.com", "")
s := NewSmtpMail("host", 25, "", "", "", "", "", "from@test.com", "")
assert.Equal(t, mail.AuthNone, s.authType)
assert.Equal(t, mail.EncryptionSTARTTLS, s.encryption)
assert.False(t, s.noTLSCheck)
}
// --- NewSendgridApiMail tests ---
@ -123,14 +121,14 @@ func TestNewSendgridApiMail_Empty(t *testing.T) {
// --- SmtpMail.Send tests (connection error path) ---
func TestSmtpMail_Send_ConnectionError(t *testing.T) {
s := NewSmtpMail("127.0.0.1", 1, "user", "pass", "", false, "PLAIN", "Sender", "from@test.com", "NONE")
s := NewSmtpMail("127.0.0.1", 1, "user", "pass", "", "PLAIN", "Sender", "from@test.com", "NONE")
err := s.Send("Recipient", "to@test.com", "Subject", "<p>Body</p>", nil)
assert.Error(t, err, "Send should fail when SMTP server is unreachable")
}
func TestSmtpMail_Send_ConnectionError_WithTLSCheck(t *testing.T) {
s := NewSmtpMail("127.0.0.1", 1, "", "", "helo.test", true, "LOGIN", "From Name", "from@test.com", "SSL")
func TestSmtpMail_Send_ConnectionError_SSL(t *testing.T) {
s := NewSmtpMail("127.0.0.1", 1, "", "", "helo.test", "LOGIN", "From Name", "from@test.com", "SSL")
err := s.Send("Recipient", "to@test.com", "Subject", "Body", []Attachment{
{Name: "test.txt", Data: []byte("hello")},
@ -139,7 +137,7 @@ func TestSmtpMail_Send_ConnectionError_WithTLSCheck(t *testing.T) {
}
func TestSmtpMail_Send_ConnectionError_STARTTLS(t *testing.T) {
s := NewSmtpMail("127.0.0.1", 1, "", "", "", false, "", "", "from@test.com", "STARTTLS")
s := NewSmtpMail("127.0.0.1", 1, "", "", "", "", "", "from@test.com", "STARTTLS")
err := s.Send("", "to@test.com", "Test", "Content", nil)
assert.Error(t, err)

View File

@ -1,7 +1,6 @@
package emailer
import (
"crypto/tls"
"fmt"
"strings"
"time"
@ -17,7 +16,6 @@ type SmtpMail struct {
smtpHelo string
authType mail.AuthType
encryption mail.Encryption
noTLSCheck bool
fromName string
from string
}
@ -48,8 +46,8 @@ func encryptionType(encryptionType string) mail.Encryption {
}
}
func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, noTLSCheck bool, auth string, fromName, from string, encryption string) *SmtpMail {
ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)}
func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, auth string, fromName, from string, encryption string) *SmtpMail {
ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)}
return &ans
}
@ -74,10 +72,6 @@ func (o *SmtpMail) Send(toName string, to string, subject string, content string
server.ConnectTimeout = 10 * time.Second
server.SendTimeout = 10 * time.Second
if o.noTLSCheck {
server.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
smtpClient, err := server.Connect()
if err != nil {

View File

@ -1,12 +1,9 @@
version: "3"
services:
boringtun:
image: ghcr.io/ntkme/boringtun:edge
command:
- wg0
container_name: boringtun
# use the network of the 'wireguard-ui' service. this enables to show active clients in the status page
network_mode: service:wireguard-ui
cap_add:
- NET_ADMIN
@ -20,12 +17,14 @@ services:
cap_add:
- NET_ADMIN
environment:
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
- OIDC_CLIENT_ID=your-client-id
- OIDC_CLIENT_SECRET=your-client-secret
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
- SESSION_SECRET=change-me
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
@ -37,7 +36,6 @@ services:
- ./db:/app/db
- ./config:/etc/wireguard
ports:
# port for wireguard-ui
- "5000:5000"
# port of the wireguard server. this must be set here as the `boringtun` container joins the network of this container and hasn't its own network over which it could publish the ports
- "51820:51820/udp"
restart: unless-stopped

View File

@ -1,5 +1,3 @@
version: "3"
services:
wireguard:
image: linuxserver/wireguard:latest
@ -9,9 +7,7 @@ services:
volumes:
- ./config:/config
ports:
# port for wireguard-ui. this must be set here as the `wireguard-ui` container joins the network of this container and hasn't its own network over which it could publish the ports
- "5000:5000"
# port of the wireguard server
- "51820:51820/udp"
wireguard-ui:
@ -21,15 +17,16 @@ services:
- wireguard
cap_add:
- NET_ADMIN
# use the network of the 'wireguard' service. this enables to show active clients in the status page
network_mode: service:wireguard
environment:
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
- OIDC_CLIENT_ID=your-client-id
- OIDC_CLIENT_SECRET=your-client-secret
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
- SESSION_SECRET=change-me
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
@ -40,3 +37,4 @@ services:
volumes:
- ./db:/app/db
- ./config:/etc/wireguard
restart: unless-stopped

View File

@ -1,23 +1,22 @@
version: "3"
services:
wireguard-ui:
image: digitaltolk/wireguard-ui:latest
container_name: wireguard-ui
cap_add:
- NET_ADMIN
# required to show active clients. with this set, you don't need to expose the ui port (5000) anymore
network_mode: host
environment:
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
- OIDC_CLIENT_ID=your-client-id
- OIDC_CLIENT_SECRET=your-client-secret
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
- SESSION_SECRET=change-me
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=false
- WGUI_MANAGE_RESTART=false
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
logging:
driver: json-file
options:
@ -25,3 +24,4 @@ services:
volumes:
- ./db:/app/db
- /etc/wireguard:/etc/wireguard
restart: unless-stopped

2
go.mod
View File

@ -13,7 +13,7 @@ require (
github.com/sendgrid/sendgrid-go v3.16.1+incompatible
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/xhit/go-simple-mail/v2 v2.16.0
golang.org/x/crypto v0.50.0
golang.org/x/crypto v0.50.0 // indirect
//golang.zx2c4.com/wireguard v0.0.20200121 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/go-playground/validator.v9 v9.31.0

View File

@ -5,11 +5,13 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/DigitalTolk/wireguard-ui/model"
"github.com/DigitalTolk/wireguard-ui/util"
)
@ -176,6 +178,10 @@ func TestAPIGetMe_WithSession(t *testing.T) {
env := setupTestEnv(t)
util.DisableLogin = false
// Create the user that the session will reference
now := time.Now().UTC()
env.db.SaveUser(model.User{Username: "admin", Email: "admin@test.com", Admin: true, CreatedAt: now, UpdatedAt: now})
// Create a route that first creates a session and then calls APIGetMe
env.echo.GET("/setup-and-getme", func(c echo.Context) error {
// Create a session for admin user

View File

@ -130,6 +130,10 @@ func TestFindOrCreateOIDCUser_NewUser_AdminViaGroups(t *testing.T) {
func TestFindOrCreateOIDCUser_NewUser_NotAdmin(t *testing.T) {
env := setupTestEnv(t)
now := time.Now().UTC()
// pre-create a user so the next one isn't the first (first user auto-gets admin)
env.db.SaveUser(model.User{Username: "existing", OIDCSub: "sub-existing", Admin: true, CreatedAt: now, UpdatedAt: now})
origAutoProvision := util.OIDCAutoProvision
util.OIDCAutoProvision = true

View File

@ -39,7 +39,6 @@ var (
flagSmtpUsername string
flagSmtpPassword string
flagSmtpAuthType = "NONE"
flagSmtpNoTLSCheck = false
flagSmtpEncryption = "STARTTLS"
flagSmtpHelo = "localhost"
flagSendgridApiKey string
@ -78,7 +77,6 @@ func init() {
flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname")
flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username")
flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.")
flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)")
flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.")
flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
@ -122,7 +120,6 @@ func init() {
util.SmtpUsername = flagSmtpUsername
util.SmtpPassword = flagSmtpPassword
util.SmtpAuthType = flagSmtpAuthType
util.SmtpNoTLSCheck = flagSmtpNoTLSCheck
util.SmtpEncryption = flagSmtpEncryption
util.SendgridApiKey = flagSendgridApiKey
util.EmailFrom = flagEmailFrom
@ -214,7 +211,7 @@ func main() {
if util.SendgridApiKey != "" {
sendmail = emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom)
} else {
sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpHelo, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpHelo, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
}
// set up Echo with session middleware

View File

@ -4,14 +4,11 @@ import "time"
// User model
type User struct {
Username string `json:"username"`
Password string `json:"password,omitempty"`
// PasswordHash takes precedence over Password.
PasswordHash string `json:"password_hash,omitempty"`
Email string `json:"email"`
DisplayName string `json:"display_name"`
OIDCSub string `json:"oidc_sub,omitempty"`
Admin bool `json:"admin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Username string `json:"username"`
Email string `json:"email"`
DisplayName string `json:"display_name"`
OIDCSub string `json:"oidc_sub,omitempty"`
Admin bool `json:"admin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -134,23 +134,7 @@ func (o *SqliteDB) Init() error {
o.db.Exec(`INSERT INTO hashes (id, client, server) VALUES (1, 'none', 'none')`)
}
// default user
var userCount int
o.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount)
if userCount == 0 {
username := util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
now := time.Now().UTC()
_, err := o.db.Exec(
`INSERT INTO users (username, admin, created_at, updated_at) VALUES (?, ?, ?, ?)`,
username, true, now, now,
)
if err != nil {
return fmt.Errorf("cannot create default user: %w", err)
}
}
// init caches
// init caches (first OIDC login auto-provisions admin user)
users, err := o.GetUsers()
if err == nil {
util.DBUsersToCRC32Mutex.Lock()

View File

@ -423,10 +423,10 @@ func TestInit_CreatesDefaults(t *testing.T) {
err := db.Init()
require.NoError(t, err)
// should have created default user
// no default user — first OIDC login creates admin
users, err := db.GetUsers()
require.NoError(t, err)
assert.GreaterOrEqual(t, len(users), 1)
assert.Len(t, users, 0)
// should have created server config
server, err := db.GetServer()

View File

@ -15,7 +15,6 @@ var (
SmtpPort int
SmtpUsername string
SmtpPassword string
SmtpNoTLSCheck bool
SmtpEncryption string
SmtpAuthType string
SmtpHelo string
@ -40,9 +39,6 @@ var (
)
const (
DefaultUsername = "admin"
DefaultPassword = "admin"
DefaultIsAdmin = true
DefaultServerAddress = "10.252.1.0/24"
DefaultServerPort = 51820
DefaultDNS = "1.1.1.1"
@ -51,11 +47,6 @@ const (
DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultTable = "auto"
DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
UsernameEnvVar = "WGUI_USERNAME"
PasswordEnvVar = "WGUI_PASSWORD"
PasswordFileEnvVar = "WGUI_PASSWORD_FILE"
PasswordHashEnvVar = "WGUI_PASSWORD_HASH"
PasswordHashFileEnvVar = "WGUI_PASSWORD_HASH_FILE"
FaviconFilePathEnvVar = "WGUI_FAVICON_FILE_PATH"
EndpointAddressEnvVar = "WGUI_ENDPOINT_ADDRESS"
DNSEnvVar = "WGUI_DNS"

View File

@ -1,32 +0,0 @@
package util
import (
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
)
func HashPassword(plaintext string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), 14)
if err != nil {
return "", fmt.Errorf("cannot hash password: %w", err)
}
return base64.StdEncoding.EncodeToString(bytes), nil
}
func VerifyHash(base64Hash string, plaintext string) (bool, error) {
hash, err := base64.StdEncoding.DecodeString(base64Hash)
if err != nil {
return false, fmt.Errorf("cannot decode base64 hash: %w", err)
}
err = bcrypt.CompareHashAndPassword(hash, []byte(plaintext))
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("cannot verify password: %w", err)
}
return true, nil
}

View File

@ -1,52 +0,0 @@
package util
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHashPassword(t *testing.T) {
hash, err := HashPassword("mypassword")
require.NoError(t, err)
assert.NotEmpty(t, hash)
// hash is base64 encoded
assert.Greater(t, len(hash), 20)
// different calls produce different hashes (bcrypt salt)
hash2, err := HashPassword("mypassword")
require.NoError(t, err)
assert.NotEqual(t, hash, hash2)
}
func TestVerifyHash(t *testing.T) {
hash, err := HashPassword("testpassword")
require.NoError(t, err)
match, err := VerifyHash(hash, "testpassword")
assert.NoError(t, err)
assert.True(t, match)
match, err = VerifyHash(hash, "wrongpassword")
assert.NoError(t, err)
assert.False(t, match)
}
func TestVerifyHash_InvalidBase64(t *testing.T) {
_, err := VerifyHash("not-base64!!!", "password")
assert.Error(t, err)
}
func TestVerifyHash_InvalidBcryptHash(t *testing.T) {
// Valid base64 but not a valid bcrypt hash
_, err := VerifyHash("aGVsbG93b3JsZA==", "password")
assert.Error(t, err)
}
func TestHashPassword_EmptyPassword(t *testing.T) {
hash, err := HashPassword("")
require.NoError(t, err)
assert.NotEmpty(t, hash)
}