import React, { useCallback, useEffect, useState } from "react"; import { View } from "react-native"; import { useAuth, validatePassword } from "../../hooks/useAuth"; import { useNotificationContext } from "../../context/NotificationContext"; import AuthService from "../../services/auth.service"; import { cn } from "../../utils/cn"; import { Button } from "../button"; import { Card, CardContent } from "../card"; import { PasswordInput } from "../input"; // See ProfileTab.tsx's comment on this same import — FormLabel's Root // is a bare Pressable, not Text; these are static captions, not // pressable label-input pairs. import { FormLabelText as Label } from "../label"; import { ErrorText, Text } from "../text"; export interface SecurityTabProps { /** Whether the surrounding dialog is currently open — drives state reset. */ visible: boolean; /** Close the surrounding dialog (consumed when navigating to MFA setup). */ onClose: () => void; /** * Fired when the user taps "Enable MFA" — apps wire this to whatever * screen they use for TOTP enrollment (vault routes to its mfa-setup * tab; bootstrap could route somewhere else). */ onEnableMfa: () => void; /** * Optional hook invoked before the password change is submitted to the * server. Apps that re-wrap a client-side encryption key on rotation * (vault) wire it here; the returned payload is forwarded to * `AuthService.changePassword` so the server can apply both changes * atomically. Apps without a wrapped key pass nothing — the password * change happens unmodified. */ onComputeReWrap?: ( currentPassword: string, newPassword: string ) => Promise< | { symmetric_key_enc: string; kdf: string; kdf_salt: string; kdf_iterations: number; kdf_memory_kib?: number; kdf_lanes?: number; } | undefined >; } /** * "Security" tab body: change master password (with optional client-side * key re-wrap) and toggle MFA. The MFA-enrolment route is consumer- * specific — pass it via `onEnableMfa`. */ export function SecurityTab({ visible, onClose, onEnableMfa, onComputeReWrap }: SecurityTabProps) { const { user, refreshUser } = useAuth(); const { push } = useNotificationContext(); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [passwordLoading, setPasswordLoading] = useState(false); const [passwordError, setPasswordError] = useState(""); const [mfaPassword, setMfaPassword] = useState(""); const [mfaLoading, setMfaLoading] = useState(false); const [mfaError, setMfaError] = useState(""); const [mfaSuccess, setMfaSuccess] = useState(""); useEffect(() => { if (visible) { return; } setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); setPasswordError(""); setMfaPassword(""); setMfaError(""); setMfaSuccess(""); }, [visible]); const handleSavePassword = useCallback(async () => { setPasswordError(""); if (!currentPassword) { setPasswordError("Current password is required"); return; } if (newPassword !== confirmPassword) { setPasswordError("Passwords do not match"); return; } const validation = validatePassword(newPassword); if (!validation.valid) { setPasswordError(validation.errors.join(", ")); return; } setPasswordLoading(true); try { const reWrappedKey = onComputeReWrap ? await onComputeReWrap(currentPassword, newPassword) : undefined; await AuthService.changePassword(currentPassword, newPassword, reWrappedKey); setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); push({ level: "success", title: "Password updated" }); } catch (err) { setPasswordError(err instanceof Error ? err.message : "Failed to update password"); } finally { setPasswordLoading(false); } }, [currentPassword, newPassword, confirmPassword, push, onComputeReWrap]); const handleDisableMfa = useCallback(async () => { setMfaError(""); setMfaSuccess(""); if (!mfaPassword) { setMfaError("Password is required to disable MFA"); return; } setMfaLoading(true); try { await AuthService.disableMfa(mfaPassword); await refreshUser(); setMfaPassword(""); setMfaSuccess("MFA disabled"); } catch (err) { setMfaError(err instanceof Error ? err.message : "Failed to disable MFA"); } finally { setMfaLoading(false); } }, [mfaPassword, refreshUser]); const handleEnableMfa = useCallback(() => { onClose(); onEnableMfa(); }, [onClose, onEnableMfa]); return ( {user?.mfa_enabled ? "Enabled" : "Not enabled"} {user?.mfa_enabled ? ( <> {mfaSuccess ? ( {mfaSuccess} ) : null} ) : ( <> {mfaError ? {mfaError} : null} )} ); }