veza/apps/web/src/features/settings/components/AccountSettings.tsx

292 lines
9.7 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { useAuthStore } from '@/stores/auth';
import { apiClient } from '@/services/api/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useToast } from '@/hooks/useToast';
import { AlertCircle, Trash2, Key, Download } from 'lucide-react';
import { Dialog } from '@/components/ui/dialog';
// FE-PAGE-004: Complete Settings page implementation - Account Settings
export function AccountSettings() {
const { logout } = useAuthStore();
const toast = useToast();
const [isChangingPassword, setIsChangingPassword] = useState(false);
const [isDeletingAccount, setIsDeletingAccount] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
// Password change form
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
// Account deletion form
const [deletePassword, setDeletePassword] = useState('');
const [deleteReason, setDeleteReason] = useState('');
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setPasswordError('');
if (newPassword !== confirmPassword) {
setPasswordError('New passwords do not match');
return;
}
if (newPassword.length < 12) {
setPasswordError('Password must be at least 12 characters long');
return;
}
try {
setIsChangingPassword(true);
await apiClient.put('/users/me/password', {
current_password: currentPassword,
new_password: newPassword,
});
toast.success('Password changed successfully');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} catch (error: any) {
const errorMessage =
error.response?.data?.error ||
error.response?.data?.message ||
error.message ||
'Failed to change password';
setPasswordError(errorMessage);
toast.error(errorMessage);
} finally {
setIsChangingPassword(false);
}
};
const handleDeleteAccount = async () => {
if (deleteConfirmText !== 'DELETE') {
toast.error('Please type DELETE to confirm');
return;
}
try {
setIsDeletingAccount(true);
await apiClient.delete('/users/me', {
data: {
password: deletePassword,
reason: deleteReason,
confirm_text: deleteConfirmText,
},
});
toast.success('Account deletion requested. You will be logged out.');
setTimeout(() => {
logout();
window.location.href = '/login';
}, 2000);
} catch (error: any) {
const errorMessage =
error.response?.data?.error ||
error.response?.data?.message ||
error.message ||
'Failed to delete account';
toast.error(errorMessage);
} finally {
setIsDeletingAccount(false);
setIsDeleteDialogOpen(false);
}
};
const handleExportData = async () => {
try {
const response = await apiClient.get('/users/me/export', {
responseType: 'blob',
});
// Create download link
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `veza-data-export-${new Date().toISOString()}.json`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Data export started');
} catch (error: any) {
const errorMessage =
error.response?.data?.error ||
error.message ||
'Failed to export data';
toast.error(errorMessage);
}
};
return (
<div className="space-y-6">
{/* FE-PAGE-004: Change Password Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
Change Password
</CardTitle>
<CardDescription>
Update your password to keep your account secure
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleChangePassword} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current-password">Current Password</Label>
<Input
id="current-password"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<Input
id="new-password"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={12}
/>
<p className="text-xs text-muted-foreground">
Password must be at least 12 characters long
</p>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">Confirm New Password</Label>
<Input
id="confirm-password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={12}
/>
</div>
{passwordError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{passwordError}</AlertDescription>
</Alert>
)}
<Button type="submit" disabled={isChangingPassword}>
{isChangingPassword ? 'Changing...' : 'Change Password'}
</Button>
</form>
</CardContent>
</Card>
{/* FE-PAGE-004: Data Export Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Download className="h-5 w-5" />
Data Export
</CardTitle>
<CardDescription>
Download a copy of your data (GDPR)
</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" onClick={handleExportData}>
<Download className="mr-2 h-4 w-4" />
Export My Data
</Button>
</CardContent>
</Card>
{/* FE-PAGE-004: Delete Account Section */}
<Card className="border-destructive">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<Trash2 className="h-5 w-5" />
Delete Account
</CardTitle>
<CardDescription>
Permanently delete your account and all associated data
</CardDescription>
</CardHeader>
<CardContent>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
This action cannot be undone. All your data will be permanently
deleted.
</AlertDescription>
</Alert>
<Dialog
open={isDeleteDialogOpen}
onClose={() => setIsDeleteDialogOpen(false)}
title="Are you absolutely sure?"
variant="alert"
onConfirm={handleDeleteAccount}
confirmLabel={isDeletingAccount ? 'Deleting...' : 'Delete Account'}
cancelLabel="Cancel"
size="lg"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
This will permanently delete your account and all associated
data. This action cannot be undone.
</p>
<div className="space-y-2">
<Label htmlFor="delete-password">Enter your password</Label>
<Input
id="delete-password"
type="password"
value={deletePassword}
onChange={(e) => setDeletePassword(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="delete-reason">Reason (optional)</Label>
<Input
id="delete-reason"
value={deleteReason}
onChange={(e) => setDeleteReason(e.target.value)}
placeholder="Why are you deleting your account?"
/>
</div>
<div className="space-y-2">
<Label htmlFor="delete-confirm">
Type <strong>DELETE</strong> to confirm
</Label>
<Input
id="delete-confirm"
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
required
placeholder="DELETE"
/>
</div>
</div>
</Dialog>
<Button
variant="destructive"
className="mt-4"
onClick={() => setIsDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Account
</Button>
</CardContent>
</Card>
</div>
);
}