Compare commits

..

6 Commits

Author SHA1 Message Date
dependabot[bot]
29dda9fcb4 chore(deps): bump the actions group with 2 updates
Bumps the actions group with 2 updates: [nolar/setup-k3d-k3s](https://github.com/nolar/setup-k3d-k3s) and [docker/login-action](https://github.com/docker/login-action).


Updates `nolar/setup-k3d-k3s` from 1.0.10 to 1.1.0
- [Release notes](https://github.com/nolar/setup-k3d-k3s/releases)
- [Commits](8bf8d22160...62c9d1bd2b)

Updates `docker/login-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](b45d80f862...4907a6ddec)

---
updated-dependencies:
- dependency-name: nolar/setup-k3d-k3s
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 13:54:41 +00:00
Mykhailo Roit
72f9123592 Add test-in-docker target to Makefile (#659)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
* Add test-in-docker target to Makefile

Add a target to run tests in Docker for non-Linux environments.

* Add GOVERSION variable to Makefile

* fix: update test-in-docker command to use user permissions

* Fix docker command syntax in Makefile
2026-04-03 22:01:07 +02:00
Mykhailo Roit
0e9e9d697f fix: "created_at" for users (#656)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
* fix: created_at for users

* added tests for: created_at for users

* cleanup fixes

---------

Co-authored-by: Christoph Haas <christoph.h@sprinternet.at>
2026-04-01 11:58:22 +02:00
Christoph
87bfd5b23a feat: allow encrypting user api token using gorm serializer 2026-04-01 11:42:07 +02:00
h44z
920806b231 chore: update frontend deps (#657)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2026-04-01 00:20:35 +02:00
Leandre Chamberland-Dozois
ec08e31eb7 feat(frontend): add confirmation dialog before deleting users, peers, and interfaces (#654)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
* feat(frontend): add confirmation dialog before deleting users, peers, and interfaces (#652)

Add a browser confirm() dialog to the delete functions in UserEditModal,
PeerEditModal, and InterfaceEditModal to prevent accidental deletions.
The bulk-delete actions in UserView already had this protection; this
change brings single-item deletion in line with that behavior.

Translation keys (confirm-delete) added for all 10 supported locales:
de, en, es, fr, ko, pt, ru, uk, vi, zh.

Signed-off-by: LeC-D <leo.openc@gmail.com>

* fix broken translation files

---------

Signed-off-by: LeC-D <leo.openc@gmail.com>
Co-authored-by: Christoph Haas <christoph.h@sprinternet.at>
2026-03-31 19:49:53 +02:00
12 changed files with 1540 additions and 1202 deletions

View File

@@ -44,7 +44,7 @@ jobs:
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml
- uses: nolar/setup-k3d-k3s@8bf8d22160e8b1d184dcb780e390d6952a7eec65 # v1.0.10
- uses: nolar/setup-k3d-k3s@62c9d1bd2bc843275c85d2e7dcd696edc1160eee # v1.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -62,7 +62,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -32,14 +32,14 @@ jobs:
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -1,7 +1,8 @@
# Go parameters
GOCMD=go
GOVERSION=1.25
MODULENAME=github.com/h44z/wg-portal
GOFILES:=$(shell go list ./... | grep -v /vendor/)
GOFILES=$(shell go list ./... | grep -v /vendor/)
BUILDDIR=dist
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
IMAGE=h44z/wg-portal
@@ -51,6 +52,11 @@ format:
.PHONY: test
test: test-vet test-race
#> test-in-docker: Run tests in Docker (for non-Linux environments e.g. MacOS)
.PHONY: test-in-docker
test-in-docker:
docker run --rm -u $(shell id -u):$(shell id -g) -e HOME=/tmp -v $(PWD):/app -w /app golang:$(GOVERSION) make test
#< test-vet: Static code analysis
.PHONY: test-vet
test-vet: build-dependencies

View File

@@ -14,7 +14,7 @@
let WGPORTAL_SITE_TITLE="WireGuard Portal";
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
</script>
<script src="/api/v0/config/frontend.js"></script>
<script src="/api/v0/config/frontend.js" vite-ignore></script>
</head>
<body class="d-flex flex-column min-vh-100">
<noscript>

File diff suppressed because it is too large Load Diff

View File

@@ -9,28 +9,28 @@
},
"dependencies": {
"@fontsource/nunito-sans": "^5.2.7",
"@fortawesome/fontawesome-free": "^7.1.0",
"@fortawesome/fontawesome-free": "^7.2.0",
"@kyvg/vue3-notification": "^3.4.2",
"@popperjs/core": "^2.11.8",
"@simplewebauthn/browser": "^13.2.2",
"@vojtechlanka/vue-tags-input": "^3.1.1",
"@simplewebauthn/browser": "^13.3.0",
"@vojtechlanka/vue-tags-input": "^3.1.2",
"bootstrap": "^5.3.8",
"bootswatch": "^5.3.8",
"cidr-tools": "^11.0.3",
"cidr-tools": "^11.3.2",
"flag-icons": "^7.5.0",
"ip-address": "^10.1.0",
"is-cidr": "^6.0.1",
"is-cidr": "^6.0.3",
"is-ip": "^5.0.1",
"pinia": "^3.0.4",
"prismjs": "^1.30.0",
"vue": "^3.5.25",
"vue-i18n": "^11.2.2",
"vue": "^3.5.31",
"vue-i18n": "^11.3.0",
"vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.6.3"
"vue-router": "^5.0.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"sass-embedded": "^1.93.3",
"vite": "^7.2.7"
"@vitejs/plugin-vue": "^6.0.5",
"sass-embedded": "^1.98.0",
"vite": "^8.0.3"
}
}

View File

@@ -26,13 +26,13 @@
display:block;
}
.modal.show {
opacity: 1;
opacity: 1.0;
}
.modal-backdrop {
background-color: rgba(0,0,0,0.6) !important;
}
.modal-backdrop.show {
opacity: 1 !important;
opacity: 1.0 !important;
}
</style>

View File

@@ -1,7 +1,6 @@
import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import InterfaceView from '../views/InterfaceView.vue'
import {authStore} from '@/stores/auth'
import {securityStore} from '@/stores/security'
@@ -20,11 +19,6 @@ const router = createRouter({
name: 'login',
component: LoginView
},
{
path: '/interface',
name: 'interface',
component: InterfaceView
},
{
path: '/interfaces',
name: 'interfaces',

View File

@@ -232,21 +232,19 @@ func (r *SqlRepo) migrate() error {
slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{}))
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
existingSysStat := SysStat{}
var existingSysStat SysStat
var err error
r.db.Order("schema_version desc").First(&existingSysStat) // get latest version
// Migration: 0 --> 1
if existingSysStat.SchemaVersion == 0 {
const schemaVersion = 1
sysStat := SysStat{
MigratedAt: time.Now(),
SchemaVersion: schemaVersion,
}
if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
}
slog.Debug("sys-stat entry written", "schema_version", schemaVersion)
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
}
// Migration: 1 --> 2
@@ -262,14 +260,10 @@ func (r *SqlRepo) migrate() error {
}
slog.Debug("migrated interface create_default_peer flags", "schema_version", schemaVersion)
}
sysStat := SysStat{
MigratedAt: time.Now(),
SchemaVersion: schemaVersion,
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
}
if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
}
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
}
// Migration: 2 --> 3
@@ -307,19 +301,45 @@ func (r *SqlRepo) migrate() error {
if err != nil {
return fmt.Errorf("failed to migrate to multi-auth: %w", err)
}
sysStat := SysStat{
MigratedAt: time.Now(),
SchemaVersion: schemaVersion,
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
}
if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
}
// Migration: 3 --> 4
if existingSysStat.SchemaVersion == 3 {
const schemaVersion = 4
cutoff := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
// Fix zero created_at timestamps for users. Set the to the last known update timestamp.
err := r.db.Model(&domain.User{}).Where("created_at < ?", cutoff).
Update("created_at", gorm.Expr("updated_at")).Error
if err != nil {
slog.Warn("failed to fix zero created_at for users", "error", err)
}
slog.Debug("fixed zero created_at timestamps for users", "schema_version", schemaVersion)
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
}
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
}
return nil
}
func (r *SqlRepo) addMigration(schemaVersion uint64) (SysStat, error) {
sysStat := SysStat{
MigratedAt: time.Now(),
SchemaVersion: schemaVersion,
}
if err := r.db.Create(&sysStat).Error; err != nil {
return SysStat{}, fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
}
return sysStat, nil
}
// region interfaces
// GetInterface returns the interface with the given id.

View File

@@ -0,0 +1,168 @@
package adapters
import (
"context"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
func newTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
require.NoError(t, err)
return db
}
func TestUpsertUser_SetsCreatedAtWhenZero(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ui := domain.SystemAdminContextUserInfo()
user := &domain.User{
Identifier: "test-user",
Email: "test@example.com",
// CreatedAt is zero
}
err := repo.upsertUser(ui, db, user)
require.NoError(t, err)
assert.False(t, user.CreatedAt.IsZero(), "CreatedAt should be set when it was zero")
assert.Equal(t, ui.UserId(), user.UpdatedBy, "UpdatedBy should be set when it was empty")
assert.WithinDuration(t, user.UpdatedAt, user.CreatedAt, time.Second,
"CreatedAt should be close to UpdatedAt for new user")
}
func TestUpsertUser_PreservesExistingCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ui := domain.SystemAdminContextUserInfo()
originalTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
user := &domain.User{
Identifier: "test-user",
Email: "test@example.com",
BaseModel: domain.BaseModel{
CreatedAt: originalTime,
CreatedBy: "original-creator",
},
}
err := repo.upsertUser(ui, db, user)
require.NoError(t, err)
assert.Equal(t, originalTime, user.CreatedAt, "CreatedAt should not be overwritten")
assert.Equal(t, "original-creator", user.CreatedBy, "CreatedBy should not be overwritten")
}
func TestSaveUser_NewUserGetsCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
before := time.Now().Add(-time.Second)
err := repo.SaveUser(ctx, "new-user", func(u *domain.User) (*domain.User, error) {
u.Email = "new@example.com"
return u, nil
})
require.NoError(t, err)
var saved domain.User
require.NoError(t, db.First(&saved, "identifier = ?", "new-user").Error)
assert.False(t, saved.CreatedAt.IsZero(), "CreatedAt should not be zero")
assert.True(t, saved.CreatedAt.After(before), "CreatedAt should be recent")
assert.NotEmpty(t, saved.CreatedBy, "CreatedBy should be set")
}
func TestMigration_FixesZeroCreatedAt(t *testing.T) {
db := newTestDB(t)
// Manually create tables and seed schema version 3
require.NoError(t, db.AutoMigrate(
&SysStat{},
&domain.User{},
&domain.UserAuthentication{},
&domain.Interface{},
&domain.Cidr{},
&domain.Peer{},
&domain.AuditEntry{},
&domain.UserWebauthnCredential{},
))
// Insert schema versions 1, 2, 3 so migration starts at 3
for v := uint64(1); v <= 3; v++ {
require.NoError(t, db.Create(&SysStat{SchemaVersion: v, MigratedAt: time.Now()}).Error)
}
updatedAt := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)
// Insert a user with zero created_at but valid updated_at
require.NoError(t, db.Exec(
"INSERT INTO users (identifier, email, created_at, updated_at) VALUES (?, ?, ?, ?)",
"zero-user", "zero@example.com", time.Time{}, updatedAt,
).Error)
// Run migration
repo := &SqlRepo{db: db, cfg: &config.Config{}}
require.NoError(t, repo.migrate())
// Verify created_at was backfilled from updated_at
var user domain.User
require.NoError(t, db.First(&user, "identifier = ?", "zero-user").Error)
assert.Equal(t, updatedAt, user.CreatedAt, "created_at should be backfilled from updated_at")
// Verify schema version advanced to 4
var latest SysStat
require.NoError(t, db.Order("schema_version DESC").First(&latest).Error)
assert.Equal(t, uint64(4), latest.SchemaVersion)
}
func TestMigration_DoesNotTouchValidCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(
&SysStat{},
&domain.User{},
&domain.UserAuthentication{},
&domain.Interface{},
&domain.Cidr{},
&domain.Peer{},
&domain.AuditEntry{},
&domain.UserWebauthnCredential{},
))
for v := uint64(1); v <= 3; v++ {
require.NoError(t, db.Create(&SysStat{SchemaVersion: v, MigratedAt: time.Now()}).Error)
}
createdAt := time.Date(2024, 3, 1, 8, 0, 0, 0, time.UTC)
updatedAt := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)
require.NoError(t, db.Exec(
"INSERT INTO users (identifier, email, created_at, updated_at) VALUES (?, ?, ?, ?)",
"valid-user", "valid@example.com", createdAt, updatedAt,
).Error)
repo := &SqlRepo{db: db, cfg: &config.Config{}}
require.NoError(t, repo.migrate())
var user domain.User
require.NoError(t, db.First(&user, "identifier = ?", "valid-user").Error)
assert.Equal(t, createdAt, user.CreatedAt, "valid created_at should not be modified")
}

View File

@@ -533,6 +533,7 @@ func (m Manager) create(ctx context.Context, user *domain.User) (*domain.User, e
}
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
user.CopyCalculatedAttributes(u, false)
return user, nil
})
if err != nil {

View File

@@ -68,7 +68,7 @@ type User struct {
WebAuthnCredentialList []UserWebauthnCredential `gorm:"foreignKey:user_identifier"` // the webauthn credentials of the user, used for webauthn authentication
// API token for REST API access
ApiToken string `form:"api_token" binding:"omitempty"`
ApiToken string `form:"api_token" binding:"omitempty" gorm:"serializer:encstr"`
ApiTokenCreated *time.Time
LinkedPeerCount int `gorm:"-"`