import Foundation import Combine import Security @MainActor class PDSService: ObservableObject { private var credentials: PDSCredentials? private var baseURL: URL? private var authHeader: String? private var session = URLSession.shared // Published properties for UI to observe @Published var isAuthenticated = false @Published var inviteCodes: [InviteCode] = [] @Published var users: [PDSUser] = [] @Published var isLoading = false @Published var errorMessage: String? // MARK: - Keychain Constants private struct KeychainConstants { static let service = "com.atproto.pdsmanager" static let usernameKey = "username" static let passwordKey = "password" static let serverURLKey = "serverURL" static let authHeaderKey = "authHeader" } init() { // Try to restore previous session on launch loadCredentialsFromKeychain() } // MARK: - Keychain Access private func saveToKeychain(key: String, value: String) -> Bool { if let data = value.data(using: .utf8) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: KeychainConstants.service, kSecAttrAccount as String: key, kSecValueData as String: data ] // First delete any existing item SecItemDelete(query as CFDictionary) // Then add the new item let status = SecItemAdd(query as CFDictionary, nil) return status == errSecSuccess } return false } private func loadFromKeychain(key: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: KeychainConstants.service, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess, let data = result as? Data, let value = String(data: data, encoding: .utf8) { return value } return nil } private func deleteFromKeychain(key: String) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: KeychainConstants.service, kSecAttrAccount as String: key ] let status = SecItemDelete(query as CFDictionary) return status == errSecSuccess || status == errSecItemNotFound } private func saveCredentialsToKeychain() { guard let credentials = credentials else { return } _ = saveToKeychain(key: KeychainConstants.usernameKey, value: credentials.username) _ = saveToKeychain(key: KeychainConstants.passwordKey, value: credentials.password) _ = saveToKeychain(key: KeychainConstants.serverURLKey, value: credentials.serverURL) if let authHeader = authHeader { _ = saveToKeychain(key: KeychainConstants.authHeaderKey, value: authHeader) } print("Credentials saved to keychain") } private func loadCredentialsFromKeychain() { guard let username = loadFromKeychain(key: KeychainConstants.usernameKey), let password = loadFromKeychain(key: KeychainConstants.passwordKey), let serverURL = loadFromKeychain(key: KeychainConstants.serverURLKey), let authHeader = loadFromKeychain(key: KeychainConstants.authHeaderKey) else { print("No credentials found in keychain") return } print("Credentials loaded from keychain") // Restore the credentials self.credentials = PDSCredentials(serverURL: serverURL, username: username, password: password) self.baseURL = URL(string: serverURL) self.authHeader = authHeader self.isAuthenticated = true // Refresh data Task { await fetchInviteCodes() await fetchUsers() } } private func clearCredentialsFromKeychain() { _ = deleteFromKeychain(key: KeychainConstants.usernameKey) _ = deleteFromKeychain(key: KeychainConstants.passwordKey) _ = deleteFromKeychain(key: KeychainConstants.serverURLKey) _ = deleteFromKeychain(key: KeychainConstants.authHeaderKey) print("Credentials cleared from keychain") } // MARK: - Response Structures // Invite Code structures struct InviteCodesResponse: Codable { var codes: [InviteCodeResponse] } struct InviteCodeResponse: Codable { var code: String var available: Int var disabled: Bool var createdAt: String var createdBy: String var uses: [CodeUse]? } struct CodeUse: Codable { var usedBy: String var usedAt: String } struct CreateCodeResponse: Codable { var code: String } // User structures struct RepoResponse: Codable { var cursor: String? var repos: [Repo] } struct Repo: Codable { var did: String var head: String var rev: String var active: Bool } struct AccountInfo: Codable { var did: String var handle: String var email: String? var emailConfirmedAt: String? var indexedAt: String var invitedBy: InviteInfo? var invites: [String]? var invitesDisabled: Bool? var createdAt: String { return indexedAt } } struct InviteInfo: Codable { var code: String var available: Int var disabled: Bool var forAccount: String var createdBy: String var createdAt: String var uses: [CodeUse]? } struct ProfileResponse: Codable { var value: ProfileValue } struct ProfileValue: Codable { var displayName: String? var description: String? var avatar: Avatar? } struct Avatar: Codable { var ref: AvatarRef } struct AvatarRef: Codable { var link: String enum CodingKeys: String, CodingKey { case link = "$link" } } // MARK: - Authentication private func setAuthenticated(_ value: Bool) { print("PDSService: setting isAuthenticated to \(value)") self.isAuthenticated = value } func login(credentials: PDSCredentials) async -> Bool { print("Attempting to connect to server: \(credentials.serverURL)") self.credentials = credentials self.isLoading = true defer { self.isLoading = false } // Clear any previous errors self.errorMessage = nil // Validate the URL format guard let components = URLComponents(string: credentials.serverURL) else { setError("Invalid URL format: The server URL is not properly formatted") return false } // Ensure scheme is either http or https guard let scheme = components.scheme, (scheme == "http" || scheme == "https") else { setError("Invalid URL scheme: URL must begin with http:// or https://") return false } // Ensure there's a host guard let host = components.host, !host.isEmpty else { setError("Invalid URL: Missing or empty host") return false } guard let url = URL(string: credentials.serverURL) else { setError("Invalid URL format") return false } // Test if we can connect to a known reliable server if await !testInternetConnectivity() { setError("Network connectivity issue: Cannot reach the internet. Please check your network connection.") return false } self.baseURL = url // Create Basic Auth header from credentials let authString = "\(credentials.username):\(credentials.password)" guard let authData = authString.data(using: .utf8) else { setError("Invalid authentication data") return false } let base64Auth = authData.base64EncodedString() self.authHeader = "Basic \(base64Auth)" // Just check if the server exists and is running, no auth needed for this endpoint guard let testURL = URL(string: "\(credentials.serverURL)/xrpc/com.atproto.server.describeServer") else { setError("Invalid server URL") return false } print("Testing connectivity to: \(testURL.absoluteString)") // Create a URLSession with shorter timeouts let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 10 // 10 seconds timeout config.timeoutIntervalForResource = 10 let quickTimeoutSession = URLSession(configuration: config) var request = URLRequest(url: testURL) request.httpMethod = "GET" do { print("Sending request...") let (data, response) = try await quickTimeoutSession.data(for: request) print("Got response: \(response)") // Debug: print response data as string if let dataString = String(data: data, encoding: .utf8) { print("Response data: \(dataString)") } guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return false } // If the server responds with 200, we consider it a success // verify auth credentials when user accesses protected endpoints if httpResponse.statusCode == 200 { setAuthenticated(true) // Save credentials securely saveCredentialsToKeychain() self.errorMessage = nil print("Login successful!") return true } else { let errorMessage = "Server error: \(httpResponse.statusCode)" print(errorMessage) setError(errorMessage) return false } } catch let error as NSError { // Handle specific network errors with better messages print("Connection error: \(error.domain) code: \(error.code) - \(error.localizedDescription)") if error.domain == NSURLErrorDomain { switch error.code { case NSURLErrorCannotFindHost: setError("Cannot find host: The server URL appears to be invalid or the server is unreachable") case NSURLErrorCannotConnectToHost: setError("Cannot connect to host: The server exists but isn't responding") case NSURLErrorNetworkConnectionLost: setError("Connection lost: The network connection was lost during the request") case NSURLErrorNotConnectedToInternet: setError("No internet connection: Please check your network settings") case NSURLErrorTimedOut: setError("Connection timed out: The server took too long to respond") default: setError("Network error (\(error.code)): \(error.localizedDescription)") } } else { setError("Connection error: \(error.localizedDescription)") } return false } } // Test basic internet connectivity by trying to reach a reliable server private func testInternetConnectivity() async -> Bool { print("Testing general internet connectivity...") // Use Apple's captive portal detection as a reliable server to test connectivity guard let connectivityURL = URL(string: "https://www.apple.com/library/test/success.html") else { return false } let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 5 config.waitsForConnectivity = false let session = URLSession(configuration: config) do { let (_, response) = try await session.data(for: URLRequest(url: connectivityURL)) guard let httpResponse = response as? HTTPURLResponse else { return false } print("Internet connectivity test: status code \(httpResponse.statusCode)") return httpResponse.statusCode == 200 } catch { print("Internet connectivity test failed: \(error.localizedDescription)") return false } } func logout() { // Clear authentication state setAuthenticated(false) self.authHeader = nil self.credentials = nil self.baseURL = nil self.users = [] self.inviteCodes = [] self.errorMessage = nil // Clear saved credentials clearCredentialsFromKeychain() } // MARK: - Invite Codes func fetchInviteCodes() async { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return } self.isLoading = true defer { self.isLoading = false } // Construct the URL for the invite codes endpoint guard let inviteCodesURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.getInviteCodes") else { setError("Invalid invite codes URL") return } // Add query parameters var components = URLComponents(url: inviteCodesURL, resolvingAgainstBaseURL: true) components?.queryItems = [ URLQueryItem(name: "sort", value: "recent"), URLQueryItem(name: "limit", value: "100"), URLQueryItem(name: "includeDisabled", value: "true") // Always include disabled codes ] guard let finalURL = components?.url else { setError("Invalid invite codes URL with parameters") return } var request = URLRequest(url: finalURL) request.httpMethod = "GET" request.addValue(authHeader, forHTTPHeaderField: "Authorization") do { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return } if httpResponse.statusCode == 200 { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let codesResponse = try decoder.decode(InviteCodesResponse.self, from: data) let dateFormatter = ISO8601DateFormatter() let parsedCodes = codesResponse.codes.map { codeResp -> InviteCode in let createdDate = dateFormatter.date(from: codeResp.createdAt) ?? Date() // Convert the uses array let inviteUses = codeResp.uses?.map { use -> PDSMan.CodeUse in return PDSMan.CodeUse(usedBy: use.usedBy, usedAt: use.usedAt) } return InviteCode( id: codeResp.code, uses: inviteUses, createdAt: createdDate, disabled: codeResp.disabled ) } self.inviteCodes = parsedCodes } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to fetch invite codes: \(httpResponse.statusCode) - \(responseString)") } } catch { setError("Failed to fetch invite codes: \(error.localizedDescription)") } } func createInviteCode(maxUses: Int = 1) async -> InviteCode? { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return nil } // Construct the URL for creating an invite code guard let createURL = URL(string: "\(baseURL)/xrpc/com.atproto.server.createInviteCode") else { setError("Invalid create invite code URL") return nil } var request = URLRequest(url: createURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(authHeader, forHTTPHeaderField: "Authorization") let createBody = ["useCount": maxUses] do { let jsonData = try JSONSerialization.data(withJSONObject: createBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return nil } if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 { // Parse the response to get the new code let decoder = JSONDecoder() let codeResponse = try decoder.decode(CreateCodeResponse.self, from: data) // Create a new InviteCode object let newCode = InviteCode( id: codeResponse.code, uses: [] as [PDSMan.CodeUse]?, createdAt: Date(), disabled: false ) // Update the local list self.inviteCodes.append(newCode) return newCode } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to create invite code: \(httpResponse.statusCode) - \(responseString)") return nil } } catch { setError("Failed to create invite code: \(error.localizedDescription)") return nil } } func disableInviteCode(_ code: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return false } guard let disableURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.disableInviteCodes") else { setError("Invalid disable invite code URL") return false } var request = URLRequest(url: disableURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(authHeader, forHTTPHeaderField: "Authorization") // Create the request body with an array of codes let disableBody = ["codes": [code]] do { let jsonData = try JSONSerialization.data(withJSONObject: disableBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return false } if httpResponse.statusCode == 200 { // Refresh the invite codes await fetchInviteCodes() return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to disable invite code: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to disable invite code: \(error.localizedDescription)") return false } } // MARK: - Users func fetchUsers() async { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return } self.isLoading = true defer { self.isLoading = false } // Construct the URL for the repos endpoint guard let reposURL = URL(string: "\(baseURL)/xrpc/com.atproto.sync.listRepos") else { setError("Invalid list repos URL") return } // Add query parameters var components = URLComponents(url: reposURL, resolvingAgainstBaseURL: true) components?.queryItems = [ URLQueryItem(name: "limit", value: "100") ] guard let finalURL = components?.url else { setError("Invalid repos URL with parameters") return } var request = URLRequest(url: finalURL) request.httpMethod = "GET" request.addValue(authHeader, forHTTPHeaderField: "Authorization") do { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return } if httpResponse.statusCode == 200 { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let reposResponse = try decoder.decode(RepoResponse.self, from: data) // Fetch details for each user var fetchedUsers: [PDSUser] = [] for repo in reposResponse.repos { if let user = await fetchUserProfile(did: repo.did, isActive: repo.active) { fetchedUsers.append(user) } } // Sort users by join date (newest first) fetchedUsers.sort { $0.joinedAt > $1.joinedAt } self.users = fetchedUsers } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to fetch users: \(httpResponse.statusCode) - \(responseString)") } } catch { setError("Failed to fetch users: \(error.localizedDescription)") } } private func fetchUserProfile(did: String, isActive: Bool = true) async -> PDSUser? { guard let baseURL = baseURL, let authHeader = authHeader else { print("Cannot fetch user profile: Missing baseURL or authHeader") return nil } print("Fetching profile for user: \(did)") // First, fetch account info do { // 1. Fetch account info guard let accountURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.getAccountInfo?did=\(did)") else { print("Invalid account info URL for did: \(did)") return nil } print("Requesting account info from: \(accountURL.absoluteString)") var accountRequest = URLRequest(url: accountURL) accountRequest.httpMethod = "GET" accountRequest.addValue(authHeader, forHTTPHeaderField: "Authorization") let (accountData, accountResponse) = try await session.data(for: accountRequest) guard let httpResponse = accountResponse as? HTTPURLResponse else { print("Invalid HTTP response for account info") return nil } // Print response status and data for debugging print("Account info response status: \(httpResponse.statusCode)") if let responseString = String(data: accountData, encoding: .utf8) { print("Account info response: \(responseString)") } guard httpResponse.statusCode == 200 else { let statusCode = httpResponse.statusCode let responseText = String(data: accountData, encoding: .utf8) ?? "Unknown error" print("Account info failed with status \(statusCode): \(responseText)") return createBasicUser(did: did, handle: did, isActive: isActive) } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase // Parse the account info let accountInfo: AccountInfo do { accountInfo = try decoder.decode(AccountInfo.self, from: accountData) print("Successfully decoded account info for \(accountInfo.handle)") } catch { print("Error decoding account info: \(error)") // If we can't decode the account info, create a basic user with the DID return createBasicUser(did: did, handle: did, isActive: isActive) } // 2. Try to fetch profile data (optional) var displayName = accountInfo.handle var description = "" var avatarURL: URL? = nil // Try to fetch profile record let profileURLString = "\(baseURL)/xrpc/com.atproto.repo.getRecord?collection=app.bsky.actor.profile&repo=\(did)&rkey=self" guard let profileURL = URL(string: profileURLString) else { print("Invalid profile URL: \(profileURLString)") // Still return user with account info let dateFormatter = ISO8601DateFormatter() let joinedDate = dateFormatter.date(from: accountInfo.indexedAt) ?? Date() return PDSUser( id: accountInfo.did, handle: accountInfo.handle, displayName: displayName, description: description, joinedAt: joinedDate, avatar: avatarURL, isActive: isActive ) } print("Fetching profile from: \(profileURL.absoluteString)") var profileRequest = URLRequest(url: profileURL) profileRequest.httpMethod = "GET" profileRequest.addValue(authHeader, forHTTPHeaderField: "Authorization") do { let (profileData, profileResponse) = try await session.data(for: profileRequest) if let httpResponse = profileResponse as? HTTPURLResponse { print("Profile response status: \(httpResponse.statusCode)") if let responseString = String(data: profileData, encoding: .utf8) { print("Profile response: \(responseString)") } if httpResponse.statusCode == 200 { // Define the structures to match the expected response do { // Try to decode the profile data let profileRecord = try decoder.decode(ProfileResponse.self, from: profileData) if let name = profileRecord.value.displayName, !name.isEmpty { displayName = name } if let desc = profileRecord.value.description, !desc.isEmpty { description = desc } if let avatar = profileRecord.value.avatar { // Construct avatar URL avatarURL = URL(string: "\(baseURL)/xrpc/com.atproto.sync.getBlob?did=\(did)&cid=\(avatar.ref.link)") print("Avatar URL: \(avatarURL?.absoluteString ?? "nil")") } } catch { print("Error decoding profile: \(error)") // Continue with basic info if profile decoding fails } } else { // Profile fetch failed, but we can still continue with account info print("Profile fetch failed with status \(httpResponse.statusCode)") } } } catch { // Continue without profile data print("Error fetching profile: \(error.localizedDescription)") } let dateFormatter = ISO8601DateFormatter() let joinedDate = dateFormatter.date(from: accountInfo.indexedAt) ?? Date() return PDSUser( id: accountInfo.did, handle: accountInfo.handle, displayName: displayName, description: description, joinedAt: joinedDate, avatar: avatarURL, isActive: isActive ) } catch { print("Error fetching account info: \(error.localizedDescription)") return createBasicUser(did: did, handle: did, isActive: isActive) } } // Helper method to create a basic user with minimal info private func createBasicUser(did: String, handle: String, isActive: Bool) -> PDSUser { print("Creating basic user for did: \(did)") return PDSUser( id: did, handle: handle, displayName: handle, description: "", joinedAt: Date(), avatar: nil, isActive: isActive ) } func editUserHandle(userId: String, newHandle: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return false } guard let updateURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.updateAccountHandle") else { setError("Invalid update handle URL") return false } var request = URLRequest(url: updateURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(authHeader, forHTTPHeaderField: "Authorization") // Create the request body let updateBody: [String: String] = [ "did": userId, "handle": newHandle ] do { let jsonData = try JSONSerialization.data(withJSONObject: updateBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response") return false } if httpResponse.statusCode == 200 { // Refresh the users list to show the updated handle await fetchUsers() return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to update handle: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to update handle: \(error.localizedDescription)") return false } } func resetUserPassword(userId: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL else { return false } // Construct the URL for resetting a user's password guard let resetURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.resetPassword") else { setError("Invalid reset password URL") return false } var request = URLRequest(url: resetURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let resetBody = ["did": userId] do { let jsonData = try JSONSerialization.data(withJSONObject: resetBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return false } if httpResponse.statusCode == 200 || httpResponse.statusCode == 204 { return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to reset password: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to reset password: \(error.localizedDescription)") return false } } func sendResetEmail(userId: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL else { return false } // Construct the URL for sending a reset email guard let emailURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.sendEmail") else { setError("Invalid send email URL") return false } var request = URLRequest(url: emailURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let emailBody = [ "recipientDid": userId, "subject": "Password Reset", "body": "Click the link to reset your password." ] do { let jsonData = try JSONSerialization.data(withJSONObject: emailBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return false } if httpResponse.statusCode == 200 || httpResponse.statusCode == 204 { return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to send reset email: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to send reset email: \(error.localizedDescription)") return false } } func deleteUser(userId: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL else { return false } // Construct the URL for deleting a user guard let deleteURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.deleteAccount") else { setError("Invalid delete user URL") return false } var request = URLRequest(url: deleteURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let deleteBody = ["did": userId] do { let jsonData = try JSONSerialization.data(withJSONObject: deleteBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response from server") return false } if httpResponse.statusCode == 200 || httpResponse.statusCode == 204 { // Update the local list self.users.removeAll { $0.id == userId } return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to delete user: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to delete user: \(error.localizedDescription)") return false } } func suspendUser(userId: String, reason: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return false } guard let suspendURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.disableAccountByDid") else { setError("Invalid suspend user URL") return false } var request = URLRequest(url: suspendURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(authHeader, forHTTPHeaderField: "Authorization") // Create the request body let suspendBody: [String: Any] = [ "did": userId, "reason": reason ] do { let jsonData = try JSONSerialization.data(withJSONObject: suspendBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response") return false } if httpResponse.statusCode == 200 { // Refresh the users list to show the updated status await fetchUsers() return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to suspend user: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to suspend user: \(error.localizedDescription)") return false } } func reactivateUser(userId: String) async -> Bool { guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return false } guard let reactivateURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.enableAccountByDid") else { setError("Invalid reactivate user URL") return false } var request = URLRequest(url: reactivateURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(authHeader, forHTTPHeaderField: "Authorization") // Create the request body let reactivateBody: [String: String] = [ "did": userId ] do { let jsonData = try JSONSerialization.data(withJSONObject: reactivateBody) request.httpBody = jsonData let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { setError("Invalid response") return false } if httpResponse.statusCode == 200 { // Refresh the users list to show the updated status await fetchUsers() return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" setError("Failed to reactivate user: \(httpResponse.statusCode) - \(responseString)") return false } } catch { setError("Failed to reactivate user: \(error.localizedDescription)") return false } } // MARK: - Helper Methods private func setError(_ message: String) { self.errorMessage = message print("Error: \(message)") } }