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.
This commit is contained in:
@@ -11,6 +11,7 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { authService } from "@/services/authService";
|
import { authService } from "@/services/authService";
|
||||||
import { AlertCircle, CheckCircle } from "lucide-react";
|
import { AlertCircle, CheckCircle } from "lucide-react";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
// Profile update form schema
|
// Profile update form schema
|
||||||
const profileFormSchema = z.object({
|
const profileFormSchema = z.object({
|
||||||
@@ -33,10 +34,10 @@ interface UpdateProfileFormProps {
|
|||||||
|
|
||||||
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [emailChangeRequested, setEmailChangeRequested] = useState(false);
|
|
||||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
|
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Initialize the form with current user data
|
// Initialize the form with current user data
|
||||||
const form = useForm<ProfileFormValues>({
|
const form = useForm<ProfileFormValues>({
|
||||||
@@ -52,7 +53,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setUpdateError(null);
|
setUpdateError(null);
|
||||||
setUpdateSuccess(null);
|
setUpdateSuccess(null);
|
||||||
setEmailChangeRequested(false);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Submitting profile update with data:", data);
|
console.log("Submitting profile update with data:", data);
|
||||||
@@ -75,19 +75,25 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
// Update user data using the userService
|
// Update user data using the userService
|
||||||
await userService.updateUser(user.id, updateData);
|
await userService.updateUser(user.id, updateData);
|
||||||
|
|
||||||
// Refresh user data in auth context
|
// If email was changed, show success message and auto-logout
|
||||||
await authService.refreshUserData();
|
|
||||||
|
|
||||||
// If email was changed, show a specific message
|
|
||||||
if (isEmailChanged) {
|
if (isEmailChanged) {
|
||||||
setEmailChangeRequested(true);
|
setUpdateSuccess("Email changed successfully! You will be logged out for security reasons. Please log in again with your new email.");
|
||||||
setUpdateSuccess("A verification email has been sent to your new email address. Please check your inbox to complete the change.");
|
|
||||||
toast({
|
toast({
|
||||||
title: "Email verification sent",
|
title: "Email changed successfully",
|
||||||
description: "A verification email has been sent to your new email address. Please check your inbox and follow the instructions to complete the change.",
|
description: "You will be logged out for security reasons. Please log in again with your new email.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-logout after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
authService.logout();
|
||||||
|
navigate("/login");
|
||||||
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
|
// Refresh user data in auth context for other field changes
|
||||||
|
await authService.refreshUserData();
|
||||||
|
|
||||||
setUpdateSuccess("Your profile information has been updated successfully.");
|
setUpdateSuccess("Your profile information has been updated successfully.");
|
||||||
toast({
|
toast({
|
||||||
title: "Profile updated",
|
title: "Profile updated",
|
||||||
@@ -132,16 +138,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{emailChangeRequested && (
|
|
||||||
<Alert className="bg-yellow-50 border-yellow-200 text-yellow-800">
|
|
||||||
<AlertCircle className="h-4 w-4 text-yellow-600" />
|
|
||||||
<AlertDescription>
|
|
||||||
A verification email has been sent to your new email address.
|
|
||||||
Please check your inbox and follow the instructions to complete the change.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -183,7 +179,7 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
{field.value !== user.email && (
|
{field.value !== user.email && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
|
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
|
||||||
import { UserFormValues, NewUserFormValues } from "../userForms";
|
import { UserFormValues, NewUserFormValues } from "../userForms";
|
||||||
import { avatarOptions } from "../avatarOptions";
|
import { avatarOptions } from "../avatarOptions";
|
||||||
|
import { authService } from "@/services/authService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const useUserOperations = (
|
export const useUserOperations = (
|
||||||
fetchUsers: () => Promise<void>,
|
fetchUsers: () => Promise<void>,
|
||||||
@@ -15,6 +17,7 @@ export const useUserOperations = (
|
|||||||
newUserFormReset: (values: any) => void
|
newUserFormReset: (values: any) => void
|
||||||
) => {
|
) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleDeleteUser = async (userToDelete: User | null) => {
|
const handleDeleteUser = async (userToDelete: User | null) => {
|
||||||
if (!userToDelete) return;
|
if (!userToDelete) return;
|
||||||
@@ -47,6 +50,11 @@ export const useUserOperations = (
|
|||||||
setUpdateError(null);
|
setUpdateError(null);
|
||||||
|
|
||||||
try {
|
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
|
// Create update object with only the fields we want to update
|
||||||
const updateData: UpdateUserData = {
|
const updateData: UpdateUserData = {
|
||||||
full_name: data.full_name,
|
full_name: data.full_name,
|
||||||
@@ -57,7 +65,6 @@ export const useUserOperations = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// For avatar, only include if it's different from current one
|
// 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) {
|
if (data.avatar && data.avatar !== currentUser.avatar) {
|
||||||
updateData.avatar = data.avatar;
|
updateData.avatar = data.avatar;
|
||||||
}
|
}
|
||||||
@@ -66,9 +73,26 @@ export const useUserOperations = (
|
|||||||
|
|
||||||
await userService.updateUser(currentUser.id, updateData);
|
await userService.updateUser(currentUser.id, updateData);
|
||||||
|
|
||||||
// After successful update, refresh the auth user data if this is the current user
|
// Handle email change for current user
|
||||||
// In a real app, you'd check if the updated user is the current logged-in 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({
|
toast({
|
||||||
title: "User updated",
|
title: "User updated",
|
||||||
description: `${data.full_name || data.username}'s profile has been updated.`,
|
description: `${data.full_name || data.username}'s profile has been updated.`,
|
||||||
@@ -155,4 +179,4 @@ export const useUserOperations = (
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onAddUser,
|
onAddUser,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -147,22 +147,11 @@ export const userService = {
|
|||||||
if (roleChange) {
|
if (roleChange) {
|
||||||
delete cleanData.role;
|
delete cleanData.role;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle email updates separately
|
// Handle email updates with proper error handling
|
||||||
const hasEmailChange = cleanData.email !== undefined;
|
const hasEmailChange = cleanData.email !== undefined;
|
||||||
const emailToUpdate = hasEmailChange ? cleanData.email : null;
|
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;
|
let updatedUser: User | null = null;
|
||||||
|
|
||||||
// First, determine if this is currently a regular user or superadmin
|
// First, determine if this is currently a regular user or superadmin
|
||||||
@@ -194,7 +183,6 @@ export const userService = {
|
|||||||
...cleanData
|
...cleanData
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to create in the new collection and delete from the old one
|
|
||||||
try {
|
try {
|
||||||
if (targetRole === "superadmin") {
|
if (targetRole === "superadmin") {
|
||||||
// Create in superadmin collection
|
// Create in superadmin collection
|
||||||
@@ -217,7 +205,6 @@ export const userService = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular update without changing collections
|
// Regular update without changing collections
|
||||||
// Only perform the regular update if there are fields to update
|
|
||||||
if (Object.keys(cleanData).length > 0) {
|
if (Object.keys(cleanData).length > 0) {
|
||||||
console.log("Final update payload to PocketBase:", cleanData);
|
console.log("Final update payload to PocketBase:", cleanData);
|
||||||
|
|
||||||
@@ -227,47 +214,31 @@ export const userService = {
|
|||||||
const updatedRecord = await pb.collection(collection).update(id, cleanData);
|
const updatedRecord = await pb.collection(collection).update(id, cleanData);
|
||||||
updatedUser = convertToUserType(updatedRecord, isCurrentlySuperadmin ? "superadmin" : "admin");
|
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) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
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;
|
return updatedUser;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update user:", error);
|
console.error("Failed to update user:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user