diff --git a/AppImage/components/login.tsx b/AppImage/components/login.tsx
index eabcd3c..4c539b3 100644
--- a/AppImage/components/login.tsx
+++ b/AppImage/components/login.tsx
@@ -2,11 +2,12 @@
import type React from "react"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
-import { Lock, User, AlertCircle, Server } from "lucide-react"
+import { Checkbox } from "./ui/checkbox"
+import { Lock, User, AlertCircle, Server, Shield } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
import Image from "next/image"
@@ -17,9 +18,23 @@ interface LoginProps {
export function Login({ onLogin }: LoginProps) {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
+ const [totpCode, setTotpCode] = useState("")
+ const [requiresTotp, setRequiresTotp] = useState(false)
+ const [rememberMe, setRememberMe] = useState(false)
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
+ useEffect(() => {
+ const savedUsername = localStorage.getItem("proxmenux-saved-username")
+ const savedPassword = localStorage.getItem("proxmenux-saved-password")
+
+ if (savedUsername && savedPassword) {
+ setUsername(savedUsername)
+ setPassword(savedPassword)
+ setRememberMe(true)
+ }
+ }, [])
+
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
@@ -29,23 +44,46 @@ export function Login({ onLogin }: LoginProps) {
return
}
+ if (requiresTotp && !totpCode) {
+ setError("Please enter your 2FA code")
+ return
+ }
+
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/login"), {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ username, password }),
+ body: JSON.stringify({
+ username,
+ password,
+ totp_token: totpCode || undefined, // Include 2FA code if provided
+ }),
})
const data = await response.json()
- if (!response.ok) {
- throw new Error(data.error || "Login failed")
+ if (data.requires_totp) {
+ setRequiresTotp(true)
+ setLoading(false)
+ return
+ }
+
+ if (!response.ok) {
+ throw new Error(data.message || "Login failed")
}
- // Save token
localStorage.setItem("proxmenux-auth-token", data.token)
+
+ if (rememberMe) {
+ localStorage.setItem("proxmenux-saved-username", username)
+ localStorage.setItem("proxmenux-saved-password", password)
+ } else {
+ localStorage.removeItem("proxmenux-saved-username")
+ localStorage.removeItem("proxmenux-saved-password")
+ }
+
onLogin()
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed")
@@ -94,46 +132,109 @@ export function Login({ onLogin }: LoginProps) {
)}
-
-
-
-
- setUsername(e.target.value)}
- className="pl-10 text-base"
- disabled={loading}
- autoComplete="username"
- />
-
-
+ {!requiresTotp ? (
+ <>
+
+
+
+
+ setUsername(e.target.value)}
+ className="pl-10 text-base"
+ disabled={loading}
+ autoComplete="username"
+ />
+
+
-
-
-
-
-
setPassword(e.target.value)}
- className="pl-10 text-base"
- disabled={loading}
- autoComplete="current-password"
- />
+
+
+
+
+ setPassword(e.target.value)}
+ className="pl-10 text-base"
+ disabled={loading}
+ autoComplete="current-password"
+ />
+
+
+
+
+ setRememberMe(checked as boolean)}
+ disabled={loading}
+ />
+
+
+ >
+ ) : (
+
+
+
+
+
Autenticación de Dos Factores
+
+ Introduce el código de 6 dígitos de tu aplicación de autenticación
+
+
+
+
+
+
+
setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
+ className="text-center text-lg tracking-widest font-mono text-base"
+ maxLength={6}
+ disabled={loading}
+ autoComplete="one-time-code"
+ autoFocus
+ />
+
+ También puedes usar un código de respaldo (formato: XXXX-XXXX)
+
+
+
+
-
+ )}
diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx
index 36ea073..b7f8e03 100644
--- a/AppImage/components/settings.tsx
+++ b/AppImage/components/settings.tsx
@@ -7,9 +7,11 @@ import { Label } from "./ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
+import { TwoFactorSetup } from "./two-factor-setup"
export function Settings() {
const [authEnabled, setAuthEnabled] = useState(false)
+ const [totpEnabled, setTotpEnabled] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
@@ -26,6 +28,10 @@ export function Settings() {
const [newPassword, setNewPassword] = useState("")
const [confirmNewPassword, setConfirmNewPassword] = useState("")
+ const [show2FASetup, setShow2FASetup] = useState(false)
+ const [show2FADisable, setShow2FADisable] = useState(false)
+ const [disable2FAPassword, setDisable2FAPassword] = useState("")
+
useEffect(() => {
checkAuthStatus()
}, [])
@@ -35,6 +41,7 @@ export function Settings() {
const response = await fetch(getApiUrl("/api/auth/status"))
const data = await response.json()
setAuthEnabled(data.auth_enabled || false)
+ setTotpEnabled(data.totp_enabled || false) // Get 2FA status
} catch (err) {
console.error("Failed to check auth status:", err)
}
@@ -196,6 +203,46 @@ export function Settings() {
}
}
+ const handleDisable2FA = async () => {
+ setError("")
+ setSuccess("")
+
+ if (!disable2FAPassword) {
+ setError("Please enter your password")
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ const token = localStorage.getItem("proxmenux-auth-token")
+ const response = await fetch(getApiUrl("/api/auth/totp/disable"), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({ password: disable2FAPassword }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.message || "Failed to disable 2FA")
+ }
+
+ setSuccess("2FA disabled successfully!")
+ setTotpEnabled(false)
+ setShow2FADisable(false)
+ setDisable2FAPassword("")
+ checkAuthStatus()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to disable 2FA")
+ } finally {
+ setLoading(false)
+ }
+ }
+
const handleLogout = () => {
localStorage.removeItem("proxmenux-auth-token")
localStorage.removeItem("proxmenux-auth-setup-complete")
@@ -418,6 +465,74 @@ export function Settings() {
)}
+ {!totpEnabled && (
+
+ )}
+
+ {totpEnabled && (
+
+
+
+ {!show2FADisable && (
+
+ )}
+
+ {show2FADisable && (
+
+
Desactivar Autenticación de Dos Factores
+
Introduce tu contraseña para confirmar
+
+
+
+
+
+ setDisable2FAPassword(e.target.value)}
+ className="pl-10"
+ disabled={loading}
+ />
+
+
+
+
+
+
+
+
+ )}
+
+ )}
+
@@ -443,6 +558,15 @@ export function Settings() {
+
+ setShow2FASetup(false)}
+ onSuccess={() => {
+ setSuccess("2FA habilitado correctamente!")
+ checkAuthStatus()
+ }}
+ />
)
}
diff --git a/AppImage/components/two-factor-setup.tsx b/AppImage/components/two-factor-setup.tsx
new file mode 100644
index 0000000..e20f1c8
--- /dev/null
+++ b/AppImage/components/two-factor-setup.tsx
@@ -0,0 +1,266 @@
+"use client"
+
+import { useState } from "react"
+import { Button } from "./ui/button"
+import { Input } from "./ui/input"
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
+import { AlertCircle, CheckCircle, Copy, Shield, Check } from "lucide-react"
+import { getApiUrl } from "../lib/api-config"
+import Image from "next/image"
+
+interface TwoFactorSetupProps {
+ open: boolean
+ onClose: () => void
+ onSuccess: () => void
+}
+
+export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps) {
+ const [step, setStep] = useState(1)
+ const [qrCode, setQrCode] = useState("")
+ const [secret, setSecret] = useState("")
+ const [backupCodes, setBackupCodes] = useState([])
+ const [verificationCode, setVerificationCode] = useState("")
+ const [error, setError] = useState("")
+ const [loading, setLoading] = useState(false)
+ const [copiedSecret, setCopiedSecret] = useState(false)
+ const [copiedCodes, setCopiedCodes] = useState(false)
+
+ const handleSetupStart = async () => {
+ setError("")
+ setLoading(true)
+
+ try {
+ const token = localStorage.getItem("proxmenux-auth-token")
+ const response = await fetch(getApiUrl("/api/auth/totp/setup"), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.message || "Failed to setup 2FA")
+ }
+
+ setQrCode(data.qr_code)
+ setSecret(data.secret)
+ setBackupCodes(data.backup_codes)
+ setStep(2)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to setup 2FA")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleVerify = async () => {
+ if (!verificationCode || verificationCode.length !== 6) {
+ setError("Please enter a 6-digit code")
+ return
+ }
+
+ setError("")
+ setLoading(true)
+
+ try {
+ const token = localStorage.getItem("proxmenux-auth-token")
+ const response = await fetch(getApiUrl("/api/auth/totp/enable"), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({ token: verificationCode }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.message || "Invalid verification code")
+ }
+
+ setStep(3)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Verification failed")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const copyToClipboard = (text: string, type: "secret" | "codes") => {
+ navigator.clipboard.writeText(text)
+ if (type === "secret") {
+ setCopiedSecret(true)
+ setTimeout(() => setCopiedSecret(false), 2000)
+ } else {
+ setCopiedCodes(true)
+ setTimeout(() => setCopiedCodes(false), 2000)
+ }
+ }
+
+ const handleClose = () => {
+ setStep(1)
+ setQrCode("")
+ setSecret("")
+ setBackupCodes([])
+ setVerificationCode("")
+ setError("")
+ onClose()
+ }
+
+ const handleFinish = () => {
+ handleClose()
+ onSuccess()
+ }
+
+ return (
+
+ )
+}
diff --git a/AppImage/scripts/auth_manager.py b/AppImage/scripts/auth_manager.py
index 9326737..dd262df 100644
--- a/AppImage/scripts/auth_manager.py
+++ b/AppImage/scripts/auth_manager.py
@@ -5,11 +5,13 @@ Handles all authentication-related operations including:
- Password hashing and verification
- JWT token generation and validation
- Auth status checking
+- Two-Factor Authentication (2FA/TOTP)
"""
import os
import json
import hashlib
+import secrets
from datetime import datetime, timedelta
from pathlib import Path
@@ -20,6 +22,16 @@ except ImportError:
JWT_AVAILABLE = False
print("Warning: PyJWT not available. Authentication features will be limited.")
+try:
+ import pyotp
+ import qrcode
+ import io
+ import base64
+ TOTP_AVAILABLE = True
+except ImportError:
+ TOTP_AVAILABLE = False
+ print("Warning: pyotp/qrcode not available. 2FA features will be disabled.")
+
# Configuration
CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
AUTH_CONFIG_FILE = CONFIG_DIR / "auth.json"
@@ -41,8 +53,11 @@ def load_auth_config():
"enabled": bool,
"username": str,
"password_hash": str,
- "declined": bool, # True if user explicitly declined auth
- "configured": bool # True if auth has been set up (enabled or declined)
+ "declined": bool,
+ "configured": bool,
+ "totp_enabled": bool, # 2FA enabled flag
+ "totp_secret": str, # TOTP secret key
+ "backup_codes": list # List of backup codes
}
"""
if not AUTH_CONFIG_FILE.exists():
@@ -51,7 +66,10 @@ def load_auth_config():
"username": None,
"password_hash": None,
"declined": False,
- "configured": False
+ "configured": False,
+ "totp_enabled": False,
+ "totp_secret": None,
+ "backup_codes": []
}
try:
@@ -60,6 +78,9 @@ def load_auth_config():
# Ensure all required fields exist
config.setdefault("declined", False)
config.setdefault("configured", config.get("enabled", False) or config.get("declined", False))
+ config.setdefault("totp_enabled", False)
+ config.setdefault("totp_secret", None)
+ config.setdefault("backup_codes", [])
return config
except Exception as e:
print(f"Error loading auth config: {e}")
@@ -68,7 +89,10 @@ def load_auth_config():
"username": None,
"password_hash": None,
"declined": False,
- "configured": False
+ "configured": False,
+ "totp_enabled": False,
+ "totp_secret": None,
+ "backup_codes": []
}
@@ -141,16 +165,18 @@ def get_auth_status():
"auth_configured": bool,
"declined": bool,
"username": str or None,
- "authenticated": bool
+ "authenticated": bool,
+ "totp_enabled": bool # 2FA status
}
"""
config = load_auth_config()
return {
"auth_enabled": config.get("enabled", False),
- "auth_configured": config.get("configured", False), # Frontend expects this field name
+ "auth_configured": config.get("configured", False),
"declined": config.get("declined", False),
"username": config.get("username") if config.get("enabled") else None,
- "authenticated": False # Will be set to True by the route handler if token is valid
+ "authenticated": False,
+ "totp_enabled": config.get("totp_enabled", False) # Include 2FA status
}
@@ -170,7 +196,10 @@ def setup_auth(username, password):
"username": username,
"password_hash": hash_password(password),
"declined": False,
- "configured": True
+ "configured": True,
+ "totp_enabled": False,
+ "totp_secret": None,
+ "backup_codes": []
}
if save_auth_config(config):
@@ -190,6 +219,9 @@ def decline_auth():
config["configured"] = True
config["username"] = None
config["password_hash"] = None
+ config["totp_enabled"] = False
+ config["totp_secret"] = None
+ config["backup_codes"] = []
if save_auth_config(config):
return True, "Authentication declined"
@@ -208,6 +240,9 @@ def disable_auth():
config["password_hash"] = None
config["declined"] = False
config["configured"] = False
+ config["totp_enabled"] = False
+ config["totp_secret"] = None
+ config["backup_codes"] = []
if save_auth_config(config):
return True, "Authentication disabled"
@@ -258,24 +293,215 @@ def change_password(old_password, new_password):
return False, "Failed to save new password"
-def authenticate(username, password):
+def generate_totp_secret():
+ """Generate a new TOTP secret key"""
+ if not TOTP_AVAILABLE:
+ return None
+ return pyotp.random_base32()
+
+
+def generate_totp_qr(username, secret):
"""
- Authenticate a user with username and password
- Returns (success: bool, token: str or None, message: str)
+ Generate a QR code for TOTP setup
+ Returns base64 encoded PNG image
+ """
+ if not TOTP_AVAILABLE:
+ return None
+
+ try:
+ # Create TOTP URI
+ totp = pyotp.TOTP(secret)
+ uri = totp.provisioning_uri(
+ name=username,
+ issuer_name="ProxMenux Monitor"
+ )
+
+ # Generate QR code
+ qr = qrcode.QRCode(version=1, box_size=10, border=5)
+ qr.add_data(uri)
+ qr.make(fit=True)
+
+ img = qr.make_image(fill_color="black", back_color="white")
+
+ # Convert to base64
+ buffer = io.BytesIO()
+ img.save(buffer, format='PNG')
+ buffer.seek(0)
+ img_base64 = base64.b64encode(buffer.getvalue()).decode()
+
+ return f"data:image/png;base64,{img_base64}"
+ except Exception as e:
+ print(f"Error generating QR code: {e}")
+ return None
+
+
+def generate_backup_codes(count=8):
+ """Generate backup codes for 2FA recovery"""
+ codes = []
+ for _ in range(count):
+ # Generate 8-character alphanumeric code
+ code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
+ # Format as XXXX-XXXX for readability
+ formatted = f"{code[:4]}-{code[4:]}"
+ codes.append({
+ "code": hashlib.sha256(formatted.encode()).hexdigest(),
+ "used": False
+ })
+ return codes
+
+
+def setup_totp(username):
+ """
+ Set up TOTP for a user
+ Returns (success: bool, secret: str, qr_code: str, backup_codes: list, message: str)
+ """
+ if not TOTP_AVAILABLE:
+ return False, None, None, None, "2FA is not available (pyotp/qrcode not installed)"
+
+ config = load_auth_config()
+
+ if not config.get("enabled"):
+ return False, None, None, None, "Authentication must be enabled first"
+
+ if config.get("username") != username:
+ return False, None, None, None, "Invalid username"
+
+ # Generate new secret and backup codes
+ secret = generate_totp_secret()
+ qr_code = generate_totp_qr(username, secret)
+ backup_codes_plain = []
+ backup_codes_hashed = generate_backup_codes()
+
+ # Generate plain text backup codes for display (only returned once)
+ for i in range(8):
+ code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
+ formatted = f"{code[:4]}-{code[4:]}"
+ backup_codes_plain.append(formatted)
+ backup_codes_hashed[i]["code"] = hashlib.sha256(formatted.encode()).hexdigest()
+
+ # Store secret and hashed backup codes (not enabled yet until verified)
+ config["totp_secret"] = secret
+ config["backup_codes"] = backup_codes_hashed
+
+ if save_auth_config(config):
+ return True, secret, qr_code, backup_codes_plain, "2FA setup initiated"
+ else:
+ return False, None, None, None, "Failed to save 2FA configuration"
+
+
+def verify_totp(username, token, use_backup=False):
+ """
+ Verify a TOTP token or backup code
+ Returns (success: bool, message: str)
+ """
+ if not TOTP_AVAILABLE and not use_backup:
+ return False, "2FA is not available"
+
+ config = load_auth_config()
+
+ if not config.get("totp_enabled"):
+ return False, "2FA is not enabled"
+
+ if config.get("username") != username:
+ return False, "Invalid username"
+
+ # Check backup code
+ if use_backup:
+ token_hash = hashlib.sha256(token.encode()).hexdigest()
+ for backup_code in config.get("backup_codes", []):
+ if backup_code["code"] == token_hash and not backup_code["used"]:
+ backup_code["used"] = True
+ save_auth_config(config)
+ return True, "Backup code accepted"
+ return False, "Invalid or already used backup code"
+
+ # Check TOTP token
+ totp = pyotp.TOTP(config.get("totp_secret"))
+ if totp.verify(token, valid_window=1): # Allow 1 time step tolerance
+ return True, "2FA verification successful"
+ else:
+ return False, "Invalid 2FA code"
+
+
+def enable_totp(username, verification_token):
+ """
+ Enable TOTP after successful verification
+ Returns (success: bool, message: str)
+ """
+ if not TOTP_AVAILABLE:
+ return False, "2FA is not available"
+
+ config = load_auth_config()
+
+ if not config.get("totp_secret"):
+ return False, "2FA has not been set up. Please set up 2FA first."
+
+ if config.get("username") != username:
+ return False, "Invalid username"
+
+ # Verify the token before enabling
+ totp = pyotp.TOTP(config.get("totp_secret"))
+ if not totp.verify(verification_token, valid_window=1):
+ return False, "Invalid verification code. Please try again."
+
+ config["totp_enabled"] = True
+
+ if save_auth_config(config):
+ return True, "2FA enabled successfully"
+ else:
+ return False, "Failed to enable 2FA"
+
+
+def disable_totp(username, password):
+ """
+ Disable TOTP (requires password confirmation)
+ Returns (success: bool, message: str)
+ """
+ config = load_auth_config()
+
+ if config.get("username") != username:
+ return False, "Invalid username"
+
+ if not verify_password(password, config.get("password_hash", "")):
+ return False, "Invalid password"
+
+ config["totp_enabled"] = False
+ config["totp_secret"] = None
+ config["backup_codes"] = []
+
+ if save_auth_config(config):
+ return True, "2FA disabled successfully"
+ else:
+ return False, "Failed to disable 2FA"
+
+
+def authenticate(username, password, totp_token=None):
+ """
+ Authenticate a user with username, password, and optional TOTP
+ Returns (success: bool, token: str or None, requires_totp: bool, message: str)
"""
config = load_auth_config()
if not config.get("enabled"):
- return False, None, "Authentication is not enabled"
+ return False, None, False, "Authentication is not enabled"
if username != config.get("username"):
- return False, None, "Invalid username or password"
+ return False, None, False, "Invalid username or password"
if not verify_password(password, config.get("password_hash", "")):
- return False, None, "Invalid username or password"
+ return False, None, False, "Invalid username or password"
+
+ if config.get("totp_enabled"):
+ if not totp_token:
+ return False, None, True, "2FA code required"
+
+ # Verify TOTP token or backup code
+ success, message = verify_totp(username, totp_token, use_backup=len(totp_token) == 9) # Backup codes are formatted XXXX-XXXX
+ if not success:
+ return False, None, True, message
token = generate_token(username)
if token:
- return True, token, "Authentication successful"
+ return True, token, False, "Authentication successful"
else:
- return False, None, "Failed to generate authentication token"
+ return False, None, False, "Failed to generate authentication token"
diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py
index d8b3691..00f4f5f 100644
--- a/AppImage/scripts/flask_auth_routes.py
+++ b/AppImage/scripts/flask_auth_routes.py
@@ -64,11 +64,14 @@ def auth_login():
data = request.json
username = data.get('username')
password = data.get('password')
+ totp_token = data.get('totp_token') # Optional 2FA token
- success, token, message = auth_manager.authenticate(username, password)
+ success, token, requires_totp, message = auth_manager.authenticate(username, password, totp_token)
if success:
return jsonify({"success": True, "token": token, "message": message})
+ elif requires_totp:
+ return jsonify({"success": False, "requires_totp": True, "message": message}), 200
else:
return jsonify({"success": False, "message": message}), 401
except Exception as e:
@@ -137,3 +140,81 @@ def auth_skip():
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
+
+
+@auth_bp.route('/api/auth/totp/setup', methods=['POST'])
+def totp_setup():
+ """Initialize TOTP setup for a user"""
+ try:
+ token = request.headers.get('Authorization', '').replace('Bearer ', '')
+ username = auth_manager.verify_token(token)
+
+ if not username:
+ return jsonify({"success": False, "message": "Unauthorized"}), 401
+
+ success, secret, qr_code, backup_codes, message = auth_manager.setup_totp(username)
+
+ if success:
+ return jsonify({
+ "success": True,
+ "secret": secret,
+ "qr_code": qr_code,
+ "backup_codes": backup_codes,
+ "message": message
+ })
+ else:
+ return jsonify({"success": False, "message": message}), 400
+ except Exception as e:
+ return jsonify({"success": False, "message": str(e)}), 500
+
+
+@auth_bp.route('/api/auth/totp/enable', methods=['POST'])
+def totp_enable():
+ """Enable TOTP after verification"""
+ try:
+ token = request.headers.get('Authorization', '').replace('Bearer ', '')
+ username = auth_manager.verify_token(token)
+
+ if not username:
+ return jsonify({"success": False, "message": "Unauthorized"}), 401
+
+ data = request.json
+ verification_token = data.get('token')
+
+ if not verification_token:
+ return jsonify({"success": False, "message": "Verification token required"}), 400
+
+ success, message = auth_manager.enable_totp(username, verification_token)
+
+ if success:
+ return jsonify({"success": True, "message": message})
+ else:
+ return jsonify({"success": False, "message": message}), 400
+ except Exception as e:
+ return jsonify({"success": False, "message": str(e)}), 500
+
+
+@auth_bp.route('/api/auth/totp/disable', methods=['POST'])
+def totp_disable():
+ """Disable TOTP (requires password confirmation)"""
+ try:
+ token = request.headers.get('Authorization', '').replace('Bearer ', '')
+ username = auth_manager.verify_token(token)
+
+ if not username:
+ return jsonify({"success": False, "message": "Unauthorized"}), 401
+
+ data = request.json
+ password = data.get('password')
+
+ if not password:
+ return jsonify({"success": False, "message": "Password required"}), 400
+
+ success, message = auth_manager.disable_totp(username, password)
+
+ if success:
+ return jsonify({"success": True, "message": message})
+ else:
+ return jsonify({"success": False, "message": message}), 400
+ except Exception as e:
+ return jsonify({"success": False, "message": str(e)}), 500