Merge a033576028 into 0a33132d26
commit
7973ebcf40
196
src/dashboard.py
196
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))
|
||||
|
|
@ -306,30 +332,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():
|
||||
|
|
@ -338,6 +365,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()
|
||||
|
|
|
|||
|
|
@ -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)}"
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,484 @@
|
|||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "dashboardAdminUsers",
|
||||
components: {LocaleText},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {store};
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
admins: [],
|
||||
loading: true,
|
||||
showAddModal: false,
|
||||
showEditModal: false,
|
||||
showResetPasswordModal: false,
|
||||
showChangePasswordModal: false,
|
||||
// Add admin form
|
||||
newAdmin: {
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
email: ''
|
||||
},
|
||||
// Edit admin form
|
||||
editAdmin: {
|
||||
id: null,
|
||||
username: '',
|
||||
email: ''
|
||||
},
|
||||
// Reset password form
|
||||
resetPassword: {
|
||||
id: null,
|
||||
username: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
// Change own password form
|
||||
changePassword: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
currentAdmin: null,
|
||||
saving: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAdmins();
|
||||
this.loadCurrentAdmin();
|
||||
},
|
||||
methods: {
|
||||
async loadAdmins() {
|
||||
this.loading = true;
|
||||
await fetchGet("/api/admins", {}, (res) => {
|
||||
if(res.status){
|
||||
this.admins = res.data;
|
||||
} else {
|
||||
this.store.newMessage("Server", res.message, "danger");
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
async loadCurrentAdmin() {
|
||||
await fetchGet("/api/admins/current", {}, (res) => {
|
||||
if(res.status){
|
||||
this.currentAdmin = res.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
// Add Admin
|
||||
openAddModal() {
|
||||
this.newAdmin = { username: '', password: '', confirmPassword: '', email: '' };
|
||||
this.showAddModal = true;
|
||||
},
|
||||
async addAdmin() {
|
||||
if (!this.newAdmin.username || !this.newAdmin.password) {
|
||||
this.store.newMessage("Error", "Username and password are required", "danger");
|
||||
return;
|
||||
}
|
||||
if (this.newAdmin.password !== this.newAdmin.confirmPassword) {
|
||||
this.store.newMessage("Error", "Passwords do not match", "danger");
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
await fetchPost("/api/admins/add", {
|
||||
username: this.newAdmin.username,
|
||||
password: this.newAdmin.password,
|
||||
email: this.newAdmin.email
|
||||
}, (res) => {
|
||||
if(res.status){
|
||||
this.store.newMessage("Server", "Admin created successfully", "success");
|
||||
this.showAddModal = false;
|
||||
this.loadAdmins();
|
||||
} else {
|
||||
this.store.newMessage("Server", res.message, "danger");
|
||||
}
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
// Edit Admin
|
||||
openEditModal(admin) {
|
||||
this.editAdmin = {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email || ''
|
||||
};
|
||||
this.showEditModal = true;
|
||||
},
|
||||
async updateAdmin() {
|
||||
if (!this.editAdmin.username) {
|
||||
this.store.newMessage("Error", "Username is required", "danger");
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
await fetchPost("/api/admins/update", {
|
||||
id: this.editAdmin.id,
|
||||
username: this.editAdmin.username,
|
||||
email: this.editAdmin.email
|
||||
}, (res) => {
|
||||
if(res.status){
|
||||
this.store.newMessage("Server", "Admin updated successfully", "success");
|
||||
this.showEditModal = false;
|
||||
this.loadAdmins();
|
||||
} else {
|
||||
this.store.newMessage("Server", res.message, "danger");
|
||||
}
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
// Reset Password (for other admins)
|
||||
openResetPasswordModal(admin) {
|
||||
this.resetPassword = {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
this.showResetPasswordModal = true;
|
||||
},
|
||||
async resetAdminPassword() {
|
||||
if (!this.resetPassword.newPassword) {
|
||||
this.store.newMessage("Error", "New password is required", "danger");
|
||||
return;
|
||||
}
|
||||
if (this.resetPassword.newPassword !== this.resetPassword.confirmPassword) {
|
||||
this.store.newMessage("Error", "Passwords do not match", "danger");
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
await fetchPost("/api/admins/resetPassword", {
|
||||
id: this.resetPassword.id,
|
||||
newPassword: this.resetPassword.newPassword
|
||||
}, (res) => {
|
||||
if(res.status){
|
||||
this.store.newMessage("Server", "Password reset successfully", "success");
|
||||
this.showResetPasswordModal = false;
|
||||
} else {
|
||||
this.store.newMessage("Server", res.message, "danger");
|
||||
}
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
// Change Own Password
|
||||
openChangePasswordModal() {
|
||||
this.changePassword = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
this.showChangePasswordModal = true;
|
||||
},
|
||||
async changeOwnPassword() {
|
||||
if (!this.changePassword.currentPassword || !this.changePassword.newPassword) {
|
||||
this.store.newMessage("Error", "All fields are required", "danger");
|
||||
return;
|
||||
}
|
||||
if (this.changePassword.newPassword !== this.changePassword.confirmPassword) {
|
||||
this.store.newMessage("Error", "New passwords do not match", "danger");
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
await fetchPost("/api/admins/changePassword", {
|
||||
currentPassword: this.changePassword.currentPassword,
|
||||
newPassword: this.changePassword.newPassword
|
||||
}, (res) => {
|
||||
if(res.status){
|
||||
this.store.newMessage("Server", "Password changed successfully", "success");
|
||||
this.showChangePasswordModal = false;
|
||||
} else {
|
||||
this.store.newMessage("Server", res.message, "danger");
|
||||
}
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
// Delete Admin
|
||||
async deleteAdmin(admin) {
|
||||
if (!confirm(`Are you sure you want to delete admin "${admin.username}"?`)) {
|
||||
return;
|
||||
}
|
||||
await fetchPost("/api/admins/delete", { id: admin.id }, (res) => {
|
||||
if(res.status){
|
||||
this.store.newMessage("Server", res.message, "success");
|
||||
this.loadAdmins();
|
||||
} else {
|
||||
this.store.newMessage("Server", res.message, "danger");
|
||||
}
|
||||
});
|
||||
},
|
||||
isCurrentAdmin(admin) {
|
||||
return this.currentAdmin && this.currentAdmin.id === admin.id;
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return 'Never';
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card rounded-3">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h6 class="my-2">
|
||||
<i class="bi bi-people-fill me-2"></i>
|
||||
<LocaleText t="Admin Users"></LocaleText>
|
||||
</h6>
|
||||
<span class="badge bg-primary ms-2">{{ admins.length }}</span>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<button class="btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle rounded-3 shadow-sm"
|
||||
@click="openAddModal()">
|
||||
<i class="bi bi-person-plus-fill me-2"></i>
|
||||
<LocaleText t="Add Admin"></LocaleText>
|
||||
</button>
|
||||
<button class="btn bg-secondary-subtle text-secondary-emphasis border-1 border-secondary-subtle rounded-3 shadow-sm"
|
||||
@click="openChangePasswordModal()">
|
||||
<i class="bi bi-key-fill me-2"></i>
|
||||
<LocaleText t="Change My Password"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin list -->
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><LocaleText t="Username"></LocaleText></th>
|
||||
<th><LocaleText t="Email"></LocaleText></th>
|
||||
<th><LocaleText t="Created"></LocaleText></th>
|
||||
<th><LocaleText t="Last Login"></LocaleText></th>
|
||||
<th><LocaleText t="TOTP"></LocaleText></th>
|
||||
<th class="text-end"><LocaleText t="Actions"></LocaleText></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="admin in admins" :key="admin.id"
|
||||
:class="{'table-active': isCurrentAdmin(admin)}">
|
||||
<td>
|
||||
<i class="bi bi-person-fill me-1"></i>
|
||||
{{ admin.username }}
|
||||
<span v-if="isCurrentAdmin(admin)" class="badge bg-success ms-1">You</span>
|
||||
</td>
|
||||
<td>{{ admin.email || '-' }}</td>
|
||||
<td><small>{{ formatDate(admin.created_at) }}</small></td>
|
||||
<td><small>{{ formatDate(admin.last_login) }}</small></td>
|
||||
<td>
|
||||
<span v-if="admin.enable_totp" class="badge bg-success">
|
||||
<i class="bi bi-shield-check"></i> Enabled
|
||||
</span>
|
||||
<span v-else class="badge bg-secondary">Disabled</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary"
|
||||
@click="openEditModal(admin)"
|
||||
title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning"
|
||||
@click="openResetPasswordModal(admin)"
|
||||
title="Reset Password">
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger"
|
||||
@click="deleteAdmin(admin)"
|
||||
:disabled="isCurrentAdmin(admin) || admins.length <= 1"
|
||||
title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Admin Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showAddModal" class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-person-plus-fill me-2"></i>Add New Admin
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @click="showAddModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" v-model="newAdmin.username"
|
||||
placeholder="Enter username" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" v-model="newAdmin.email"
|
||||
placeholder="Enter email (optional)" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="newAdmin.password"
|
||||
placeholder="Enter password" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="newAdmin.confirmPassword"
|
||||
placeholder="Confirm password" :disabled="saving">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="showAddModal = false" :disabled="saving">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="addAdmin()" :disabled="saving">
|
||||
<span v-if="saving" class="spinner-border spinner-border-sm me-1"></span>
|
||||
Add Admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Edit Admin Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showEditModal" class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-pencil me-2"></i>Edit Admin
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @click="showEditModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" v-model="editAdmin.username"
|
||||
placeholder="Enter username" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" v-model="editAdmin.email"
|
||||
placeholder="Enter email (optional)" :disabled="saving">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="showEditModal = false" :disabled="saving">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="updateAdmin()" :disabled="saving">
|
||||
<span v-if="saving" class="spinner-border spinner-border-sm me-1"></span>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Reset Password Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showResetPasswordModal" class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-key me-2"></i>Reset Password for {{ resetPassword.username }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @click="showResetPasswordModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">New Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="resetPassword.newPassword"
|
||||
placeholder="Enter new password" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="resetPassword.confirmPassword"
|
||||
placeholder="Confirm new password" :disabled="saving">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="showResetPasswordModal = false" :disabled="saving">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" @click="resetAdminPassword()" :disabled="saving">
|
||||
<span v-if="saving" class="spinner-border spinner-border-sm me-1"></span>
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Change Own Password Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showChangePasswordModal" class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-key-fill me-2"></i>Change Your Password
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @click="showChangePasswordModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Current Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="changePassword.currentPassword"
|
||||
placeholder="Enter current password" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">New Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="changePassword.newPassword"
|
||||
placeholder="Enter new password" :disabled="saving">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Confirm New Password <span class="text-danger">*</span></label>
|
||||
<input type="password" class="form-control" v-model="changePassword.confirmPassword"
|
||||
placeholder="Confirm new password" :disabled="saving">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="showChangePasswordModal = false" :disabled="saving">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="changeOwnPassword()" :disabled="saving">
|
||||
<span v-if="saving" class="spinner-border spinner-border-sm me-1"></span>
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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()
|
|||
<DashboardIPPortInput></DashboardIPPortInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3">
|
||||
<div class="card-header">
|
||||
<h6 class="my-2">
|
||||
<i class="bi bi-people-fill me-2"></i>
|
||||
<LocaleText t="Account Settings"></LocaleText>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
<div>
|
||||
<AccountSettingsInputUsername targetData="username"
|
||||
title="Username"
|
||||
></AccountSettingsInputUsername>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<AccountSettingsInputPassword
|
||||
targetData="password">
|
||||
</AccountSettingsInputPassword>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<h6 >
|
||||
<LocaleText t="Multi-Factor Authentication (MFA)"></LocaleText>
|
||||
</h6>
|
||||
<AccountSettingsMFA v-if="!dashboardConfigurationStore.getActiveCrossServer()"></AccountSettingsMFA>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Users Management (Multiple Admins) -->
|
||||
<DashboardAdminUsers></DashboardAdminUsers>
|
||||
|
||||
<DashboardAPIKeys></DashboardAPIKeys>
|
||||
<DashboardEmailSettings></DashboardEmailSettings>
|
||||
|
|
|
|||
|
|
@ -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": "Запази промените"
|
||||
}
|
||||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
@ -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": "Сохранить изменения"
|
||||
}
|
||||
Loading…
Reference in New Issue