From a88f9335fada1f8aec60b5fc6442743b167aa551 Mon Sep 17 00:00:00 2001 From: V-Bantserov Date: Mon, 2 Feb 2026 22:45:24 +0200 Subject: [PATCH 1/2] feat: Add Multi-Admin and Per-Interface Peer Defaults Multi-Admin Feature: - New DashboardAdmins.py module for managing multiple admin users - API endpoints for admin CRUD operations - Frontend UI in Settings for admin management - Auto-migration from config file to database Per-Interface Peer Defaults: - Override Peer Settings now apply when creating new peers - Frontend loads per-interface defaults in Add Peer modal - Fallback to global defaults if override not set Both features are backward compatible with WGDashboard v4.3.1 --- src/dashboard.py | 196 ++++++- src/modules/DashboardAdmins.py | 351 +++++++++++++ .../configurationComponents/peerAddModal.vue | 31 +- .../configurationComponents/peerCreate.vue | 38 +- .../settingsComponent/dashboardAdminUsers.vue | 484 ++++++++++++++++++ .../settingsComponent/wgdashboardSettings.vue | 32 +- 6 files changed, 1072 insertions(+), 60 deletions(-) create mode 100644 src/modules/DashboardAdmins.py create mode 100644 src/static/app/src/components/settingsComponent/dashboardAdminUsers.vue diff --git a/src/dashboard.py b/src/dashboard.py index ec57ef9d..76c21552 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -40,6 +40,7 @@ from modules.DashboardClients import DashboardClients from modules.DashboardPlugins import DashboardPlugins from modules.DashboardWebHooks import DashboardWebHooks from modules.NewConfigurationTemplates import NewConfigurationTemplates +from modules.DashboardAdmins import DashboardAdmins class CustomJsonEncoder(DefaultJSONProvider): def __init__(self, app): @@ -201,6 +202,31 @@ with app.app_context(): DashboardPlugins: DashboardPlugins = DashboardPlugins(app, WireguardConfigurations) DashboardWebHooks: DashboardWebHooks = DashboardWebHooks(DashboardConfig) NewConfigurationTemplates: NewConfigurationTemplates = NewConfigurationTemplates() + + # Initialize DashboardAdmins + DashboardAdmins: DashboardAdmins = DashboardAdmins( + DashboardConfig.engine, + DashboardConfig.GetConfig('Database', 'type')[1] + ) + + # Migrate admin from config file to database (first-time only) + if DashboardAdmins.getAdminCount() == 0: + _, configUsername = DashboardConfig.GetConfig("Account", "username") + _, configPassword = DashboardConfig.GetConfig("Account", "password") + _, enableTotp = DashboardConfig.GetConfig("Account", "enable_totp") + _, totpKey = DashboardConfig.GetConfig("Account", "totp_key") + + success, msg = DashboardAdmins.migrateFromConfig( + username=configUsername, + passwordHash=configPassword, + enableTotp=enableTotp, + totpKey=totpKey + ) + if success: + print(f"[WGDashboard] Admin migrated to database: {configUsername}") + else: + print(f"[WGDashboard] Admin migration warning: {msg}") + InitWireguardConfigurationsList(startup=True) DashboardClients: DashboardClients = DashboardClients(WireguardConfigurations) app.register_blueprint(createClientBlueprint(WireguardConfigurations, DashboardConfig, DashboardClients)) @@ -297,30 +323,31 @@ def API_AuthenticateLogin(): resp.set_cookie("authToken", authToken) session.permanent = True return resp - valid = bcrypt.checkpw(data['password'].encode("utf-8"), - DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8")) - totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1] - totpValid = False - if totpEnabled: - totpValid = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp'] - - if (valid - and data['username'] == DashboardConfig.GetConfig("Account", "username")[1] - and ((totpEnabled and totpValid) or not totpEnabled) - ): - authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest() - session['role'] = 'admin' - session['username'] = authToken - resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1]) - resp.set_cookie("authToken", authToken) - session.permanent = True - DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login success: {data['username']}") - return resp - DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed: {data['username']}") - if totpEnabled: - return ResponseObject(False, "Sorry, your username, password or OTP is incorrect.") - else: + + # Authenticate using DashboardAdmins + success, admin, msg = DashboardAdmins.authenticate(data['username'], data['password']) + + if not success: + DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed: {data['username']}") return ResponseObject(False, "Sorry, your username or password is incorrect.") + + # Check TOTP if enabled + if admin['enable_totp'] and admin['totp_verified']: + totpValid = pyotp.TOTP(admin['totp_key']).now() == data.get('totp', '') + if not totpValid: + DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed (TOTP): {data['username']}") + return ResponseObject(False, "Sorry, your username, password or OTP is incorrect.") + + authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest() + session['role'] = 'admin' + session['username'] = authToken + session['admin_id'] = admin['id'] + session['admin_username'] = admin['username'] + resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1]) + resp.set_cookie("authToken", authToken) + session.permanent = True + DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login success: {data['username']}") + return resp @app.get(f'{APP_PREFIX}/api/signout') def API_SignOut(): @@ -329,6 +356,129 @@ def API_SignOut(): session.clear() return resp +# ===================================================== +# Admin Management API Endpoints +# ===================================================== + +@app.get(f'{APP_PREFIX}/api/admins') +def API_GetAdmins(): + """Get all admin users""" + admins = DashboardAdmins.getAllAdmins() + return ResponseObject(True, data=[admin.toDict() for admin in admins]) + +@app.post(f'{APP_PREFIX}/api/admins/add') +def API_AddAdmin(): + """Add a new admin user""" + data = request.get_json() + username = data.get('username', '').strip() + password = data.get('password', '') + email = data.get('email', '').strip() + + if not username or not password: + return ResponseObject(False, "Username and password are required") + + success, msg = DashboardAdmins.addAdmin(username, password, email) + if success: + DashboardLogger.log(str(request.url), str(request.remote_addr), + Message=f"Admin created: {username} by {session.get('admin_username', 'unknown')}") + return ResponseObject(success, msg) + +@app.post(f'{APP_PREFIX}/api/admins/update') +def API_UpdateAdmin(): + """Update admin details (username, email)""" + data = request.get_json() + adminId = data.get('id') + username = data.get('username') + email = data.get('email') + + if not adminId: + return ResponseObject(False, "Admin ID is required") + + success, msg = DashboardAdmins.updateAdmin(adminId, username, email) + if success: + DashboardLogger.log(str(request.url), str(request.remote_addr), + Message=f"Admin updated: ID {adminId} by {session.get('admin_username', 'unknown')}") + return ResponseObject(success, msg) + +@app.post(f'{APP_PREFIX}/api/admins/changePassword') +def API_ChangeAdminPassword(): + """Change password for current logged-in admin""" + data = request.get_json() + currentPassword = data.get('currentPassword', '') + newPassword = data.get('newPassword', '') + + adminId = session.get('admin_id') + if not adminId: + return ResponseObject(False, "Not authenticated") + + if not currentPassword or not newPassword: + return ResponseObject(False, "Current password and new password are required") + + success, msg = DashboardAdmins.changePassword(adminId, currentPassword, newPassword) + if success: + DashboardLogger.log(str(request.url), str(request.remote_addr), + Message=f"Password changed: {session.get('admin_username', 'unknown')}") + return ResponseObject(success, msg) + +@app.post(f'{APP_PREFIX}/api/admins/resetPassword') +def API_ResetAdminPassword(): + """Reset password for any admin (admin action)""" + data = request.get_json() + adminId = data.get('id') + newPassword = data.get('newPassword', '') + + if not adminId or not newPassword: + return ResponseObject(False, "Admin ID and new password are required") + + success, msg = DashboardAdmins.resetPassword(adminId, newPassword) + if success: + admin = DashboardAdmins.getAdminById(adminId) + DashboardLogger.log(str(request.url), str(request.remote_addr), + Message=f"Password reset for: {admin['username'] if admin else adminId} by {session.get('admin_username', 'unknown')}") + return ResponseObject(success, msg) + +@app.post(f'{APP_PREFIX}/api/admins/delete') +def API_DeleteAdmin(): + """Delete an admin user""" + data = request.get_json() + adminId = data.get('id') + + if not adminId: + return ResponseObject(False, "Admin ID is required") + + # Prevent self-deletion + if adminId == session.get('admin_id'): + return ResponseObject(False, "Cannot delete your own account") + + admin = DashboardAdmins.getAdminById(adminId) + success, msg = DashboardAdmins.deleteAdmin(adminId) + if success: + DashboardLogger.log(str(request.url), str(request.remote_addr), + Message=f"Admin deleted: {admin['username'] if admin else adminId} by {session.get('admin_username', 'unknown')}") + return ResponseObject(success, msg) + +@app.get(f'{APP_PREFIX}/api/admins/current') +def API_GetCurrentAdmin(): + """Get current logged-in admin info""" + adminId = session.get('admin_id') + if not adminId: + return ResponseObject(False, "Not authenticated") + + admin = DashboardAdmins.getAdminById(adminId) + if admin: + # Don't return sensitive data + return ResponseObject(True, data={ + 'id': admin['id'], + 'username': admin['username'], + 'email': admin['email'], + 'enable_totp': admin['enable_totp'] + }) + return ResponseObject(False, "Admin not found") + +# ===================================================== +# End Admin Management API Endpoints +# ===================================================== + @app.get(f'{APP_PREFIX}/api/getWireguardConfigurations') def API_getWireguardConfigurations(): InitWireguardConfigurationsList() diff --git a/src/modules/DashboardAdmins.py b/src/modules/DashboardAdmins.py new file mode 100644 index 00000000..7c8265e5 --- /dev/null +++ b/src/modules/DashboardAdmins.py @@ -0,0 +1,351 @@ +""" +Dashboard Admins - Multiple admin users management +""" +import bcrypt +import sqlalchemy as db +from datetime import datetime +from typing import Optional, List +from dataclasses import dataclass, asdict + + +@dataclass +class AdminUser: + """Represents an admin user""" + id: int + username: str + email: str + created_at: str + last_login: Optional[str] + enable_totp: bool = False + totp_verified: bool = False + + def toDict(self) -> dict: + return asdict(self) + + +class DashboardAdmins: + """Manages multiple admin users in the database""" + + def __init__(self, engine, dbType: str = 'sqlite'): + self.engine = engine + self.dbType = dbType + self.dbMetadata = db.MetaData() + self._createTable() + + def _createTable(self): + """Create the DashboardAdmins table if it doesn't exist""" + self.adminsTable = db.Table( + 'DashboardAdmins', + self.dbMetadata, + db.Column("id", db.Integer, primary_key=True, autoincrement=True), + db.Column("username", db.String(255), nullable=False, unique=True), + db.Column("password", db.String(255), nullable=False), + db.Column("email", db.String(255), nullable=True, default=""), + db.Column("enable_totp", db.Boolean, default=False), + db.Column("totp_verified", db.Boolean, default=False), + db.Column("totp_key", db.String(255), nullable=True), + db.Column("created_at", + (db.DATETIME if self.dbType == 'sqlite' else db.TIMESTAMP), + server_default=db.func.now()), + db.Column("last_login", + (db.DATETIME if self.dbType == 'sqlite' else db.TIMESTAMP), + nullable=True) + ) + self.dbMetadata.create_all(self.engine) + + def _hashPassword(self, plainPassword: str) -> str: + """Hash a password using bcrypt""" + return bcrypt.hashpw(plainPassword.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + def _checkPassword(self, plainPassword: str, hashedPassword: str) -> bool: + """Verify a password against its hash""" + try: + return bcrypt.checkpw(plainPassword.encode('utf-8'), hashedPassword.encode('utf-8')) + except Exception: + return False + + def getAdminCount(self) -> int: + """Get total number of admins""" + with self.engine.connect() as conn: + result = conn.execute( + db.select(db.func.count()).select_from(self.adminsTable) + ).scalar() + return result or 0 + + def getAllAdmins(self) -> List[AdminUser]: + """Get all admin users (without passwords)""" + admins = [] + with self.engine.connect() as conn: + result = conn.execute( + self.adminsTable.select().order_by(self.adminsTable.c.id) + ).fetchall() + + for row in result: + admins.append(AdminUser( + id=row.id, + username=row.username, + email=row.email or "", + created_at=row.created_at.strftime("%Y-%m-%d %H:%M:%S") if row.created_at else "", + last_login=row.last_login.strftime("%Y-%m-%d %H:%M:%S") if row.last_login else None, + enable_totp=row.enable_totp or False, + totp_verified=row.totp_verified or False + )) + return admins + + def getAdminByUsername(self, username: str) -> Optional[dict]: + """Get admin by username (includes password hash for auth)""" + with self.engine.connect() as conn: + result = conn.execute( + self.adminsTable.select().where( + self.adminsTable.c.username == username + ) + ).fetchone() + + if result: + return { + 'id': result.id, + 'username': result.username, + 'password': result.password, + 'email': result.email or "", + 'enable_totp': result.enable_totp or False, + 'totp_verified': result.totp_verified or False, + 'totp_key': result.totp_key, + 'created_at': result.created_at, + 'last_login': result.last_login + } + return None + + def getAdminById(self, adminId: int) -> Optional[dict]: + """Get admin by ID""" + with self.engine.connect() as conn: + result = conn.execute( + self.adminsTable.select().where( + self.adminsTable.c.id == adminId + ) + ).fetchone() + + if result: + return { + 'id': result.id, + 'username': result.username, + 'email': result.email or "", + 'enable_totp': result.enable_totp or False, + 'totp_verified': result.totp_verified or False, + 'totp_key': result.totp_key, + 'created_at': result.created_at, + 'last_login': result.last_login + } + return None + + def authenticate(self, username: str, password: str) -> tuple[bool, Optional[dict], str]: + """ + Authenticate an admin user + Returns: (success, admin_data, message) + """ + admin = self.getAdminByUsername(username) + + if not admin: + return False, None, "Invalid username or password" + + if not self._checkPassword(password, admin['password']): + return False, None, "Invalid username or password" + + # Update last login + self.updateLastLogin(admin['id']) + + return True, admin, "Login successful" + + def addAdmin(self, username: str, password: str, email: str = "") -> tuple[bool, str]: + """Add a new admin user""" + # Check if username already exists + if self.getAdminByUsername(username): + return False, "Username already exists" + + # Validate username + if len(username) < 3: + return False, "Username must be at least 3 characters" + + # Validate password + if len(password) < 4: + return False, "Password must be at least 4 characters" + + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.insert().values( + username=username, + password=self._hashPassword(password), + email=email, + enable_totp=False, + totp_verified=False, + created_at=datetime.now() + ) + ) + return True, "Admin created successfully" + except Exception as e: + return False, f"Error creating admin: {str(e)}" + + def updateAdmin(self, adminId: int, username: str = None, email: str = None) -> tuple[bool, str]: + """Update admin details (not password)""" + admin = self.getAdminById(adminId) + if not admin: + return False, "Admin not found" + + updates = {} + if username is not None and username != admin['username']: + # Check if new username is taken + existing = self.getAdminByUsername(username) + if existing and existing['id'] != adminId: + return False, "Username already taken" + updates['username'] = username + + if email is not None: + updates['email'] = email + + if not updates: + return True, "No changes to apply" + + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.update() + .where(self.adminsTable.c.id == adminId) + .values(**updates) + ) + return True, "Admin updated successfully" + except Exception as e: + return False, f"Error updating admin: {str(e)}" + + def changePassword(self, adminId: int, currentPassword: str, newPassword: str) -> tuple[bool, str]: + """Change admin password (requires current password)""" + admin = self.getAdminById(adminId) + if not admin: + return False, "Admin not found" + + # Get full admin record with password + with self.engine.connect() as conn: + result = conn.execute( + self.adminsTable.select().where(self.adminsTable.c.id == adminId) + ).fetchone() + + if not result: + return False, "Admin not found" + + if not self._checkPassword(currentPassword, result.password): + return False, "Current password is incorrect" + + if len(newPassword) < 4: + return False, "New password must be at least 4 characters" + + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.update() + .where(self.adminsTable.c.id == adminId) + .values(password=self._hashPassword(newPassword)) + ) + return True, "Password changed successfully" + except Exception as e: + return False, f"Error changing password: {str(e)}" + + def resetPassword(self, adminId: int, newPassword: str) -> tuple[bool, str]: + """Reset admin password (admin action, no current password needed)""" + admin = self.getAdminById(adminId) + if not admin: + return False, "Admin not found" + + if len(newPassword) < 4: + return False, "Password must be at least 4 characters" + + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.update() + .where(self.adminsTable.c.id == adminId) + .values(password=self._hashPassword(newPassword)) + ) + return True, "Password reset successfully" + except Exception as e: + return False, f"Error resetting password: {str(e)}" + + def deleteAdmin(self, adminId: int) -> tuple[bool, str]: + """Delete an admin user""" + # Cannot delete if only one admin remains + if self.getAdminCount() <= 1: + return False, "Cannot delete the last admin" + + admin = self.getAdminById(adminId) + if not admin: + return False, "Admin not found" + + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.delete().where(self.adminsTable.c.id == adminId) + ) + return True, f"Admin '{admin['username']}' deleted successfully" + except Exception as e: + return False, f"Error deleting admin: {str(e)}" + + def updateLastLogin(self, adminId: int): + """Update last login timestamp""" + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.update() + .where(self.adminsTable.c.id == adminId) + .values(last_login=datetime.now()) + ) + except Exception: + pass # Non-critical error + + def updateTOTP(self, adminId: int, enable: bool, totpKey: str = None, verified: bool = False) -> tuple[bool, str]: + """Update TOTP settings for an admin""" + admin = self.getAdminById(adminId) + if not admin: + return False, "Admin not found" + + try: + updates = { + 'enable_totp': enable, + 'totp_verified': verified + } + if totpKey: + updates['totp_key'] = totpKey + + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.update() + .where(self.adminsTable.c.id == adminId) + .values(**updates) + ) + return True, "TOTP updated successfully" + except Exception as e: + return False, f"Error updating TOTP: {str(e)}" + + def migrateFromConfig(self, username: str, passwordHash: str, + enableTotp: bool = False, totpKey: str = None) -> tuple[bool, str]: + """ + Migrate admin from wg-dashboard.ini config to database. + Used for first-time migration. + """ + # Check if any admins exist + if self.getAdminCount() > 0: + return False, "Admins already exist in database" + + try: + with self.engine.begin() as conn: + conn.execute( + self.adminsTable.insert().values( + username=username, + password=passwordHash, # Already hashed + email="", + enable_totp=enableTotp, + totp_verified=enableTotp, # If TOTP was enabled, it was verified + totp_key=totpKey, + created_at=datetime.now() + ) + ) + return True, f"Admin '{username}' migrated successfully" + except Exception as e: + return False, f"Error migrating admin: {str(e)}" diff --git a/src/static/app/src/components/configurationComponents/peerAddModal.vue b/src/static/app/src/components/configurationComponents/peerAddModal.vue index 5212464c..72c198ee 100644 --- a/src/static/app/src/components/configurationComponents/peerAddModal.vue +++ b/src/static/app/src/components/configurationComponents/peerAddModal.vue @@ -18,6 +18,20 @@ import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStor const dashboardStore = DashboardConfigurationStore() const wireguardStore = WireguardConfigurationsStore() +const route = useRoute() + +// Find current configuration to get Override Peer Settings +const currentConfig = wireguardStore.Configurations.find( + x => x.Name === route.params.id +); + +// Get Override Peer Settings (if they exist and have values) +const override = currentConfig?.Info?.OverridePeerSettings || {}; + +// Get global defaults +const globalDefaults = dashboardStore.Configuration.Peers; + +// Initialize peerData with Override settings, falling back to global defaults const peerData = ref({ bulkAdd: false, bulkAddAmount: 0, @@ -25,10 +39,18 @@ const peerData = ref({ allowed_ips: [], private_key: "", public_key: "", - DNS: dashboardStore.Configuration.Peers.peer_global_dns, - endpoint_allowed_ip: dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip, - keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive), - mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu), + // DNS: Override if set, otherwise global default + DNS: override.DNS || globalDefaults.peer_global_dns, + // Endpoint Allowed IPs: Override if set, otherwise global default + endpoint_allowed_ip: override.EndpointAllowedIPs || globalDefaults.peer_endpoint_allowed_ip, + // Persistent Keepalive: Override if set (and > 0), otherwise global default + keepalive: (override.PersistentKeepalive && parseInt(override.PersistentKeepalive) > 0) + ? parseInt(override.PersistentKeepalive) + : parseInt(globalDefaults.peer_keep_alive), + // MTU: Override if set (and > 0), otherwise global default + mtu: (override.MTU && parseInt(override.MTU) > 0) + ? parseInt(override.MTU) + : parseInt(globalDefaults.peer_mtu), preshared_key: "", preshared_key_bulkAdd: false, advanced_security: "off", @@ -37,7 +59,6 @@ const peerData = ref({ const availableIp = ref([]) const saving = ref(false) -const route = useRoute() await fetchGet("/api/getAvailableIPs/" + route.params.id, {}, (res) => { if (res.status){ availableIp.value = res.data; diff --git a/src/static/app/src/components/configurationComponents/peerCreate.vue b/src/static/app/src/components/configurationComponents/peerCreate.vue index 6f6defdf..ad58d2c8 100644 --- a/src/static/app/src/components/configurationComponents/peerCreate.vue +++ b/src/static/app/src/components/configurationComponents/peerCreate.vue @@ -31,10 +31,11 @@ export default { allowed_ips: [], private_key: "", public_key: "", - DNS: this.dashboardStore.Configuration.Peers.peer_global_dns, - endpoint_allowed_ip: this.dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip, - keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive), - mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu), + // Will be set in mounted() with Override Peer Settings priority + DNS: "", + endpoint_allowed_ip: "", + keepalive: 0, + mtu: 0, preshared_key: "", preshared_key_bulkAdd: false, advanced_security: "off", @@ -46,11 +47,40 @@ export default { } }, mounted() { + // Load available IPs fetchGet("/api/getAvailableIPs/" + this.$route.params.id, {}, (res) => { if (res.status){ this.availableIp = res.data; } }) + + // Find current configuration to get Override Peer Settings + const currentConfig = this.store.Configurations.find( + x => x.Name === this.$route.params.id + ); + + // Get Override Peer Settings (if they exist and have values) + const override = currentConfig?.Info?.OverridePeerSettings || {}; + + // Get global defaults + const globalDefaults = this.dashboardStore.Configuration.Peers; + + // Apply Override settings with fallback to global defaults + // DNS: Override if set, otherwise global default + this.data.DNS = override.DNS || globalDefaults.peer_global_dns; + + // Endpoint Allowed IPs: Override if set, otherwise global default + this.data.endpoint_allowed_ip = override.EndpointAllowedIPs || globalDefaults.peer_endpoint_allowed_ip; + + // MTU: Override if set (and > 0), otherwise global default + this.data.mtu = (override.MTU && parseInt(override.MTU) > 0) + ? parseInt(override.MTU) + : parseInt(globalDefaults.peer_mtu); + + // Persistent Keepalive: Override if set (and > 0), otherwise global default + this.data.keepalive = (override.PersistentKeepalive && parseInt(override.PersistentKeepalive) > 0) + ? parseInt(override.PersistentKeepalive) + : parseInt(globalDefaults.peer_keep_alive); }, setup(){ const store = WireguardConfigurationsStore(); diff --git a/src/static/app/src/components/settingsComponent/dashboardAdminUsers.vue b/src/static/app/src/components/settingsComponent/dashboardAdminUsers.vue new file mode 100644 index 00000000..47b1fd61 --- /dev/null +++ b/src/static/app/src/components/settingsComponent/dashboardAdminUsers.vue @@ -0,0 +1,484 @@ + + + + + diff --git a/src/static/app/src/components/settingsComponent/wgdashboardSettings.vue b/src/static/app/src/components/settingsComponent/wgdashboardSettings.vue index bdbe2bb0..f01ccf46 100644 --- a/src/static/app/src/components/settingsComponent/wgdashboardSettings.vue +++ b/src/static/app/src/components/settingsComponent/wgdashboardSettings.vue @@ -11,6 +11,7 @@ import AccountSettingsMFA from "@/components/settingsComponent/accountSettingsMF import AccountSettingsInputUsername from "@/components/settingsComponent/accountSettingsInputUsername.vue"; import DashboardEmailSettings from "@/components/settingsComponent/dashboardEmailSettings.vue"; import DashboardWebHooks from "@/components/settingsComponent/dashboardWebHooks.vue"; +import DashboardAdminUsers from "@/components/settingsComponent/dashboardAdminUsers.vue"; const dashboardConfigurationStore = DashboardConfigurationStore() @@ -47,34 +48,9 @@ const dashboardConfigurationStore = DashboardConfigurationStore() -
-
-
- - -
-
-
-
- -
-
-
- - -
-
-
-
- -
- -
-
-
+ + + From a03357602867de9752f464385bc912deafb6de50 Mon Sep 17 00:00:00 2001 From: V-Bantserov Date: Fri, 6 Feb 2026 08:20:28 +0200 Subject: [PATCH 2/2] Add Multi-Admin locale keys - Added 16 new translation keys for Multi-Admin feature - Added Bulgarian and Russian translations for new keys --- src/static/locales/bg-BG.json | 18 +++++++++++++++++- src/static/locales/locale_template.json | 18 +++++++++++++++++- src/static/locales/ru-RU.json | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/static/locales/bg-BG.json b/src/static/locales/bg-BG.json index c8c30538..9e854610 100644 --- a/src/static/locales/bg-BG.json +++ b/src/static/locales/bg-BG.json @@ -438,5 +438,21 @@ "or": "или", "or click the button below to download the ": "или щракнете бутона по-долу, за да изтеглите ", "then": "тогава", - "to add your server": "за да добавите сървъра си" + "to add your server": "за да добавите сървъра си", + "Admin Users": "Администратори", + "Add Admin": "Добави администратор", + "Change My Password": "Смени паролата ми", + "Created": "Създаден", + "Last Login": "Последен вход", + "TOTP": "TOTP", + "Actions": "Действия", + "You": "Вие", + "Add New Admin": "Добави нов администратор", + "Edit Admin": "Редактирай администратор", + "Change Password": "Смени парола", + "Reset Password": "Нулирай парола", + "Delete Admin": "Изтрий администратор", + "Confirm Password": "Потвърди парола", + "Confirm New Password": "Потвърди новата парола", + "Save Changes": "Запази промените" } \ No newline at end of file diff --git a/src/static/locales/locale_template.json b/src/static/locales/locale_template.json index af259d73..4856137f 100644 --- a/src/static/locales/locale_template.json +++ b/src/static/locales/locale_template.json @@ -438,5 +438,21 @@ "Assign successfully!": "", "Sessions": "", "Data": "", - "Back": "" + "Back": "", + "Admin Users": "", + "Add Admin": "", + "Change My Password": "", + "Created": "", + "Last Login": "", + "TOTP": "", + "Actions": "", + "You": "", + "Add New Admin": "", + "Edit Admin": "", + "Change Password": "", + "Reset Password": "", + "Delete Admin": "", + "Confirm Password": "", + "Confirm New Password": "", + "Save Changes": "" } \ No newline at end of file diff --git a/src/static/locales/ru-RU.json b/src/static/locales/ru-RU.json index cab481ad..3fa50806 100644 --- a/src/static/locales/ru-RU.json +++ b/src/static/locales/ru-RU.json @@ -365,5 +365,21 @@ "or": "или", "or click the button below to download the ": "Так же, вы можете скачать ", "then": "тогда", - "to add your server": "чтобы добавить свой сервер" + "to add your server": "чтобы добавить свой сервер", + "Admin Users": "Администраторы", + "Add Admin": "Добавить администратора", + "Change My Password": "Сменить мой пароль", + "Created": "Создан", + "Last Login": "Последний вход", + "TOTP": "TOTP", + "Actions": "Действия", + "You": "Вы", + "Add New Admin": "Добавить нового администратора", + "Edit Admin": "Редактировать администратора", + "Change Password": "Сменить пароль", + "Reset Password": "Сбросить пароль", + "Delete Admin": "Удалить администратора", + "Confirm Password": "Подтвердить пароль", + "Confirm New Password": "Подтвердить новый пароль", + "Save Changes": "Сохранить изменения" } \ No newline at end of file