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";
export interface User {
@@ -38,20 +37,54 @@ export interface UpdateUserData {
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 = {
async getUsers(): Promise<User[] | null> {
try {
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',
});
console.log("Users API response:", result);
if (result.items.length > 0) {
return result.items as unknown as User[];
let superadminUsers: any = [];
try {
// Try to get superadmin users if exists
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) {
console.error("Failed to fetch users:", error);
return null;
@@ -61,9 +94,25 @@ export const userService = {
async getUser(id: string): Promise<User | null> {
try {
console.log(`Fetching user with ID: ${id}`);
const result = await pb.collection('users').getOne(id);
console.log("User fetch result:", result);
return result as unknown as User;
// Try fetching from regular users first
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) {
console.error(`Failed to fetch user ${id}:`, error);
return null;
@@ -90,6 +139,15 @@ export const userService = {
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
const hasEmailChange = cleanData.email !== undefined;
const emailToUpdate = hasEmailChange ? cleanData.email : null;
@@ -107,39 +165,106 @@ export const userService = {
let updatedUser: User | null = null;
// 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);
// First, determine if this is currently a regular user or superadmin
let isCurrentlySuperadmin = false;
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 {
// Use PocketBase's update method for the regular fields
updatedUser = await pb.collection('users').update(id, cleanData) as unknown as User;
console.log("PocketBase update response for regular fields:", updatedUser);
if (targetRole === "superadmin") {
// Create in superadmin collection
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) {
console.error("Error updating user regular fields:", error);
throw error;
console.error("Failed to transfer user between collections:", error);
throw new Error("Failed to change user role: " + (error instanceof Error ? error.message : "Unknown error"));
}
} else {
// If no other fields to update, get the current user
updatedUser = await this.getUser(id);
// 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);
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
if (emailToUpdate) {
if (emailToUpdate && updatedUser) {
try {
console.log("Processing email change request for new email:", emailToUpdate);
// Use the proper API endpoint for email change requests
const emailChangeResponse = await pb.collection('users').requestEmailChange(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("Email change request response:", emailChangeResponse);
console.log("Email verification has been sent to:", emailToUpdate);
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);
// 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) {
console.error("Failed to request email change:", error);
throw new Error(`Failed to request email change: ${error instanceof Error ? error.message : "Unknown 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");
}
}
@@ -152,8 +277,22 @@ export const userService = {
async deleteUser(id: string): Promise<boolean> {
try {
await pb.collection('users').delete(id);
return true;
// Try to delete from regular users first
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) {
console.error("Failed to delete user:", error);
return false;
@@ -162,16 +301,35 @@ export const userService = {
async createUser(data: CreateUserData): Promise<User | null> {
try {
// Handle the avatar field for static files
if (data.avatar && typeof data.avatar === 'string' && data.avatar.startsWith('/upload/profile/')) {
// Remove avatar field for new users if it's a local path
// PocketBase requires actual file uploads, not paths
console.log("Removing local avatar path for new user");
delete data.avatar;
// Create a clean data object without avatar field if it's a URL
// PocketBase requires actual file uploads for avatar, not URLs
const cleanData = { ...data };
// Remove avatar if it's a URL (we'll handle this differently in the future)
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);
return result as unknown as User;
// Determine which collection to use based on the role
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) {
console.error("Failed to create user:", error);
throw error;