import React, { useState, useEffect } from 'react'; import { Layout } from '../components/Layout'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import * as api from '../api'; import type { Collection } from '../types'; import { User, Lock, Save, X, Shield } from 'lucide-react'; import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, USER_KEY } from '../utils/impersonation'; export const Profile: React.FC = () => { const { user: authUser, logout, authEnabled } = useAuth(); const navigate = useNavigate(); const isAdmin = authUser?.role === 'ADMIN'; const mustResetPassword = Boolean(authUser?.mustResetPassword); const [collections, setCollections] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [registrationEnabled, setRegistrationEnabled] = useState(null); const [registrationLoading, setRegistrationLoading] = useState(false); // User info state const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [showEmailForm, setShowEmailForm] = useState(false); const [emailCurrentPassword, setEmailCurrentPassword] = useState(''); const [emailLoading, setEmailLoading] = useState(false); // Password change state const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [showPasswordForm, setShowPasswordForm] = useState(false); useEffect(() => { if (authEnabled === false) { navigate('/settings', { replace: true }); return; } const fetchData = async () => { try { const collectionsData = await api.getCollections(); setCollections(collectionsData); // Fetch user info if (authUser) { setName(authUser.name); setEmail(authUser.email); } if (isAdmin) { const statusResponse = await api.api.get<{ registrationEnabled: boolean }>('/auth/status'); setRegistrationEnabled(statusResponse.data.registrationEnabled); } else { setRegistrationEnabled(null); } } catch (err) { console.error('Failed to fetch data:', err); } }; fetchData(); }, [authEnabled, authUser, isAdmin, navigate]); useEffect(() => { if (mustResetPassword) { setShowPasswordForm(true); } }, [mustResetPassword]); const handleToggleRegistration = async () => { if (!isAdmin || registrationEnabled === null) return; setRegistrationLoading(true); setError(''); setSuccess(''); try { const response = await api.api.post<{ registrationEnabled: boolean }>('/auth/registration/toggle', { enabled: !registrationEnabled, }); setRegistrationEnabled(response.data.registrationEnabled); setSuccess(response.data.registrationEnabled ? 'Registration enabled' : 'Registration disabled'); } catch (err: unknown) { let message = 'Failed to update registration setting'; if (api.isAxiosError(err)) { if (err.response?.data?.message) { message = err.response.data.message; } else if (err.response?.data?.error) { message = err.response.data.error; } } setError(message); } finally { setRegistrationLoading(false); } }; const handleSelectCollection = (id: string | null | undefined) => { if (id === undefined) navigate('/'); else if (id === null) navigate('/collections?id=unorganized'); else navigate(`/collections?id=${id}`); }; const handleCreateCollection = async (name: string) => { await api.createCollection(name); const newCollections = await api.getCollections(); setCollections(newCollections); }; const handleEditCollection = async (id: string, name: string) => { setCollections(prev => prev.map(c => c.id === id ? { ...c, name } : c)); await api.updateCollection(id, name); }; const handleDeleteCollection = async (id: string) => { setCollections(prev => prev.filter(c => c.id !== id)); await api.deleteCollection(id); }; const handleUpdateName = async () => { if (mustResetPassword) { setError('You must reset your password before updating your profile'); return; } if (!name.trim()) { setError('Name cannot be empty'); return; } setLoading(true); setError(''); setSuccess(''); try { const response = await api.api.put<{ user: { id: string; email: string; name: string; createdAt: string; updatedAt: string } }>('/auth/profile', { name: name.trim() }); setSuccess('Name updated successfully'); // Update auth context - refresh user data if (response.data?.user) { // Update localStorage with new user data localStorage.setItem('excalidash-user', JSON.stringify(response.data.user)); // Reload to update auth context setTimeout(() => window.location.reload(), 500); } } catch (err: unknown) { let message = 'Failed to update name'; if (api.isAxiosError(err)) { if (err.response?.data?.message) { message = err.response.data.message; } else if (err.response?.data?.error) { message = err.response.data.error; } } setError(message); } finally { setLoading(false); } }; const handleChangePassword = async () => { if (!currentPassword || !newPassword || !confirmPassword) { setError('All password fields are required'); return; } if (newPassword.length < 8) { setError('New password must be at least 8 characters long'); return; } if (newPassword !== confirmPassword) { setError('New passwords do not match'); return; } setLoading(true); setError(''); setSuccess(''); try { await api.api.post('/auth/change-password', { currentPassword, newPassword, }); setSuccess('Password changed successfully'); setShowPasswordForm(false); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); // Logout user to force re-login with new password setTimeout(() => { logout(); navigate('/login'); }, 2000); } catch (err: unknown) { let message = 'Failed to change password'; if (api.isAxiosError(err)) { if (err.response?.data?.message) { message = err.response.data.message; } else if (err.response?.data?.error) { message = err.response.data.error; } } setError(message); } finally { setLoading(false); } }; const handleUpdateEmail = async () => { if (mustResetPassword) { setError('You must reset your password before changing your email'); return; } if (!email.trim()) { setError('Email cannot be empty'); return; } if (!emailCurrentPassword) { setError('Current password is required to change email'); return; } setEmailLoading(true); setError(''); setSuccess(''); try { const response = await api.api.put<{ user: { id: string; email: string; name: string; createdAt: string; updatedAt: string }; accessToken: string; refreshToken: string; }>('/auth/email', { email: email.trim(), currentPassword: emailCurrentPassword, }); localStorage.setItem(ACCESS_TOKEN_KEY, response.data.accessToken); localStorage.setItem(REFRESH_TOKEN_KEY, response.data.refreshToken); localStorage.setItem(USER_KEY, JSON.stringify(response.data.user)); setSuccess('Email updated successfully'); setShowEmailForm(false); setEmailCurrentPassword(''); setTimeout(() => window.location.reload(), 500); } catch (err: unknown) { let message = 'Failed to update email'; if (api.isAxiosError(err)) { if (err.response?.data?.message) { message = err.response.data.message; } else if (err.response?.data?.error) { message = err.response.data.error; } } setError(message); } finally { setEmailLoading(false); } }; return (

Profile

{/* Success/Error Messages */} {success && (

{success}

)} {error && (

{error}

)}
{/* Personal Information Section */}

Personal Information

{mustResetPassword && (

Password reset required

Change your password below before using ExcaliDash.

)}
setEmail(e.target.value)} disabled={!showEmailForm} className={ showEmailForm ? "flex-1 px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 font-medium" : "flex-1 px-4 py-3 bg-slate-50 dark:bg-neutral-800 border-2 border-slate-200 dark:border-neutral-700 rounded-xl text-slate-600 dark:text-neutral-400 cursor-not-allowed" } /> {!showEmailForm && ( )}
{showEmailForm && (
setEmailCurrentPassword(e.target.value)} className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 font-medium" placeholder="Enter current password" />
)}
setName(e.target.value)} className="flex-1 px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 font-medium" placeholder="Your name" />
{/* Admin Settings */} {isAdmin && (

Admin Settings

User registration

{registrationEnabled === null ? 'Loading…' : registrationEnabled ? 'New users can create accounts.' : 'Registration is disabled.'}

)} {/* Password Change Section */}

Change Password

{!showPasswordForm && !mustResetPassword && ( )}
{showPasswordForm && (
setCurrentPassword(e.target.value)} className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-rose-500 dark:focus:ring-rose-400 font-medium" placeholder="Enter current password" />
setNewPassword(e.target.value)} className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-rose-500 dark:focus:ring-rose-400 font-medium" placeholder="Enter new password (min 8 characters)" />
setConfirmPassword(e.target.value)} className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-rose-500 dark:focus:ring-rose-400 font-medium" placeholder="Confirm new password" />
{!mustResetPassword && ( )}
)}
); };