diff --git a/application/src/services/userService.ts b/application/src/services/userService.ts index e79ee55..7193c68 100644 --- a/application/src/services/userService.ts +++ b/application/src/services/userService.ts @@ -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 { 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 { 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 { 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 { 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;