mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 07:56:17 +00:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0cc7ebb83e | ||
|
eb6a787cfc | ||
|
b546eec4ed | ||
|
9be2133220 | ||
|
b05837b2d9 | ||
|
08c8f8eac0 | ||
|
d864e24145 | ||
|
5b56e58fe9 | ||
|
930ef7b573 | ||
|
8816165260 | ||
|
ab9995350f |
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -66,10 +66,6 @@ jobs:
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
# add v{{major}} tag, even for beta or release-canidate releases
|
||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
# add {{major}} tag, even for beta releases or release-canidate releases
|
||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
|
@@ -61,6 +61,26 @@ const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
const wgVersion = ref(WGPORTAL_VERSION);
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
|
||||
const userDisplayName = computed(() => {
|
||||
let displayName = "Unknown";
|
||||
if (auth.IsAuthenticated) {
|
||||
if (auth.User.Firstname === "" && auth.User.Lastname === "") {
|
||||
displayName = auth.User.Identifier;
|
||||
} else if (auth.User.Firstname === "" && auth.User.Lastname !== "") {
|
||||
displayName = auth.User.Lastname;
|
||||
} else if (auth.User.Firstname !== "" && auth.User.Lastname === "") {
|
||||
displayName = auth.User.Firstname;
|
||||
} else if (auth.User.Firstname !== "" && auth.User.Lastname !== "") {
|
||||
displayName = auth.User.Firstname + " " + auth.User.Lastname;
|
||||
}
|
||||
}
|
||||
|
||||
// pad string to 20 characters so that the menu is always the same size on desktop
|
||||
if (displayName.length < 20 && window.innerWidth > 992) {
|
||||
displayName = displayName.padStart(20, "\u00A0");
|
||||
}
|
||||
return displayName;
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,7 +113,7 @@ const currentYear = ref(new Date().getFullYear())
|
||||
<div class="navbar-nav d-flex justify-content-end">
|
||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||
href="#" role="button">{{ userDisplayName }}</a>
|
||||
<div class="dropdown-menu">
|
||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||
|
@@ -32,7 +32,7 @@ const selectedInterface = computed(() => {
|
||||
function freshForm() {
|
||||
return {
|
||||
Identifiers: [],
|
||||
Suffix: "",
|
||||
Prefix: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ async function save() {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Prefix">
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
@@ -57,6 +57,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login/{provider}/callback": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Handle the OAuth callback.",
|
||||
"operationId": "auth_handleOauthCallbackGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login/{provider}/init": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Initiate the OAuth login flow.",
|
||||
"operationId": "auth_handleOauthInitiateGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@@ -1805,7 +1851,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Suffix": {
|
||||
"Prefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@@ -206,7 +206,7 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
Suffix:
|
||||
Prefix:
|
||||
type: string
|
||||
type: object
|
||||
model.Peer:
|
||||
@@ -383,6 +383,8 @@ definitions:
|
||||
type: boolean
|
||||
MailLinkOnly:
|
||||
type: boolean
|
||||
MinPasswordLength:
|
||||
type: integer
|
||||
PersistentConfigSupported:
|
||||
type: boolean
|
||||
SelfProvisioning:
|
||||
@@ -456,7 +458,22 @@ paths:
|
||||
summary: Get all available audit entries. Ordered by timestamp.
|
||||
tags:
|
||||
- Audit
|
||||
/auth/{provider}/callback:
|
||||
/auth/login:
|
||||
post:
|
||||
operationId: auth_handleLoginPost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/login/{provider}/callback:
|
||||
get:
|
||||
operationId: auth_handleOauthCallbackGet
|
||||
produces:
|
||||
@@ -471,7 +488,7 @@ paths:
|
||||
summary: Handle the OAuth callback.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/{provider}/init:
|
||||
/auth/login/{provider}/init:
|
||||
get:
|
||||
operationId: auth_handleOauthInitiateGet
|
||||
produces:
|
||||
@@ -486,21 +503,6 @@ paths:
|
||||
summary: Initiate the OAuth login flow.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/login:
|
||||
post:
|
||||
operationId: auth_handleLoginPost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/logout:
|
||||
post:
|
||||
operationId: auth_handleLogoutPost
|
||||
|
@@ -138,7 +138,8 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
||||
s.versions[version].HandleFunc("GET /doc.html", s.rapiDocHandler(version))
|
||||
|
||||
groupSetupFn(s.versions[version])
|
||||
versionGroup := s.versions[version].Group()
|
||||
groupSetupFn(versionGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -132,7 +133,7 @@ func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc {
|
||||
// @Summary Initiate the OAuth login flow.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.LoginProviderInfo
|
||||
// @Router /auth/{provider}/init [get]
|
||||
// @Router /auth/login/{provider}/init [get]
|
||||
func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
@@ -177,6 +178,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
|
||||
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
||||
if err != nil {
|
||||
slog.Debug("failed to create oauth auth code URL",
|
||||
"provider", provider, "error", err)
|
||||
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
@@ -211,7 +214,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||
// @Summary Handle the OAuth callback.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.LoginProviderInfo
|
||||
// @Router /auth/{provider}/callback [get]
|
||||
// @Router /auth/login/{provider}/callback [get]
|
||||
func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
@@ -249,6 +252,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
oauthState := request.Query(r, "state")
|
||||
|
||||
if provider != currentSession.OauthProvider {
|
||||
slog.Debug("invalid oauth provider in callback",
|
||||
"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
@@ -258,6 +263,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if oauthState != currentSession.OauthState {
|
||||
slog.Debug("invalid oauth state in callback",
|
||||
"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
@@ -267,11 +274,13 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
loginCtx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
|
||||
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
|
||||
user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
||||
oauthCode)
|
||||
cancel()
|
||||
if err != nil {
|
||||
slog.Debug("failed to process oauth code",
|
||||
"provider", provider, "state", oauthState, "error", err)
|
||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||
redirectToReturn()
|
||||
} else {
|
||||
|
@@ -172,13 +172,13 @@ func NewDomainPeer(src *Peer) *domain.Peer {
|
||||
|
||||
type MultiPeerRequest struct {
|
||||
Identifiers []string `json:"Identifiers"`
|
||||
Suffix string `json:"Suffix"`
|
||||
Prefix string `json:"Prefix"`
|
||||
}
|
||||
|
||||
func NewDomainPeerCreationRequest(src *MultiPeerRequest) *domain.PeerCreationRequest {
|
||||
return &domain.PeerCreationRequest{
|
||||
UserIdentifiers: src.Identifiers,
|
||||
Suffix: src.Suffix,
|
||||
Prefix: src.Prefix,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -54,7 +54,7 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
|
||||
|
||||
attrs := []string{"dn"}
|
||||
|
||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
|
||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", ldap.EscapeFilter(string(userId)), -1)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
l.cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
||||
@@ -100,7 +100,7 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
||||
|
||||
attrs := internal.LdapSearchAttributes(&l.cfg.FieldMap)
|
||||
|
||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
|
||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", ldap.EscapeFilter(string(userId)), -1)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
l.cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
||||
|
@@ -47,11 +47,18 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
|
||||
}
|
||||
latestVersion := "1.0.9"
|
||||
if lastVersion.Version != latestVersion {
|
||||
return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err)
|
||||
return fmt.Errorf("unsupported old version, update to database version %s first", latestVersion)
|
||||
}
|
||||
|
||||
slog.Info("found valid V1 database", "version", lastVersion.Version)
|
||||
|
||||
// validate target database
|
||||
if err := validateTargetDatabase(db); err != nil {
|
||||
return fmt.Errorf("target database validation failed: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("found valid target database, starting migration...")
|
||||
|
||||
if err := migrateV1Users(oldDb, db); err != nil {
|
||||
return fmt.Errorf("user migration failed: %w", err)
|
||||
}
|
||||
@@ -70,6 +77,36 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTargetDatabase checks if the target database is empty and ready for migration.
|
||||
func validateTargetDatabase(db *gorm.DB) error {
|
||||
var count int64
|
||||
err := db.Model(&domain.User{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check user table: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("target database contains %d users, please use an empty database for migration", count)
|
||||
}
|
||||
|
||||
err = db.Model(&domain.Interface{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check interface table: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("target database contains %d interfaces, please use an empty database for migration", count)
|
||||
}
|
||||
|
||||
err = db.Model(&domain.Peer{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check peer table: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("target database contains %d peers, please use an empty database for migration", count)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||
type User struct {
|
||||
Email string `gorm:"primaryKey"`
|
||||
@@ -123,7 +160,7 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||
LinkedPeerCount: 0,
|
||||
}
|
||||
|
||||
if err := newDb.Save(&newUser).Error; err != nil {
|
||||
if err := newDb.Create(&newUser).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
||||
}
|
||||
|
||||
@@ -217,7 +254,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
|
||||
PeerDefPostDown: "",
|
||||
}
|
||||
|
||||
if err := newDb.Save(&newInterface).Error; err != nil {
|
||||
// Create new interface with associations
|
||||
if err := newDb.Create(&newInterface).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
|
||||
}
|
||||
|
||||
@@ -362,7 +400,7 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
||||
},
|
||||
}
|
||||
|
||||
if err := newDb.Save(&newPeer).Error; err != nil {
|
||||
if err := newDb.Create(&newPeer).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ package wireguard
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
@@ -76,6 +77,8 @@ type Manager struct {
|
||||
db InterfaceAndPeerDatabaseRepo
|
||||
wg InterfaceController
|
||||
quick WgQuickController
|
||||
|
||||
userLockMap *sync.Map
|
||||
}
|
||||
|
||||
func NewWireGuardManager(
|
||||
@@ -86,11 +89,12 @@ func NewWireGuardManager(
|
||||
db InterfaceAndPeerDatabaseRepo,
|
||||
) (*Manager, error) {
|
||||
m := &Manager{
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
wg: wg,
|
||||
db: db,
|
||||
quick: quick,
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
wg: wg,
|
||||
db: db,
|
||||
quick: quick,
|
||||
userLockMap: &sync.Map{},
|
||||
}
|
||||
|
||||
m.connectToMessageBus()
|
||||
@@ -117,6 +121,12 @@ func (m Manager) handleUserCreationEvent(user domain.User) {
|
||||
return
|
||||
}
|
||||
|
||||
_, loaded := m.userLockMap.LoadOrStore(user.Identifier, "create")
|
||||
if loaded {
|
||||
return // another goroutine is already handling this user
|
||||
}
|
||||
defer m.userLockMap.Delete(user.Identifier)
|
||||
|
||||
slog.Debug("handling new user event", "user", user.Identifier)
|
||||
|
||||
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
|
||||
@@ -132,6 +142,12 @@ func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) {
|
||||
return
|
||||
}
|
||||
|
||||
_, loaded := m.userLockMap.LoadOrStore(userId, "login")
|
||||
if loaded {
|
||||
return // another goroutine is already handling this user
|
||||
}
|
||||
defer m.userLockMap.Delete(userId)
|
||||
|
||||
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
||||
if err != nil {
|
||||
slog.Error("failed to retrieve existing peers prior to default peer creation",
|
||||
|
@@ -217,6 +217,15 @@ func (m Manager) RestoreInterfaceState(
|
||||
if err != nil && !iface.IsDisabled() {
|
||||
slog.Debug("creating missing interface", "interface", iface.Identifier)
|
||||
|
||||
// temporarily disable interface in database so that the current state is reflected correctly
|
||||
_ = m.db.SaveInterface(ctx, iface.Identifier,
|
||||
func(in *domain.Interface) (*domain.Interface, error) {
|
||||
now := time.Now()
|
||||
in.Disabled = &now // set
|
||||
in.DisabledReason = domain.DisabledReasonInterfaceMissing
|
||||
return in, nil
|
||||
})
|
||||
|
||||
// try to create a new interface
|
||||
_, err = m.saveInterface(ctx, &iface)
|
||||
if err != nil {
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
@@ -23,12 +24,24 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
|
||||
return fmt.Errorf("failed to fetch all interfaces: %w", err)
|
||||
}
|
||||
|
||||
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve existing peers prior to default peer creation: %w", err)
|
||||
}
|
||||
|
||||
var newPeers []domain.Peer
|
||||
for _, iface := range existingInterfaces {
|
||||
if iface.Type != domain.InterfaceTypeServer {
|
||||
continue // only create default peers for server interfaces
|
||||
}
|
||||
|
||||
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
|
||||
return peer.InterfaceIdentifier == iface.Identifier
|
||||
})
|
||||
if peerAlreadyCreated {
|
||||
continue // skip creation if a peer already exists for this interface
|
||||
}
|
||||
|
||||
peer, err := m.PreparePeer(ctx, iface.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
|
||||
@@ -220,7 +233,7 @@ func (m Manager) CreateMultiplePeers(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var newPeers []*domain.Peer
|
||||
createdPeers := make([]domain.Peer, 0, len(r.UserIdentifiers))
|
||||
|
||||
for _, id := range r.UserIdentifiers {
|
||||
freshPeer, err := m.PreparePeer(ctx, interfaceId)
|
||||
@@ -229,27 +242,22 @@ func (m Manager) CreateMultiplePeers(
|
||||
}
|
||||
|
||||
freshPeer.UserIdentifier = domain.UserIdentifier(id) // use id as user identifier. peers are allowed to have invalid user identifiers
|
||||
if r.Suffix != "" {
|
||||
freshPeer.DisplayName += " " + r.Suffix
|
||||
if r.Prefix != "" {
|
||||
freshPeer.DisplayName = r.Prefix + " " + freshPeer.DisplayName
|
||||
}
|
||||
|
||||
if err := m.validatePeerCreation(ctx, nil, freshPeer); err != nil {
|
||||
return nil, fmt.Errorf("creation not allowed: %w", err)
|
||||
}
|
||||
|
||||
newPeers = append(newPeers, freshPeer)
|
||||
}
|
||||
// Save immediately to reserve the assigned IPs so the next prepared peer gets the next free IPs
|
||||
if err := m.savePeers(ctx, freshPeer); err != nil {
|
||||
return nil, fmt.Errorf("failed to create new peer %s: %w", freshPeer.Identifier, err)
|
||||
}
|
||||
|
||||
err := m.savePeers(ctx, newPeers...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new peers: %w", err)
|
||||
}
|
||||
createdPeers = append(createdPeers, *freshPeer)
|
||||
|
||||
createdPeers := make([]domain.Peer, len(newPeers))
|
||||
for i := range newPeers {
|
||||
createdPeers[i] = *newPeers[i]
|
||||
|
||||
m.bus.Publish(app.TopicPeerCreated, *newPeers[i])
|
||||
m.bus.Publish(app.TopicPeerCreated, *freshPeer)
|
||||
}
|
||||
|
||||
return createdPeers, nil
|
||||
|
@@ -190,6 +190,8 @@ func GetConfig() (*Config, error) {
|
||||
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
|
||||
}
|
||||
|
||||
cfg.Web.Sanitize()
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// WebConfig contains the configuration for the web server.
|
||||
type WebConfig struct {
|
||||
// RequestLogging enables logging of all HTTP requests.
|
||||
@@ -26,3 +28,7 @@ type WebConfig struct {
|
||||
// KeyFile is the path to the TLS certificate key file.
|
||||
KeyFile string `yaml:"key_file"`
|
||||
}
|
||||
|
||||
func (c *WebConfig) Sanitize() {
|
||||
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
|
||||
}
|
||||
|
@@ -136,6 +136,7 @@ func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
|
||||
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
||||
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
||||
p.PresharedKey = userPeer.PresharedKey
|
||||
p.Identifier = userPeer.Identifier
|
||||
}
|
||||
p.Interface.Mtu = userPeer.Interface.Mtu
|
||||
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
||||
@@ -268,5 +269,5 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
||||
|
||||
type PeerCreationRequest struct {
|
||||
UserIdentifiers []string
|
||||
Suffix string
|
||||
Prefix string
|
||||
}
|
||||
|
Reference in New Issue
Block a user