From 0b9bc76d37861718408ff06c7aa577857b48f448 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 29 May 2025 20:41:00 +0800 Subject: [PATCH] Fix: Improve email change flow and auto-logout - Improved the email change flow in user management and profile dashboards. - Added a success message upon successful email change. - Implemented auto-logout after a successful email change. - Fixed potential errors during the email change process. --- .../components/profile/UpdateProfileForm.tsx | 38 +++++------ .../hooks/useUserOperations.ts | 32 +++++++-- application/src/services/userService.ts | 67 ++++++------------- 3 files changed, 64 insertions(+), 73 deletions(-) diff --git a/application/src/components/profile/UpdateProfileForm.tsx b/application/src/components/profile/UpdateProfileForm.tsx index ac601ec..05ef7ff 100644 --- a/application/src/components/profile/UpdateProfileForm.tsx +++ b/application/src/components/profile/UpdateProfileForm.tsx @@ -11,6 +11,7 @@ import { useToast } from "@/hooks/use-toast"; import { authService } from "@/services/authService"; import { AlertCircle, CheckCircle } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useNavigate } from "react-router-dom"; // Profile update form schema const profileFormSchema = z.object({ @@ -33,10 +34,10 @@ interface UpdateProfileFormProps { export function UpdateProfileForm({ user }: UpdateProfileFormProps) { const [isSubmitting, setIsSubmitting] = useState(false); - const [emailChangeRequested, setEmailChangeRequested] = useState(false); const [updateError, setUpdateError] = useState(null); const [updateSuccess, setUpdateSuccess] = useState(null); const { toast } = useToast(); + const navigate = useNavigate(); // Initialize the form with current user data const form = useForm({ @@ -52,7 +53,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) { setIsSubmitting(true); setUpdateError(null); setUpdateSuccess(null); - setEmailChangeRequested(false); try { console.log("Submitting profile update with data:", data); @@ -75,19 +75,25 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) { // Update user data using the userService await userService.updateUser(user.id, updateData); - // Refresh user data in auth context - await authService.refreshUserData(); - - // If email was changed, show a specific message + // If email was changed, show success message and auto-logout if (isEmailChanged) { - setEmailChangeRequested(true); - setUpdateSuccess("A verification email has been sent to your new email address. Please check your inbox to complete the change."); + setUpdateSuccess("Email changed successfully! You will be logged out for security reasons. Please log in again with your new email."); + toast({ - title: "Email verification sent", - description: "A verification email has been sent to your new email address. Please check your inbox and follow the instructions to complete the change.", + title: "Email changed successfully", + description: "You will be logged out for security reasons. Please log in again with your new email.", variant: "default", }); + + // Auto-logout after 3 seconds + setTimeout(() => { + authService.logout(); + navigate("/login"); + }, 3000); } else { + // Refresh user data in auth context for other field changes + await authService.refreshUserData(); + setUpdateSuccess("Your profile information has been updated successfully."); toast({ title: "Profile updated", @@ -132,16 +138,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) { )} - - {emailChangeRequested && ( - - - - A verification email has been sent to your new email address. - Please check your inbox and follow the instructions to complete the change. - - - )} {field.value !== user.email && (

