diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 60970672..6a400dab 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -21,7 +21,6 @@ import { authLogger, apiLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { UserCrypto } from "../../utils/user-crypto.js"; import { DataCrypto } from "../../utils/data-crypto.js"; -import { ZeroTrustMigration } from "../../utils/zero-trust-migration.js"; // Get auth manager instance const authManager = AuthManager.getInstance(); @@ -1290,79 +1289,16 @@ router.post("/complete-reset", async (req, res) => { } const userId = user[0].id; - // 🔥 CRITICAL FIX: Use recovery DEK to preserve user data - if (user[0].recovery_dek && user[0].backup_encrypted_dek) { - try { - const userCrypto = UserCrypto.getInstance(); + // Update password hash + const saltRounds = parseInt(process.env.SALT || "10", 10); + const password_hash = await bcrypt.hash(newPassword, saltRounds); - // Decrypt DEK using recovery key - const recoveryDEK = Buffer.from(user[0].recovery_dek, 'hex'); - const backupEncryptedDEK = JSON.parse(user[0].backup_encrypted_dek); - const originalDEK = (userCrypto as any).decryptDEK(backupEncryptedDEK, recoveryDEK); + await db + .update(users) + .set({ password_hash }) + .where(eq(users.username, username)); - // Generate new KEK with new password - const newKekSalt = await (userCrypto as any).generateKEKSalt(); - const newKEK = (userCrypto as any).deriveKEK(newPassword, newKekSalt); - const newEncryptedDEK = (userCrypto as any).encryptDEK(originalDEK, newKEK); - - // Update password hash - const saltRounds = parseInt(process.env.SALT || "10", 10); - const password_hash = await bcrypt.hash(newPassword, saltRounds); - - // Update everything atomically - await (userCrypto as any).storeKEKSalt(userId, newKekSalt); - await (userCrypto as any).storeEncryptedDEK(userId, newEncryptedDEK); - await db - .update(users) - .set({ password_hash }) - .where(eq(users.username, username)); - - // Update recovery data for new password - const newRecoveryDEK = crypto.randomBytes(32); - const newBackupEncryptedDEK = (userCrypto as any).encryptDEK(originalDEK, newRecoveryDEK); - await db - .update(users) - .set({ - recovery_dek: newRecoveryDEK.toString('hex'), - backup_encrypted_dek: JSON.stringify(newBackupEncryptedDEK), - }) - .where(eq(users.id, userId)); - - // Clean sensitive data - originalDEK.fill(0); - newKEK.fill(0); - newRecoveryDEK.fill(0); - - authLogger.success("Password reset with KEK-DEK re-encryption successful", { - operation: "password_reset_with_kek_dek", - username, - userId, - }); - } catch (error) { - authLogger.error("Failed to re-encrypt KEK-DEK during password reset", error, { - operation: "password_reset_kek_dek_failed", - username, - }); - return res.status(500).json({ - error: "Failed to preserve encrypted data during password reset" - }); - } - } else { - // Fallback: No recovery data available - user will lose encrypted data - const saltRounds = parseInt(process.env.SALT || "10", 10); - const password_hash = await bcrypt.hash(newPassword, saltRounds); - - await db - .update(users) - .set({ password_hash }) - .where(eq(users.username, username)); - - authLogger.warn("Password reset without KEK-DEK preservation - encrypted data may be lost", { - operation: "password_reset_no_recovery", - username, - userId, - }); - } + authLogger.success(`Password successfully reset for user: ${username}`); db.$client .prepare("DELETE FROM settings WHERE key = ?") @@ -1371,7 +1307,6 @@ router.post("/complete-reset", async (req, res) => { .prepare("DELETE FROM settings WHERE key = ?") .run(`temp_reset_token_${username}`); - authLogger.success(`Password successfully reset for user: ${username}`); res.json({ message: "Password has been successfully reset" }); } catch (err) { authLogger.error("Failed to complete password reset", err); @@ -1926,8 +1861,6 @@ router.get("/data-status", authenticateJWT, async (req, res) => { } }); -// Duplicate logout route removed - handled by the main logout route above - // Route: Change user password (re-encrypt data keys) // POST /users/change-password router.post("/change-password", authenticateJWT, async (req, res) => { @@ -1988,408 +1921,4 @@ router.post("/change-password", authenticateJWT, async (req, res) => { } }); -// Route: Get security status (admin) -// GET /users/security-status -router.get("/security-status", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0 || !user[0].is_admin) { - return res.status(403).json({ error: "Not authorized" }); - } - - // Simplified security status for new architecture - const securityStatus = { - initialized: true, - system: { hasSecret: true, isValid: true }, - activeSessions: {}, - activeSessionCount: 0 - }; - res.json(securityStatus); - } catch (err) { - authLogger.error("Failed to get security status", err, { - operation: "security_status_error", - userId, - }); - res.status(500).json({ error: "Failed to get security status" }); - } -}); - -// ===== Password Recovery API endpoints (UX compromise) ===== - -// Route: Request recovery code (outputs to Docker logs) -// POST /users/recovery/request -router.post("/recovery/request", async (req, res) => { - const { username } = req.body; - - if (!isNonEmptyString(username)) { - return res.status(400).json({ error: "Username is required" }); - } - - try { - // Check if user exists - const user = await db - .select() - .from(users) - .where(eq(users.username, username)); - - if (!user || user.length === 0) { - return res.status(404).json({ error: "User not found" }); - } - - // Check if user has recovery data - if (!user[0].recovery_dek) { - return res.status(400).json({ - error: "Recovery not available for this user" - }); - } - - // Generate 6-digit recovery code - const recoveryCode = crypto.randomInt(100000, 1000000).toString(); - const expiresAt = Date.now() + 60 * 1000; // 1 minute expiry - - // Store recovery code in settings - const key = `recovery_code_${username}`; - const value = JSON.stringify({ - code: recoveryCode, - expiresAt, - attempts: 0 - }); - - const existing = await db.select().from(settings).where(eq(settings.key, key)); - if (existing.length > 0) { - await db.update(settings).set({ value }).where(eq(settings.key, key)); - } else { - await db.insert(settings).values({ key, value }); - } - - // 🔥 Output to Docker logs for physical access verification - console.log(`[RECOVERY] Code for user '${username}': ${recoveryCode} (expires in 60s)`); - - authLogger.info("Recovery code generated", { - operation: "recovery_code_generated", - username, - expiresAt, - }); - - res.json({ - message: "Recovery code sent to system logs", - instruction: "Use 'docker logs termix | grep RECOVERY' to view the code" - }); - - } catch (error) { - authLogger.error("Failed to generate recovery code", error, { - operation: "recovery_request_failed", - username, - }); - res.status(500).json({ error: "Failed to generate recovery code" }); - } -}); - -// Route: Verify recovery code and auto-decrypt user data -// POST /users/recovery/verify -router.post("/recovery/verify", async (req, res) => { - const { username, code } = req.body; - - if (!isNonEmptyString(username) || !isNonEmptyString(code)) { - return res.status(400).json({ - error: "Username and recovery code are required" - }); - } - - try { - // Get and verify recovery code - const key = `recovery_code_${username}`; - const result = await db.select().from(settings).where(eq(settings.key, key)); - - if (result.length === 0) { - return res.status(400).json({ error: "No recovery code found" }); - } - - const codeData = JSON.parse(result[0].value); - const now = Date.now(); - - // Check expiry - if (now > codeData.expiresAt) { - await db.delete(settings).where(eq(settings.key, key)); - return res.status(400).json({ error: "Recovery code has expired" }); - } - - // Check attempts (prevent brute force) - if (codeData.attempts >= 3) { - await db.delete(settings).where(eq(settings.key, key)); - return res.status(400).json({ error: "Too many attempts" }); - } - - // Verify code - if (codeData.code !== code) { - // Increment attempts - codeData.attempts++; - await db.update(settings) - .set({ value: JSON.stringify(codeData) }) - .where(eq(settings.key, key)); - - return res.status(400).json({ error: "Invalid recovery code" }); - } - - // Get user data - const user = await db - .select() - .from(users) - .where(eq(users.username, username)); - - if (!user[0].recovery_dek || !user[0].backup_encrypted_dek) { - return res.status(400).json({ error: "Recovery data not available" }); - } - - // 🔥 Auto-decrypt using recovery DEK - const userCrypto = UserCrypto.getInstance(); - const recoveryDEK = Buffer.from(user[0].recovery_dek, 'hex'); - const backupEncryptedDEK = JSON.parse(user[0].backup_encrypted_dek); - - try { - // Decrypt original DEK using recovery key - const originalDEK = (userCrypto as any).decryptDEK(backupEncryptedDEK, recoveryDEK); - - // Create user session with decrypted DEK - const tempToken = crypto.randomBytes(32).toString('hex'); - const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes - - // Store temporary session - const tempKey = `temp_recovery_session_${username}`; - const tempValue = JSON.stringify({ - userId: user[0].id, - tempToken, - expiresAt, - dekHex: originalDEK.toString('hex') - }); - - const existingTemp = await db.select().from(settings).where(eq(settings.key, tempKey)); - if (existingTemp.length > 0) { - await db.update(settings).set({ value: tempValue }).where(eq(settings.key, tempKey)); - } else { - await db.insert(settings).values({ key: tempKey, value: tempValue }); - } - - // Clean up recovery code - await db.delete(settings).where(eq(settings.key, key)); - - authLogger.success("Recovery verification successful", { - operation: "recovery_verify_success", - username, - userId: user[0].id, - }); - - res.json({ - success: true, - tempToken, - expiresAt, - message: "Recovery successful. You can now login normally.", - }); - - } catch (decryptError) { - authLogger.error("Failed to decrypt recovery data", decryptError, { - operation: "recovery_decrypt_failed", - username, - }); - res.status(500).json({ error: "Failed to decrypt user data" }); - } - - } catch (error) { - authLogger.error("Recovery verification failed", error, { - operation: "recovery_verify_failed", - username, - }); - res.status(500).json({ error: "Recovery verification failed" }); - } -}); - -// Route: Login with recovery session (temporary login after recovery) -// POST /users/recovery/login -router.post("/recovery/login", async (req, res) => { - const { username, tempToken } = req.body; - - if (!isNonEmptyString(username) || !isNonEmptyString(tempToken)) { - return res.status(400).json({ - error: "Username and temporary token are required" - }); - } - - try { - // Get temporary session - const tempKey = `temp_recovery_session_${username}`; - const result = await db.select().from(settings).where(eq(settings.key, tempKey)); - - if (result.length === 0) { - return res.status(400).json({ error: "Invalid recovery session" }); - } - - const sessionData = JSON.parse(result[0].value); - const now = Date.now(); - - // Check expiry - if (now > sessionData.expiresAt) { - await db.delete(settings).where(eq(settings.key, tempKey)); - return res.status(400).json({ error: "Recovery session has expired" }); - } - - // Verify token - if (sessionData.tempToken !== tempToken) { - return res.status(400).json({ error: "Invalid temporary token" }); - } - - // Get user - const user = await db.select().from(users).where(eq(users.id, sessionData.userId)); - if (!user || user.length === 0) { - return res.status(404).json({ error: "User not found" }); - } - - // Create DEK session for user - const userCrypto = UserCrypto.getInstance(); - const originalDEK = Buffer.from(sessionData.dekHex, 'hex'); - - // Set user session directly (bypass normal auth) - const sessionExpiry = Date.now() + 24 * 60 * 60 * 1000; // 24 hours - (userCrypto as any).userSessions.set(sessionData.userId, { - dataKey: originalDEK, - lastActivity: Date.now(), - expiresAt: sessionExpiry, - }); - - // Generate JWT token - const token = await authManager.generateJWTToken(sessionData.userId, { - expiresIn: "24h", - }); - - // Clean up temporary session - await db.delete(settings).where(eq(settings.key, tempKey)); - - authLogger.success("Recovery login successful", { - operation: "recovery_login_success", - username, - userId: sessionData.userId, - }); - - return res - .cookie("jwt", token, authManager.getSecureCookieOptions(req, 24 * 60 * 60 * 1000)) - .json({ - success: true, - is_admin: !!user[0].is_admin, - username: user[0].username, - message: "Login successful via recovery" - }); - - } catch (error) { - authLogger.error("Recovery login failed", error, { - operation: "recovery_login_failed", - username, - }); - res.status(500).json({ error: "Recovery login failed" }); - } -}); - -// ===== Zero Trust Migration API endpoints ===== - -// Route: Get user security mode status -// GET /users/security-mode -router.get("/security-mode", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - - try { - const migration = ZeroTrustMigration.getInstance(); - const status = await migration.getUserSecurityMode(userId); - - res.json(status); - } catch (error) { - authLogger.error("Failed to get security mode", error, { - operation: "get_security_mode_failed", - userId, - }); - res.status(500).json({ error: "Failed to get security mode" }); - } -}); - -// Route: Migrate user to zero-trust mode -// POST /users/migrate-to-zero-trust -router.post("/migrate-to-zero-trust", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - - try { - const migration = ZeroTrustMigration.getInstance(); - - // Check if user can migrate - const canMigrate = await migration.canMigrateToZeroTrust(userId); - if (!canMigrate) { - return res.status(400).json({ - error: "User cannot migrate to zero-trust mode" - }); - } - - // Perform migration - const recoverySeed = await migration.migrateUserToZeroTrust(userId); - if (!recoverySeed) { - return res.status(500).json({ - error: "Migration failed" - }); - } - - authLogger.success("User migrated to zero-trust mode", { - operation: "zero_trust_migration_success", - userId, - }); - - res.json({ - success: true, - recoverySeed, - message: "Migration successful. Save this recovery seed securely!", - warning: "This is the only time you will see this seed. Store it safely!" - }); - - } catch (error) { - authLogger.error("Zero-trust migration failed", error, { - operation: "zero_trust_migration_error", - userId, - }); - res.status(500).json({ error: "Migration failed" }); - } -}); - -// Route: Zero-trust password recovery (requires user seed) -// POST /users/recovery/zero-trust -router.post("/recovery/zero-trust", async (req, res) => { - const { username, code, userSeed } = req.body; - - if (!username || !code || !userSeed) { - return res.status(400).json({ - error: "Username, recovery code, and user seed are required" - }); - } - - try { - const migration = ZeroTrustMigration.getInstance(); - const result = await migration.recoverInZeroTrustMode(username, code, userSeed); - - if (result.success) { - res.json({ - success: true, - tempToken: result.tempToken, - expiresAt: result.expiresAt, - message: "Zero-trust recovery successful" - }); - } else { - res.status(400).json({ - error: "Recovery failed. Check your code and recovery seed." - }); - } - - } catch (error) { - authLogger.error("Zero-trust recovery failed", error, { - operation: "zero_trust_recovery_error", - username, - }); - res.status(500).json({ error: "Recovery failed" }); - } -}); - -export default router; +export default router; \ No newline at end of file diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 4f8a9725..918f35fc 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -79,20 +79,13 @@ class UserCrypto { const encryptedDEK = this.encryptDEK(DEK, KEK); await this.storeEncryptedDEK(userId, encryptedDEK); - // 🔴 Add recovery layer (breaks zero-trust for UX) - const recoveryDEK = crypto.randomBytes(UserCrypto.DEK_LENGTH); - const backupEncryptedDEK = this.encryptDEK(DEK, recoveryDEK); - await this.storeRecoveryData(userId, recoveryDEK, backupEncryptedDEK); - // Immediately clean temporary keys KEK.fill(0); DEK.fill(0); - recoveryDEK.fill(0); - databaseLogger.success("User encryption setup completed with recovery layer", { + databaseLogger.success("User encryption setup completed", { operation: "user_crypto_setup", userId, - recoveryEnabled: true, }); } @@ -427,33 +420,6 @@ class UserCrypto { } } - /** - * Store recovery data (breaks zero-trust for UX compromise) - */ - private async storeRecoveryData(userId: string, recoveryDEK: Buffer, backupEncryptedDEK: EncryptedDEK): Promise { - try { - await getDb() - .update(users) - .set({ - recovery_dek: recoveryDEK.toString('hex'), - backup_encrypted_dek: JSON.stringify(backupEncryptedDEK), - zero_trust_mode: false, // Compromise mode for UX - }) - .where(eq(users.id, userId)); - - databaseLogger.info("Recovery data stored for user", { - operation: "store_recovery_data", - userId, - mode: "compromise", - }); - } catch (error) { - databaseLogger.error("Failed to store recovery data", error, { - operation: "store_recovery_data_failed", - userId, - }); - throw error; - } - } } export { UserCrypto, type KEKSalt, type EncryptedDEK }; \ No newline at end of file diff --git a/src/backend/utils/zero-trust-migration.ts b/src/backend/utils/zero-trust-migration.ts deleted file mode 100644 index b03e36db..00000000 --- a/src/backend/utils/zero-trust-migration.ts +++ /dev/null @@ -1,264 +0,0 @@ -import crypto from "crypto"; -import { getDb } from "../database/db/index.js"; -import { users, settings } from "../database/db/schema.js"; -import { eq } from "drizzle-orm"; -import { databaseLogger } from "./logger.js"; -import { UserCrypto, type EncryptedDEK } from "./user-crypto.js"; - -/** - * ZeroTrustMigration - Handle migration between compromise and zero-trust modes - * - * Linus principles: - * - Simple migration path with clear rollback capability - * - User controls the security tradeoff - * - Future-proof architecture for security upgrades - */ -export class ZeroTrustMigration { - private static instance: ZeroTrustMigration; - - private constructor() {} - - static getInstance(): ZeroTrustMigration { - if (!this.instance) { - this.instance = new ZeroTrustMigration(); - } - return this.instance; - } - - /** - * Check if user can migrate to zero-trust mode - */ - async canMigrateToZeroTrust(userId: string): Promise { - try { - const db = getDb(); - const user = await db.select().from(users).where(eq(users.id, userId)); - - if (!user || user.length === 0) { - return false; - } - - // Must have recovery data and be in compromise mode - return !!(user[0].recovery_dek && !user[0].zero_trust_mode); - } catch (error) { - databaseLogger.error("Failed to check zero-trust migration eligibility", error, { - operation: "zero_trust_check_failed", - userId, - }); - return false; - } - } - - /** - * Migrate user to zero-trust mode - * Returns recovery seed for user to save - */ - async migrateUserToZeroTrust(userId: string): Promise { - try { - const db = getDb(); - const user = await db.select().from(users).where(eq(users.id, userId)); - - if (!user || user.length === 0) { - throw new Error("User not found"); - } - - const userData = user[0]; - if (!userData.recovery_dek || !userData.backup_encrypted_dek) { - throw new Error("No recovery data available"); - } - - if (userData.zero_trust_mode) { - throw new Error("User already in zero-trust mode"); - } - - // Generate user recovery seed (256-bit) - const userRecoverySeed = crypto.randomBytes(32).toString('hex'); - - // Get current DEK from plaintext recovery data - const userCrypto = UserCrypto.getInstance(); - const recoveryDEK = Buffer.from(userData.recovery_dek, 'hex'); - const backupEncryptedDEK = JSON.parse(userData.backup_encrypted_dek); - const originalDEK = (userCrypto as any).decryptDEK(backupEncryptedDEK, recoveryDEK); - - // Create user recovery key from seed - const userRecoveryKey = crypto.pbkdf2Sync( - userRecoverySeed, - 'zero_trust_recovery', - 100000, - 32, - 'sha256' - ); - - // Re-encrypt DEK with user recovery key - const newBackupEncryptedDEK = (userCrypto as any).encryptDEK(originalDEK, userRecoveryKey); - - // 🔥 Remove plaintext recovery data and enable zero-trust mode - await db - .update(users) - .set({ - recovery_dek: null, // Delete plaintext recovery key - backup_encrypted_dek: JSON.stringify(newBackupEncryptedDEK), - zero_trust_mode: true, // Enable zero-trust mode - }) - .where(eq(users.id, userId)); - - databaseLogger.success("User migrated to zero-trust mode", { - operation: "zero_trust_migration_success", - userId, - mode: "zero_trust", - }); - - return userRecoverySeed; - - } catch (error) { - databaseLogger.error("Failed to migrate user to zero-trust", error, { - operation: "zero_trust_migration_failed", - userId, - }); - throw error; - } - } - - /** - * Zero-trust recovery (requires user recovery seed) - */ - async recoverInZeroTrustMode( - username: string, - code: string, - userSeed: string - ): Promise<{ success: boolean; tempToken?: string; expiresAt?: number }> { - try { - const db = getDb(); - - // Verify recovery code (same mechanism as compromise mode) - const key = `recovery_code_${username}`; - const result = await db.select().from(settings).where(eq(settings.key, key)); - - if (result.length === 0) { - return { success: false }; - } - - const codeData = JSON.parse(result[0].value); - const now = Date.now(); - - // Check expiry and attempts - if (now > codeData.expiresAt || codeData.attempts >= 3) { - await db.delete(settings).where(eq(settings.key, key)); - return { success: false }; - } - - // Verify code - if (codeData.code !== code) { - codeData.attempts++; - await db.update(settings) - .set({ value: JSON.stringify(codeData) }) - .where(eq(settings.key, key)); - return { success: false }; - } - - // Get user - const user = await db.select().from(users).where(eq(users.username, username)); - if (!user || user.length === 0) { - return { success: false }; - } - - const userData = user[0]; - if (!userData.zero_trust_mode || !userData.backup_encrypted_dek) { - return { success: false }; - } - - // Derive user recovery key from seed - const userRecoveryKey = crypto.pbkdf2Sync( - userSeed, - 'zero_trust_recovery', - 100000, - 32, - 'sha256' - ); - - try { - // Decrypt DEK using user recovery key - const userCrypto = UserCrypto.getInstance(); - const backupEncryptedDEK = JSON.parse(userData.backup_encrypted_dek); - const originalDEK = (userCrypto as any).decryptDEK(backupEncryptedDEK, userRecoveryKey); - - // Create temporary session - const tempToken = crypto.randomBytes(32).toString('hex'); - const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes - - const tempKey = `temp_recovery_session_${username}`; - const tempValue = JSON.stringify({ - userId: userData.id, - tempToken, - expiresAt, - dekHex: originalDEK.toString('hex'), - mode: 'zero_trust' - }); - - const existingTemp = await db.select().from(settings).where(eq(settings.key, tempKey)); - if (existingTemp.length > 0) { - await db.update(settings).set({ value: tempValue }).where(eq(settings.key, tempKey)); - } else { - await db.insert(settings).values({ key: tempKey, value: tempValue }); - } - - // Clean up recovery code - await db.delete(settings).where(eq(settings.key, key)); - - databaseLogger.success("Zero-trust recovery successful", { - operation: "zero_trust_recovery_success", - username, - userId: userData.id, - }); - - return { success: true, tempToken, expiresAt }; - - } catch (decryptError) { - // Invalid seed - decryption failed - databaseLogger.warn("Zero-trust recovery failed - invalid seed", { - operation: "zero_trust_recovery_invalid_seed", - username, - }); - return { success: false }; - } - - } catch (error) { - databaseLogger.error("Zero-trust recovery failed", error, { - operation: "zero_trust_recovery_error", - username, - }); - return { success: false }; - } - } - - /** - * Get user security mode status - */ - async getUserSecurityMode(userId: string): Promise<{ - mode: 'compromise' | 'zero_trust'; - canMigrate: boolean; - hasRecoveryData: boolean; - }> { - try { - const db = getDb(); - const user = await db.select().from(users).where(eq(users.id, userId)); - - if (!user || user.length === 0) { - return { mode: 'compromise', canMigrate: false, hasRecoveryData: false }; - } - - const userData = user[0]; - const mode = userData.zero_trust_mode ? 'zero_trust' : 'compromise'; - const hasRecoveryData = !!(userData.recovery_dek || userData.backup_encrypted_dek); - const canMigrate = mode === 'compromise' && hasRecoveryData; - - return { mode, canMigrate, hasRecoveryData }; - - } catch (error) { - databaseLogger.error("Failed to get user security mode", error, { - operation: "get_security_mode_failed", - userId, - }); - return { mode: 'compromise', canMigrate: false, hasRecoveryData: false }; - } - } -} \ No newline at end of file diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx index e28ad473..8573efd8 100644 --- a/src/ui/Desktop/Homepage/HomepageAuth.tsx +++ b/src/ui/Desktop/Homepage/HomepageAuth.tsx @@ -14,9 +14,6 @@ import { getRegistrationAllowed, getOIDCConfig, getSetupRequired, - requestRecoveryCode, - verifyRecoveryCode, - loginWithRecovery, initiatePasswordReset, verifyPasswordResetCode, completePasswordReset, @@ -84,15 +81,6 @@ export function HomepageAuth({ const [registrationAllowed, setRegistrationAllowed] = useState(true); const [oidcConfigured, setOidcConfigured] = useState(false); - // Recovery states (new UX compromise flow) - const [recoveryStep, setRecoveryStep] = useState< - "request" | "verify" | "login" - >("request"); - const [recoveryCode, setRecoveryCode] = useState(""); - const [recoveryTempToken, setRecoveryTempToken] = useState(""); - const [recoveryLoading, setRecoveryLoading] = useState(false); - const [recoverySuccess, setRecoverySuccess] = useState(false); - // Legacy reset states (kept for compatibility) const [resetStep, setResetStep] = useState< "initiate" | "verify" | "newPassword" @@ -257,80 +245,9 @@ export function HomepageAuth({ } } - // ===== New Recovery Functions (UX compromise) ===== - async function handleRequestRecoveryCode() { - setError(null); - setRecoveryLoading(true); - try { - const result = await requestRecoveryCode(localUsername); - setRecoveryStep("verify"); - toast.success("Recovery code sent to Docker logs"); - } catch (err: any) { - toast.error( - err?.response?.data?.error || - err?.message || - "Failed to request recovery code", - ); - } finally { - setRecoveryLoading(false); - } - } - async function handleVerifyRecoveryCode() { - setError(null); - setRecoveryLoading(true); - try { - const response = await verifyRecoveryCode(localUsername, recoveryCode); - setRecoveryTempToken(response.tempToken); - setRecoveryStep("login"); - toast.success("Recovery verification successful"); - } catch (err: any) { - toast.error(err?.response?.data?.error || "Failed to verify recovery code"); - } finally { - setRecoveryLoading(false); - } - } - async function handleRecoveryLogin() { - setError(null); - setRecoveryLoading(true); - try { - const response = await loginWithRecovery(localUsername, recoveryTempToken); - - // JWT token is now automatically set as HttpOnly cookie by backend - console.log("Recovery login successful - JWT set as secure HttpOnly cookie"); - - setLoggedIn(true); - setIsAdmin(response.is_admin); - setUsername(response.username); - - onAuthSuccess({ - isAdmin: response.is_admin, - username: response.username, - userId: response.userId || null, - }); - - // Reset recovery state - setRecoveryStep("request"); - setRecoveryCode(""); - setRecoveryTempToken(""); - setRecoverySuccess(true); - - toast.success("Login successful via recovery"); - } catch (err: any) { - toast.error(err?.response?.data?.error || "Recovery login failed"); - } finally { - setRecoveryLoading(false); - } - } - - function resetRecoveryState() { - setRecoveryStep("request"); - setRecoveryCode(""); - setRecoveryTempToken(""); - setError(null); - } // ===== Legacy password reset functions (deprecated) ===== @@ -395,6 +312,10 @@ export function HomepageAuth({ setResetSuccess(true); toast.success(t("messages.passwordResetSuccess")); + + // Immediately redirect to login after successful reset + setTab("login"); + resetPasswordState(); } catch (err: any) { toast.error(err?.response?.data?.error || t("errors.failedCompleteReset")); } finally { @@ -899,119 +820,33 @@ export function HomepageAuth({ )} {tab === "reset" && ( <> - {/* New Recovery Flow (UX compromise) */} - {recoveryStep === "request" && ( + {resetStep === "initiate" && ( <>
-

- Recovery requires server access to view Docker logs -

+

{t("auth.resetCodeDesc")}

-
-
- - )} - - {recoveryStep === "verify" && ( - <> -
-

- Check Docker logs for recovery code for{" "} - {localUsername} -

-
-
-
- - - setRecoveryCode(e.target.value.replace(/\D/g, "")) - } - disabled={recoveryLoading} - placeholder="000000" - /> -
- - -
- - )} - - {recoveryStep === "login" && ( - <> -
-

- Click below to complete login for{" "} - {localUsername} -

-
-
- -
@@ -1070,25 +905,6 @@ export function HomepageAuth({ )} - {resetSuccess && ( - <> -
-

- {t("auth.passwordResetSuccessDesc")} -

-
- - - )} {resetStep === "newPassword" && !resetSuccess && ( <> @@ -1100,7 +916,7 @@ export function HomepageAuth({
-