diff --git a/frontend/src/components/AccountSettings.jsx b/frontend/src/components/AccountSettings.jsx
index 191564b..8571866 100644
--- a/frontend/src/components/AccountSettings.jsx
+++ b/frontend/src/components/AccountSettings.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import {
Modal,
Badge,
@@ -11,8 +11,10 @@ import {
Text,
PasswordInput,
Box,
+ LoadingOverlay,
} from '@mantine/core';
import { useAuth } from '../contexts/AuthContext';
+import { useProfileSettings } from '../hooks/useProfileSettings';
const AccordionControl = ({ children }) => (
@@ -20,56 +22,172 @@ const AccordionControl = ({ children }) => (
);
-const ProfileSettings = ({ displayName, email }) => (
-
-
-
-
-);
+const ProfileSettings = ({ displayName, email, onUpdate, loading }) => {
+ const [newDisplayName, setNewDisplayName] = useState(displayName || '');
+ const [newEmail, setNewEmail] = useState(email);
+ const [currentPassword, setCurrentPassword] = useState('');
+ const hasEmailChanges = newEmail !== email;
+ const hasDisplayNameChanges = newDisplayName !== displayName;
+ const hasChanges = hasEmailChanges || hasDisplayNameChanges;
-const SecuritySettings = () => (
-
-
-
-
-
- Password must be at least 8 characters long and contain at least one
- uppercase letter, one lowercase letter, one number, and one special
- character.
-
-
-);
+ const handleSave = () => {
+ const updates = {};
+ if (hasDisplayNameChanges) updates.displayName = newDisplayName;
+ if (hasEmailChanges) {
+ updates.email = newEmail;
+ updates.currentPassword = currentPassword;
+ }
+ onUpdate(updates);
+ };
-const DangerZone = () => (
-
-
-
-
-
-);
+ return (
+
+ setNewDisplayName(e.currentTarget.value)}
+ placeholder="Enter display name"
+ />
+ setNewEmail(e.currentTarget.value)}
+ placeholder="Enter email"
+ />
+ {hasEmailChanges && (
+ setCurrentPassword(e.currentTarget.value)}
+ placeholder="Required to change email"
+ required
+ />
+ )}
+ {hasChanges && (
+
+ )}
+
+ );
+};
+
+const SecuritySettings = ({ onUpdate, loading }) => {
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [error, setError] = useState('');
+
+ const handlePasswordChange = () => {
+ if (newPassword !== confirmPassword) {
+ setError('Passwords do not match');
+ return;
+ }
+ if (newPassword.length < 8) {
+ setError('Password must be at least 8 characters long');
+ return;
+ }
+ setError('');
+ onUpdate({ currentPassword, newPassword });
+ };
+
+ const hasChanges = currentPassword && newPassword && confirmPassword;
+
+ return (
+
+ setCurrentPassword(e.currentTarget.value)}
+ placeholder="Enter current password"
+ />
+ setNewPassword(e.currentTarget.value)}
+ placeholder="Enter new password"
+ />
+ setConfirmPassword(e.currentTarget.value)}
+ placeholder="Confirm new password"
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+ Password must be at least 8 characters long
+
+ {hasChanges && (
+
+ )}
+
+ );
+};
+
+const DangerZone = ({ onDelete, loading }) => {
+ const [password, setPassword] = useState('');
+ const [confirmDelete, setConfirmDelete] = useState(false);
+
+ const handleDelete = () => {
+ if (confirmDelete && password) {
+ onDelete(password);
+ } else {
+ setConfirmDelete(true);
+ }
+ };
+
+ return (
+
+ {confirmDelete && (
+ setPassword(e.currentTarget.value)}
+ placeholder="Enter password to confirm"
+ required
+ />
+ )}
+
+
+
+
+ );
+};
const AccountSettings = ({ opened, onClose }) => {
- const { user } = useAuth();
+ const { user, logout, refreshUser } = useAuth();
+ const { loading, updateProfile, deleteAccount } = useProfileSettings();
+ const [activeSection, setActiveSection] = useState(['profile']);
+
+ const handleProfileUpdate = async (updates) => {
+ const result = await updateProfile(updates);
+ if (result.success) {
+ await refreshUser();
+ }
+ };
+
+ const handleDelete = async (password) => {
+ const result = await deleteAccount(password);
+ if (result.success) {
+ onClose();
+ logout();
+ }
+ };
+
return (
{
centered
size="lg"
>
+
-
- Changes are currently disabled
-
-
({
control: {
@@ -104,11 +220,6 @@ const AccountSettings = ({ opened, onClose }) => {
: theme.colors.gray[0],
},
},
- chevron: {
- '&[data-rotate]': {
- transform: 'rotate(180deg)',
- },
- },
})}
>
@@ -117,6 +228,8 @@ const AccountSettings = ({ opened, onClose }) => {
@@ -124,23 +237,25 @@ const AccountSettings = ({ opened, onClose }) => {
Security
-
+
Danger Zone
-
+
-
diff --git a/frontend/src/components/UserMenu.jsx b/frontend/src/components/UserMenu.jsx
index bca0295..6d50b44 100644
--- a/frontend/src/components/UserMenu.jsx
+++ b/frontend/src/components/UserMenu.jsx
@@ -8,7 +8,7 @@ import {
Text,
Divider,
} from '@mantine/core';
-import { IconUser, IconLogout, IconUserCircle } from '@tabler/icons-react';
+import { IconUser, IconLogout, IconSettings } from '@tabler/icons-react';
import { useAuth } from '../contexts/AuthContext';
import AccountSettings from './AccountSettings';
@@ -76,7 +76,7 @@ const UserMenu = () => {
})}
>
-
+
Account Settings
diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx
index 2a716d8..a93d0c5 100644
--- a/frontend/src/contexts/AuthContext.jsx
+++ b/frontend/src/contexts/AuthContext.jsx
@@ -89,6 +89,15 @@ export const AuthProvider = ({ children }) => {
}
}, [logout]);
+ const refreshUser = useCallback(async () => {
+ try {
+ const userData = await authApi.getCurrentUser();
+ setUser(userData);
+ } catch (error) {
+ console.error('Failed to refresh user data:', error);
+ }
+ }, []);
+
const value = {
user,
loading,
@@ -96,6 +105,7 @@ export const AuthProvider = ({ children }) => {
login,
logout,
refreshToken,
+ refreshUser,
};
return {children};
diff --git a/frontend/src/hooks/useProfileSettings.js b/frontend/src/hooks/useProfileSettings.js
new file mode 100644
index 0000000..381e784
--- /dev/null
+++ b/frontend/src/hooks/useProfileSettings.js
@@ -0,0 +1,71 @@
+import { useState, useCallback } from 'react';
+import { notifications } from '@mantine/notifications';
+import { updateProfile, deleteProfile } from '../services/api';
+
+export function useProfileSettings() {
+ const [loading, setLoading] = useState(false);
+
+ const handleProfileUpdate = useCallback(async (updates) => {
+ setLoading(true);
+ try {
+ const updatedUser = await updateProfile(updates);
+
+ notifications.show({
+ title: 'Success',
+ message: 'Profile updated successfully',
+ color: 'green',
+ });
+
+ return { success: true, user: updatedUser };
+ } catch (error) {
+ let errorMessage = 'Failed to update profile';
+
+ if (error.message.includes('password')) {
+ errorMessage = 'Current password is incorrect';
+ } else if (error.message.includes('email')) {
+ errorMessage = 'Email is already in use';
+ }
+
+ notifications.show({
+ title: 'Error',
+ message: errorMessage,
+ color: 'red',
+ });
+
+ return { success: false, error: error.message };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const handleAccountDeletion = useCallback(async (password) => {
+ setLoading(true);
+ try {
+ await deleteProfile(password);
+
+ notifications.show({
+ title: 'Success',
+ message: 'Account deleted successfully',
+ color: 'green',
+ });
+
+ return { success: true };
+ } catch (error) {
+ notifications.show({
+ title: 'Error',
+ message: error.message || 'Failed to delete account',
+ color: 'red',
+ });
+
+ return { success: false, error: error.message };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ return {
+ loading,
+ updateProfile: handleProfileUpdate,
+ deleteAccount: handleAccountDeletion,
+ };
+}
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index ad7e84f..dc8c1df 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -1,6 +1,22 @@
import { API_BASE_URL } from '../utils/constants';
import { apiCall } from './authApi';
+export const updateProfile = async (updates) => {
+ const response = await apiCall(`${API_BASE_URL}/profile`, {
+ method: 'PUT',
+ body: JSON.stringify(updates),
+ });
+ return response.json();
+};
+
+export const deleteProfile = async (password) => {
+ const response = await apiCall(`${API_BASE_URL}/profile`, {
+ method: 'DELETE',
+ body: JSON.stringify({ password }),
+ });
+ return response.json();
+};
+
export const fetchLastWorkspaceName = async () => {
const response = await apiCall(`${API_BASE_URL}/workspaces/last`);
return response.json();