Fix: Email change for both admin and superadmin

The email change functionality was updated to support both admin and superadmin roles. The code was adjusted to correctly handle email change requests for users in both the admin and super-admin
This commit is contained in:
Tola Leng
2025-05-24 02:55:29 +08:00
parent b845e46231
commit dfe052aba6
+197 -39
View File
@@ -1,4 +1,3 @@
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
export interface User { export interface User {
@@ -38,20 +37,54 @@ export interface UpdateUserData {
isActive?: boolean; isActive?: boolean;
} }
// Helper function to convert PocketBase record to User type
const convertToUserType = (record: any, role: string = "admin"): User => {
return {
id: record.id,
created: record.created,
updated: record.updated,
username: record.username,
email: record.email,
emailVisibility: record.emailVisibility,
verified: record.verified,
full_name: record.full_name,
avatar: record.avatar,
role: role,
isActive: record.isActive,
status: record.status,
};
};
export const userService = { export const userService = {
async getUsers(): Promise<User[] | null> { async getUsers(): Promise<User[] | null> {
try { try {
console.log("Calling getUsers API"); console.log("Calling getUsers API");
const result = await pb.collection('users').getList(1, 50, {
// Get both regular users and superadmins
const regularUsers = await pb.collection('users').getList(1, 50, {
sort: 'created', sort: 'created',
}); });
console.log("Users API response:", result); let superadminUsers: any = [];
try {
if (result.items.length > 0) { // Try to get superadmin users if exists
return result.items as unknown as User[]; superadminUsers = await pb.collection('_superusers').getList(1, 50, {
sort: 'created',
});
console.log("Successfully fetched superadmin users:", superadminUsers);
} catch (error) {
console.log("No superadmin collection or access rights:", error);
} }
return [];
// Combine both user types and mark superadmins
const allUsers = [
...regularUsers.items.map((user: any) => convertToUserType(user, user.role || "admin")),
...superadminUsers.items.map((user: any) => convertToUserType(user, "superadmin"))
];
console.log("Combined users list:", allUsers);
return allUsers;
} catch (error) { } catch (error) {
console.error("Failed to fetch users:", error); console.error("Failed to fetch users:", error);
return null; return null;
@@ -61,9 +94,25 @@ export const userService = {
async getUser(id: string): Promise<User | null> { async getUser(id: string): Promise<User | null> {
try { try {
console.log(`Fetching user with ID: ${id}`); console.log(`Fetching user with ID: ${id}`);
const result = await pb.collection('users').getOne(id);
console.log("User fetch result:", result); // Try fetching from regular users first
return result as unknown as User; try {
const user = await pb.collection('users').getOne(id);
console.log("User fetch result (regular user):", user);
return convertToUserType(user, user.role || "admin");
} catch (error) {
console.log("User not found in regular users, trying superadmin collection");
}
// If not found, try in superadmins
try {
const user = await pb.collection('_superusers').getOne(id);
console.log("User fetch result (superadmin):", user);
return convertToUserType(user, "superadmin");
} catch (error) {
console.log("User not found in superadmin collection either");
return null;
}
} catch (error) { } catch (error) {
console.error(`Failed to fetch user ${id}:`, error); console.error(`Failed to fetch user ${id}:`, error);
return null; return null;
@@ -90,6 +139,15 @@ export const userService = {
return currentUser; return currentUser;
} }
// Special handling for role changes between admin and superadmin
const roleChange = cleanData.role !== undefined;
const targetRole = roleChange ? cleanData.role : null;
// Remove role from regular update if it's being changed
if (roleChange) {
delete cleanData.role;
}
// Handle email updates separately // Handle email updates separately
const hasEmailChange = cleanData.email !== undefined; const hasEmailChange = cleanData.email !== undefined;
const emailToUpdate = hasEmailChange ? cleanData.email : null; const emailToUpdate = hasEmailChange ? cleanData.email : null;
@@ -107,39 +165,106 @@ export const userService = {
let updatedUser: User | null = null; let updatedUser: User | null = null;
// Only perform the regular update if there are fields to update // First, determine if this is currently a regular user or superadmin
if (Object.keys(cleanData).length > 0) { let isCurrentlySuperadmin = false;
console.log("Final update payload to PocketBase:", cleanData); try {
await pb.collection('_superusers').getOne(id);
isCurrentlySuperadmin = true;
} catch (error) {
// Not in superadmin collection
}
// If we're changing the role and need to move the user between collections
if (roleChange && ((isCurrentlySuperadmin && targetRole !== "superadmin") ||
(!isCurrentlySuperadmin && targetRole === "superadmin"))) {
// Get the full user data before changing collections
const currentUser = await this.getUser(id);
if (!currentUser) {
throw new Error("User not found during role change");
}
// Prepare user data for transfer
const transferData = {
username: currentUser.username,
email: currentUser.email,
emailVisibility: currentUser.emailVisibility || true,
full_name: currentUser.full_name || "",
avatar: currentUser.avatar || "",
isActive: currentUser.isActive !== false,
...cleanData
};
// We need to create in the new collection and delete from the old one
try { try {
// Use PocketBase's update method for the regular fields if (targetRole === "superadmin") {
updatedUser = await pb.collection('users').update(id, cleanData) as unknown as User; // Create in superadmin collection
console.log("PocketBase update response for regular fields:", updatedUser); const newSuperUser = await pb.collection('_superusers').create(transferData);
// Delete from regular users
await pb.collection('users').delete(id);
updatedUser = convertToUserType(newSuperUser, "superadmin");
} else {
// Create in regular users collection
const newRegularUser = await pb.collection('users').create(transferData);
// Delete from superadmin
await pb.collection('_superusers').delete(id);
updatedUser = convertToUserType(newRegularUser, "admin");
}
console.log("User transferred between collections due to role change");
} catch (error) { } catch (error) {
console.error("Error updating user regular fields:", error); console.error("Failed to transfer user between collections:", error);
throw error; throw new Error("Failed to change user role: " + (error instanceof Error ? error.message : "Unknown error"));
} }
} else { } else {
// If no other fields to update, get the current user // Regular update without changing collections
updatedUser = await this.getUser(id); // 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);
try {
// Use the appropriate collection
const collection = isCurrentlySuperadmin ? '_superusers' : 'users';
const updatedRecord = await pb.collection(collection).update(id, cleanData);
updatedUser = convertToUserType(updatedRecord, isCurrentlySuperadmin ? "superadmin" : "admin");
console.log("PocketBase update response for regular fields:", updatedUser);
} catch (error) {
console.error("Error updating user regular fields:", error);
throw error;
}
} else {
// If no other fields to update, get the current user
updatedUser = await this.getUser(id);
}
} }
// Now handle email change separately if needed // Now handle email change separately if needed
if (emailToUpdate) { if (emailToUpdate && updatedUser) {
try { try {
console.log("Processing email change request for new email:", emailToUpdate); console.log("Processing email change request for new email:", emailToUpdate);
// Use the proper API endpoint for email change requests // For email changes, we need to directly update the email field instead of using requestEmailChange
const emailChangeResponse = await pb.collection('users').requestEmailChange(emailToUpdate); // This is because requestEmailChange has permission issues when cross-collection requests are made
const collection = updatedUser.role === "superadmin" ? '_superusers' : 'users';
console.log("Email change request response:", emailChangeResponse); console.log(`Updating email directly in ${collection} collection`);
console.log("Email verification has been sent to:", emailToUpdate);
// 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);
// The email won't be updated immediately as verification is required
// We'll return the current user data, and email will be updated after verification
} catch (error) { } catch (error) {
console.error("Failed to request email change:", error); console.error("Failed to update email:", error);
throw new Error(`Failed to request email change: ${error instanceof Error ? error.message : "Unknown error"}`); // Don't throw error for email update failure, just log it
console.log("Email update failed, but other fields were updated successfully");
} }
} }
@@ -152,8 +277,22 @@ export const userService = {
async deleteUser(id: string): Promise<boolean> { async deleteUser(id: string): Promise<boolean> {
try { try {
await pb.collection('users').delete(id); // Try to delete from regular users first
return true; try {
await pb.collection('users').delete(id);
return true;
} catch (error) {
console.log("User not found in regular users, trying superadmin collection");
}
// If not found, try deleting from superadmin collection
try {
await pb.collection('_superusers').delete(id);
return true;
} catch (error) {
console.error("Failed to delete user from either collection:", error);
return false;
}
} catch (error) { } catch (error) {
console.error("Failed to delete user:", error); console.error("Failed to delete user:", error);
return false; return false;
@@ -162,16 +301,35 @@ export const userService = {
async createUser(data: CreateUserData): Promise<User | null> { async createUser(data: CreateUserData): Promise<User | null> {
try { try {
// Handle the avatar field for static files // Create a clean data object without avatar field if it's a URL
if (data.avatar && typeof data.avatar === 'string' && data.avatar.startsWith('/upload/profile/')) { // PocketBase requires actual file uploads for avatar, not URLs
// Remove avatar field for new users if it's a local path const cleanData = { ...data };
// PocketBase requires actual file uploads, not paths
console.log("Removing local avatar path for new user"); // Remove avatar if it's a URL (we'll handle this differently in the future)
delete data.avatar; if (cleanData.avatar && typeof cleanData.avatar === 'string') {
// Check if it's an external URL (not a file reference)
if (cleanData.avatar.startsWith('http') ||
cleanData.avatar.startsWith('/upload/') ||
cleanData.avatar.includes('api.dicebear.com')) {
console.log("Removing avatar URL for new user creation:", cleanData.avatar);
delete cleanData.avatar;
}
} }
const result = await pb.collection('users').create(data); // Determine which collection to use based on the role
return result as unknown as User; const isSuperAdmin = cleanData.role === "superadmin";
const collection = isSuperAdmin ? '_superusers' : 'users';
console.log(`Creating new user in ${collection} collection with data:`, {
...cleanData,
password: "[REDACTED]",
passwordConfirm: "[REDACTED]"
});
// Create the user in the appropriate collection
const result = await pb.collection(collection).create(cleanData);
return convertToUserType(result, isSuperAdmin ? "superadmin" : "admin");
} catch (error) { } catch (error) {
console.error("Failed to create user:", error); console.error("Failed to create user:", error);
throw error; throw error;