- Changing your email requires verification. A verification email will be sent. + Changing your email will log you out for security reasons. You will need to log in again with your new email.

)} diff --git a/application/src/components/settings/user-management/hooks/useUserOperations.ts b/application/src/components/settings/user-management/hooks/useUserOperations.ts index 33537b5..1882fb1 100644 --- a/application/src/components/settings/user-management/hooks/useUserOperations.ts +++ b/application/src/components/settings/user-management/hooks/useUserOperations.ts @@ -4,6 +4,8 @@ import { useToast } from "@/hooks/use-toast"; import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService"; import { UserFormValues, NewUserFormValues } from "../userForms"; import { avatarOptions } from "../avatarOptions"; +import { authService } from "@/services/authService"; +import { useNavigate } from "react-router-dom"; export const useUserOperations = ( fetchUsers: () => Promise, @@ -15,6 +17,7 @@ export const useUserOperations = ( newUserFormReset: (values: any) => void ) => { const { toast } = useToast(); + const navigate = useNavigate(); const handleDeleteUser = async (userToDelete: User | null) => { if (!userToDelete) return; @@ -47,6 +50,11 @@ export const useUserOperations = ( setUpdateError(null); try { + // Get current logged-in user to check if we're editing ourselves + const loggedInUser = authService.getCurrentUser(); + const isEditingSelf = loggedInUser?.id === currentUser.id; + const isEmailChanged = data.email !== currentUser.email; + // Create update object with only the fields we want to update const updateData: UpdateUserData = { full_name: data.full_name, @@ -57,7 +65,6 @@ export const useUserOperations = ( }; // For avatar, only include if it's different from current one - // Note: We're still sending the avatar path, but our updated userService will handle it properly if (data.avatar && data.avatar !== currentUser.avatar) { updateData.avatar = data.avatar; } @@ -66,9 +73,26 @@ export const useUserOperations = ( await userService.updateUser(currentUser.id, updateData); - // After successful update, refresh the auth user data if this is the current user - // In a real app, you'd check if the updated user is the current logged-in user + // Handle email change for current user + if (isEditingSelf && isEmailChanged) { + toast({ + title: "Email changed successfully", + description: "You will be logged out for security reasons. Please log in again with your new email.", + variant: "default", + }); + + setIsDialogOpen(false); + + // Auto-logout after 2 seconds if editing own email + setTimeout(() => { + authService.logout(); + navigate("/login"); + }, 2000); + + return; // Don't continue with normal flow + } + // Normal success flow for other users or non-email changes toast({ title: "User updated", description: `${data.full_name || data.username}'s profile has been updated.`, @@ -155,4 +179,4 @@ export const useUserOperations = ( onSubmit, onAddUser, }; -}; +}; \ No newline at end of file diff --git a/application/src/services/userService.ts b/application/src/services/userService.ts index 7193c68..73e41bb 100644 --- a/application/src/services/userService.ts +++ b/application/src/services/userService.ts @@ -147,22 +147,11 @@ export const userService = { if (roleChange) { delete cleanData.role; } - - // Handle email updates separately + + // Handle email updates with proper error handling const hasEmailChange = cleanData.email !== undefined; const emailToUpdate = hasEmailChange ? cleanData.email : null; - // Remove email from regular update if it's being changed - if (hasEmailChange) { - console.log("Email change detected, will handle separately:", emailToUpdate); - delete cleanData.email; - - // For email changes, we should always set emailVisibility - if (cleanData.emailVisibility === undefined) { - cleanData.emailVisibility = true; - } - } - let updatedUser: User | null = null; // First, determine if this is currently a regular user or superadmin @@ -194,7 +183,6 @@ export const userService = { ...cleanData }; - // We need to create in the new collection and delete from the old one try { if (targetRole === "superadmin") { // Create in superadmin collection @@ -217,7 +205,6 @@ export const userService = { } } else { // Regular update without changing collections - // Only perform the regular update if there are fields to update if (Object.keys(cleanData).length > 0) { console.log("Final update payload to PocketBase:", cleanData); @@ -227,47 +214,31 @@ export const userService = { const updatedRecord = await pb.collection(collection).update(id, cleanData); updatedUser = convertToUserType(updatedRecord, isCurrentlySuperadmin ? "superadmin" : "admin"); - console.log("PocketBase update response for regular fields:", updatedUser); + console.log("PocketBase update response:", updatedUser); + + // If email was updated successfully, show success message + if (hasEmailChange) { + console.log("Email updated successfully to:", emailToUpdate); + } + } catch (error) { - console.error("Error updating user regular fields:", error); + console.error("Error updating user:", error); + + // Provide more specific error messages for email issues + if (hasEmailChange && error instanceof Error) { + if (error.message.includes("email")) { + throw new Error("Email update failed. The email address may already be in use or invalid."); + } + } + throw error; } } else { - // If no other fields to update, get the current user + // If no fields to update, get the current user updatedUser = await this.getUser(id); } } - // Now handle email change separately if needed - if (emailToUpdate && updatedUser) { - try { - console.log("Processing email change request for new email:", emailToUpdate); - - // For email changes, we need to directly update the email field instead of using requestEmailChange - // This is because requestEmailChange has permission issues when cross-collection requests are made - const collection = updatedUser.role === "superadmin" ? '_superusers' : 'users'; - - console.log(`Updating email directly in ${collection} collection`); - - // Update the email directly in the user record - const emailUpdateData = { - email: emailToUpdate, - emailVisibility: true, - verified: false // Reset verification status when email changes - }; - - const updatedRecord = await pb.collection(collection).update(id, emailUpdateData); - updatedUser = convertToUserType(updatedRecord, updatedUser.role === "superadmin" ? "superadmin" : "admin"); - - console.log("Email updated successfully:", updatedUser); - - } catch (error) { - console.error("Failed to update email:", error); - // Don't throw error for email update failure, just log it - console.log("Email update failed, but other fields were updated successfully"); - } - } - return updatedUser; } catch (error) { console.error("Failed to update user:", error);