Changes
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Smartphone, Copy, CheckCircle, Loader2, AlertCircle, ShieldCheck } from "lucide-react";
|
import { Smartphone, Copy, CheckCircle, Loader2, AlertCircle, ShieldCheck } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -11,8 +11,9 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { api, ApiError, TotpEnrollResponse } from "@/lib/api";
|
||||||
|
|
||||||
type WizardStep = "setup" | "verify" | "backup-codes" | "success";
|
type WizardStep = "loading" | "setup" | "verify" | "backup-codes" | "success";
|
||||||
|
|
||||||
interface TotpEnrollmentWizardProps {
|
interface TotpEnrollmentWizardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -25,35 +26,56 @@ export function TotpEnrollmentWizard({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: TotpEnrollmentWizardProps) {
|
}: TotpEnrollmentWizardProps) {
|
||||||
const [step, setStep] = useState<WizardStep>("setup");
|
const [step, setStep] = useState<WizardStep>("loading");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [verificationCode, setVerificationCode] = useState("");
|
const [verificationCode, setVerificationCode] = useState("");
|
||||||
const [verifyError, setVerifyError] = useState<string | null>(null);
|
const [verifyError, setVerifyError] = useState<string | null>(null);
|
||||||
const [copiedSecret, setCopiedSecret] = useState(false);
|
const [copiedSecret, setCopiedSecret] = useState(false);
|
||||||
const [copiedBackupCodes, setCopiedBackupCodes] = useState(false);
|
const [copiedBackupCodes, setCopiedBackupCodes] = useState(false);
|
||||||
|
const [enrollmentData, setEnrollmentData] = useState<TotpEnrollResponse | null>(null);
|
||||||
|
const [enrollError, setEnrollError] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Mock data - will be replaced with actual API calls
|
// Fetch enrollment data when dialog opens
|
||||||
const mockSecret = "JBSWY3DPEHPK3PXP";
|
useEffect(() => {
|
||||||
const mockQrCode = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=otpauth://totp/Gatehouse:user@example.com?secret=${mockSecret}&issuer=Gatehouse`;
|
if (open) {
|
||||||
const mockBackupCodes = [
|
initiateEnrollment();
|
||||||
"ABCD-1234-EFGH",
|
}
|
||||||
"IJKL-5678-MNOP",
|
}, [open]);
|
||||||
"QRST-9012-UVWX",
|
|
||||||
"YZAB-3456-CDEF",
|
const initiateEnrollment = async () => {
|
||||||
"GHIJ-7890-KLMN",
|
setStep("loading");
|
||||||
"OPQR-1234-STUV",
|
setEnrollError(null);
|
||||||
"WXYZ-5678-ABCD",
|
setEnrollmentData(null);
|
||||||
"EFGH-9012-IJKL",
|
|
||||||
];
|
try {
|
||||||
|
const data = await api.totp.enroll();
|
||||||
|
setEnrollmentData(data);
|
||||||
|
setStep("setup");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("TOTP enrollment initiation failed:", err);
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
if (err.code === 409) {
|
||||||
|
setEnrollError("TOTP is already enabled on this account. Please remove it first to reconfigure.");
|
||||||
|
} else {
|
||||||
|
setEnrollError(err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEnrollError("Failed to initiate TOTP enrollment. Please try again.");
|
||||||
|
}
|
||||||
|
setStep("setup"); // Show error state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const resetWizard = () => {
|
const resetWizard = () => {
|
||||||
setStep("setup");
|
setStep("loading");
|
||||||
setVerificationCode("");
|
setVerificationCode("");
|
||||||
setVerifyError(null);
|
setVerifyError(null);
|
||||||
setCopiedSecret(false);
|
setCopiedSecret(false);
|
||||||
setCopiedBackupCodes(false);
|
setCopiedBackupCodes(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
setEnrollmentData(null);
|
||||||
|
setEnrollError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (isOpen: boolean) => {
|
const handleClose = (isOpen: boolean) => {
|
||||||
@@ -64,8 +86,9 @@ export function TotpEnrollmentWizard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopySecret = async () => {
|
const handleCopySecret = async () => {
|
||||||
|
if (!enrollmentData) return;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(mockSecret);
|
await navigator.clipboard.writeText(enrollmentData.secret);
|
||||||
setCopiedSecret(true);
|
setCopiedSecret(true);
|
||||||
setTimeout(() => setCopiedSecret(false), 2000);
|
setTimeout(() => setCopiedSecret(false), 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -74,8 +97,9 @@ export function TotpEnrollmentWizard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyBackupCodes = async () => {
|
const handleCopyBackupCodes = async () => {
|
||||||
|
if (!enrollmentData) return;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(mockBackupCodes.join("\n"));
|
await navigator.clipboard.writeText(enrollmentData.backup_codes.join("\n"));
|
||||||
setCopiedBackupCodes(true);
|
setCopiedBackupCodes(true);
|
||||||
toast({
|
toast({
|
||||||
title: "Backup codes copied",
|
title: "Backup codes copied",
|
||||||
@@ -97,20 +121,15 @@ export function TotpEnrollmentWizard({
|
|||||||
setVerifyError(null);
|
setVerifyError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Call actual API to verify TOTP code
|
await api.totp.verifyEnrollment(verificationCode);
|
||||||
// await api.users.verifyTotpEnrollment(verificationCode);
|
|
||||||
|
|
||||||
// Mock verification - accept "123456" for testing
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
if (verificationCode === "123456") {
|
|
||||||
setStep("backup-codes");
|
setStep("backup-codes");
|
||||||
} else {
|
|
||||||
setVerifyError("Invalid verification code. Please try again.");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("TOTP verification failed:", err);
|
console.error("TOTP verification failed:", err);
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
setVerifyError(err.message || "Invalid verification code. Please try again.");
|
||||||
|
} else {
|
||||||
setVerifyError("Verification failed. Please try again.");
|
setVerifyError("Verification failed. Please try again.");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -137,13 +156,15 @@ export function TotpEnrollmentWizard({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Smartphone className="w-5 h-5" />
|
<Smartphone className="w-5 h-5" />
|
||||||
|
{step === "loading" && "Setting up..."}
|
||||||
{step === "setup" && "Set up Authenticator App"}
|
{step === "setup" && "Set up Authenticator App"}
|
||||||
{step === "verify" && "Verify Setup"}
|
{step === "verify" && "Verify Setup"}
|
||||||
{step === "backup-codes" && "Save Backup Codes"}
|
{step === "backup-codes" && "Save Backup Codes"}
|
||||||
{step === "success" && "Setup Complete"}
|
{step === "success" && "Setup Complete"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{step === "setup" && "Scan the QR code with your authenticator app"}
|
{step === "loading" && "Preparing your TOTP enrollment..."}
|
||||||
|
{step === "setup" && (enrollError ? "An error occurred" : "Scan the QR code with your authenticator app")}
|
||||||
{step === "verify" && "Enter the code from your authenticator app"}
|
{step === "verify" && "Enter the code from your authenticator app"}
|
||||||
{step === "backup-codes" && "Save these codes in a safe place"}
|
{step === "backup-codes" && "Save these codes in a safe place"}
|
||||||
{step === "success" && "Two-factor authentication is now enabled"}
|
{step === "success" && "Two-factor authentication is now enabled"}
|
||||||
@@ -151,11 +172,40 @@ export function TotpEnrollmentWizard({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{step === "setup" && (
|
{step === "loading" && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">Preparing enrollment...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "setup" && enrollError && (
|
||||||
|
<>
|
||||||
|
<div className="p-4 bg-destructive/10 border border-destructive/30 rounded-lg">
|
||||||
|
<div className="flex gap-2 text-destructive">
|
||||||
|
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium">Enrollment failed</p>
|
||||||
|
<p className="text-destructive/80">{enrollError}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button variant="outline" onClick={() => handleClose(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={initiateEnrollment}>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "setup" && !enrollError && enrollmentData && (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-center p-4 bg-white rounded-lg">
|
<div className="flex justify-center p-4 bg-white rounded-lg">
|
||||||
<img
|
<img
|
||||||
src={mockQrCode}
|
src={`data:image/png;base64,${enrollmentData.qr_code}`}
|
||||||
alt="TOTP QR Code"
|
alt="TOTP QR Code"
|
||||||
className="w-48 h-48"
|
className="w-48 h-48"
|
||||||
/>
|
/>
|
||||||
@@ -167,7 +217,7 @@ export function TotpEnrollmentWizard({
|
|||||||
</Label>
|
</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<code className="flex-1 px-3 py-2 text-sm font-mono bg-muted rounded-md break-all">
|
<code className="flex-1 px-3 py-2 text-sm font-mono bg-muted rounded-md break-all">
|
||||||
{mockSecret}
|
{enrollmentData.secret}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -251,7 +301,7 @@ export function TotpEnrollmentWizard({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === "backup-codes" && (
|
{step === "backup-codes" && enrollmentData && (
|
||||||
<>
|
<>
|
||||||
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg">
|
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg">
|
||||||
<div className="flex gap-2 text-warning">
|
<div className="flex gap-2 text-warning">
|
||||||
@@ -267,7 +317,7 @@ export function TotpEnrollmentWizard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 p-4 bg-muted rounded-lg">
|
<div className="grid grid-cols-2 gap-2 p-4 bg-muted rounded-lg">
|
||||||
{mockBackupCodes.map((code, index) => (
|
{enrollmentData.backup_codes.map((code, index) => (
|
||||||
<code
|
<code
|
||||||
key={index}
|
key={index}
|
||||||
className="px-2 py-1 text-sm font-mono text-center bg-background rounded"
|
className="px-2 py-1 text-sm font-mono text-center bg-background rounded"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { api, ApiError } from "@/lib/api";
|
||||||
|
|
||||||
interface TotpRemoveDialogProps {
|
interface TotpRemoveDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -24,16 +27,34 @@ export function TotpRemoveDialog({
|
|||||||
isRequired = false,
|
isRequired = false,
|
||||||
}: TotpRemoveDialogProps) {
|
}: TotpRemoveDialogProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const resetDialog = () => {
|
||||||
|
setPassword("");
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetDialog();
|
||||||
|
}
|
||||||
|
onOpenChange(isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemove = async () => {
|
const handleRemove = async () => {
|
||||||
|
if (!password) {
|
||||||
|
setError("Password is required to disable TOTP");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Call actual API to remove TOTP
|
await api.totp.disable(password);
|
||||||
// await api.users.removeTotpEnrollment();
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Two-factor authentication disabled",
|
title: "Two-factor authentication disabled",
|
||||||
@@ -41,21 +62,31 @@ export function TotpRemoveDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onOpenChange(false);
|
handleClose(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to remove TOTP:", err);
|
console.error("Failed to remove TOTP:", err);
|
||||||
toast({
|
if (err instanceof ApiError) {
|
||||||
title: "Failed to remove TOTP",
|
if (err.type === "INVALID_CREDENTIALS" || err.code === 401) {
|
||||||
description: "An error occurred. Please try again.",
|
setError("Incorrect password. Please try again.");
|
||||||
variant: "destructive",
|
} else {
|
||||||
});
|
setError(err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError("An error occurred. Please try again.");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && password) {
|
||||||
|
handleRemove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={handleClose}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
@@ -74,17 +105,34 @@ export function TotpRemoveDialog({
|
|||||||
You may lose access to certain features if you disable it.
|
You may lose access to certain features if you disable it.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm">
|
|
||||||
Are you sure you want to continue?
|
|
||||||
</p>
|
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password-confirm">Enter your password to confirm</Label>
|
||||||
|
<Input
|
||||||
|
id="password-confirm"
|
||||||
|
type="password"
|
||||||
|
placeholder="Your current password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={isLoading}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-destructive">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => handleClose(false)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -92,12 +140,13 @@ export function TotpRemoveDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
disabled={isLoading}
|
disabled={isLoading || !password}
|
||||||
>
|
>
|
||||||
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||||
Remove TOTP
|
Remove TOTP
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ export interface LoginResponse {
|
|||||||
user: User;
|
user: User;
|
||||||
token: string;
|
token: string;
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
|
requires_totp?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TotpEnrollResponse {
|
||||||
|
secret: string;
|
||||||
|
provisioning_uri: string;
|
||||||
|
qr_code: string; // base64 PNG
|
||||||
|
backup_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TotpStatusResponse {
|
||||||
|
totp_enabled: boolean;
|
||||||
|
verified_at: string | null;
|
||||||
|
backup_codes_remaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TotpVerifyResponse {
|
||||||
|
user: User;
|
||||||
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileResponse {
|
export interface ProfileResponse {
|
||||||
@@ -199,6 +219,46 @@ export const api = {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
totp: {
|
||||||
|
// Initiate TOTP enrollment - returns secret, QR code, and backup codes
|
||||||
|
enroll: () =>
|
||||||
|
request<TotpEnrollResponse>('/auth/totp/enroll', {
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Verify TOTP enrollment with a code from authenticator app
|
||||||
|
verifyEnrollment: (code: string) =>
|
||||||
|
request<{ message: string }>('/auth/totp/verify-enrollment', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Verify TOTP code during login (no auth required - uses session state)
|
||||||
|
verify: (code: string, isBackupCode = false) =>
|
||||||
|
request<TotpVerifyResponse>('/auth/totp/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ code, is_backup_code: isBackupCode }),
|
||||||
|
}, false),
|
||||||
|
|
||||||
|
// Get TOTP status
|
||||||
|
status: () =>
|
||||||
|
request<TotpStatusResponse>('/auth/totp/status'),
|
||||||
|
|
||||||
|
// Disable TOTP (requires password confirmation)
|
||||||
|
disable: (password: string) =>
|
||||||
|
request<{ message: string }>('/auth/totp/disable', {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Regenerate backup codes (requires password confirmation)
|
||||||
|
regenerateBackupCodes: (password: string) =>
|
||||||
|
request<{ backup_codes: string[] }>('/auth/totp/regenerate-backup-codes', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ApiError };
|
export { ApiError };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Lock, Fingerprint, Smartphone, Shield, Plus, CheckCircle, Loader2 } from "lucide-react";
|
import { Lock, Fingerprint, Smartphone, Shield, Plus, CheckCircle, Loader2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -9,7 +9,7 @@ import { AddPasskeyWizard } from "@/components/security/AddPasskeyWizard";
|
|||||||
import { TotpEnrollmentWizard } from "@/components/security/TotpEnrollmentWizard";
|
import { TotpEnrollmentWizard } from "@/components/security/TotpEnrollmentWizard";
|
||||||
import { TotpRemoveDialog } from "@/components/security/TotpRemoveDialog";
|
import { TotpRemoveDialog } from "@/components/security/TotpRemoveDialog";
|
||||||
import { PasswordStrengthMeter, isPasswordValid } from "@/components/auth/PasswordStrengthMeter";
|
import { PasswordStrengthMeter, isPasswordValid } from "@/components/auth/PasswordStrengthMeter";
|
||||||
import { api, ApiError } from "@/lib/api";
|
import { api, ApiError, TotpStatusResponse } from "@/lib/api";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
export default function SecurityPage() {
|
export default function SecurityPage() {
|
||||||
@@ -27,9 +27,31 @@ export default function SecurityPage() {
|
|||||||
|
|
||||||
// TOTP state
|
// TOTP state
|
||||||
const [totpEnabled, setTotpEnabled] = useState(false);
|
const [totpEnabled, setTotpEnabled] = useState(false);
|
||||||
|
const [totpStatus, setTotpStatus] = useState<TotpStatusResponse | null>(null);
|
||||||
|
const [isTotpStatusLoading, setIsTotpStatusLoading] = useState(true);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Fetch TOTP status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTotpStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchTotpStatus = async () => {
|
||||||
|
setIsTotpStatusLoading(true);
|
||||||
|
try {
|
||||||
|
const status = await api.totp.status();
|
||||||
|
setTotpStatus(status);
|
||||||
|
setTotpEnabled(status.totp_enabled);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch TOTP status:", err);
|
||||||
|
// Don't show error toast - just assume TOTP is not enabled
|
||||||
|
setTotpEnabled(false);
|
||||||
|
} finally {
|
||||||
|
setIsTotpStatusLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Mock security data
|
// Mock security data
|
||||||
const security = {
|
const security = {
|
||||||
passwordLastChanged: "3 months ago",
|
passwordLastChanged: "3 months ago",
|
||||||
@@ -234,7 +256,9 @@ export default function SecurityPage() {
|
|||||||
Use an authenticator app for two-factor authentication
|
Use an authenticator app for two-factor authentication
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{totpEnabled ? (
|
{isTotpStatusLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
) : totpEnabled ? (
|
||||||
<Badge className="bg-success/10 text-success border-0">
|
<Badge className="bg-success/10 text-success border-0">
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
Enabled
|
Enabled
|
||||||
@@ -332,6 +356,7 @@ export default function SecurityPage() {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setTotpEnabled(true);
|
setTotpEnabled(true);
|
||||||
setShowTotpEnrollment(false);
|
setShowTotpEnrollment(false);
|
||||||
|
fetchTotpStatus(); // Refresh status after enrollment
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -340,6 +365,7 @@ export default function SecurityPage() {
|
|||||||
onOpenChange={setShowTotpRemove}
|
onOpenChange={setShowTotpRemove}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setTotpEnabled(false);
|
setTotpEnabled(false);
|
||||||
|
setTotpStatus(null);
|
||||||
setShowTotpRemove(false);
|
setShowTotpRemove(false);
|
||||||
}}
|
}}
|
||||||
isRequired={security.policyRequirements.totpRequired}
|
isRequired={security.policyRequirements.totpRequired}
|
||||||
|
|||||||
Reference in New Issue
Block a user