From 5dd8b3ee36960720b14cfca7ecf55f02450e2cbb Mon Sep 17 00:00:00 2001 From: MacRimi Date: Fri, 7 Nov 2025 20:36:46 +0100 Subject: [PATCH] Update AppImage --- AppImage/components/login.tsx | 185 ++++++++++++---- AppImage/components/settings.tsx | 124 +++++++++++ AppImage/components/two-factor-setup.tsx | 266 +++++++++++++++++++++++ AppImage/scripts/auth_manager.py | 258 ++++++++++++++++++++-- AppImage/scripts/flask_auth_routes.py | 83 ++++++- 5 files changed, 857 insertions(+), 59 deletions(-) create mode 100644 AppImage/components/two-factor-setup.tsx 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 && ( +
+
+ +

2FA está activado

+
+ + {!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 ( + + + + + + Configurar Autenticación de Dos Factores + + Añade una capa extra de seguridad a tu cuenta + + + {error && ( +
+ +

{error}

+
+ )} + + {step === 1 && ( +
+
+

+ La autenticación de dos factores (2FA) añade una capa extra de seguridad requiriendo un código de tu + aplicación de autenticación además de tu contraseña. +

+
+ +
+

Necesitarás:

+
    +
  • Una aplicación de autenticación (Google Authenticator, Authy, etc.)
  • +
  • Escanear un código QR o introducir una clave manualmente
  • +
  • Guardar códigos de respaldo de forma segura
  • +
+
+ + +
+ )} + + {step === 2 && ( +
+
+

1. Escanea el código QR

+

+ Abre tu aplicación de autenticación y escanea este código QR +

+ {qrCode && ( +
+ QR Code +
+ )} +
+ +
+

O introduce la clave manualmente:

+
+ + +
+
+ +
+

2. Introduce el código de verificación

+

+ Introduce el código de 6 dígitos que aparece en tu aplicación +

+ setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 6))} + className="text-center text-lg tracking-widest font-mono" + maxLength={6} + disabled={loading} + /> +
+ +
+ + +
+
+ )} + + {step === 3 && ( +
+
+ +
+

2FA Activado Correctamente

+

+ Tu cuenta ahora está protegida con autenticación de dos factores +

+
+
+ +
+

Importante: Guarda tus códigos de respaldo

+

+ Estos códigos te permitirán acceder a tu cuenta si pierdes acceso a tu aplicación de autenticación. + Guárdalos en un lugar seguro. +

+ +
+
+ Códigos de Respaldo + +
+
+ {backupCodes.map((code, index) => ( +
+ {code} +
+ ))} +
+
+
+ + +
+ )} +
+
+ ) +} 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