851 lines
28 KiB
Swift
851 lines
28 KiB
Swift
import Foundation
|
|
import Combine
|
|
import Security
|
|
|
|
@MainActor
|
|
class PDSService: ObservableObject {
|
|
private var credentials: PDSCredentials?
|
|
private var baseURL: URL?
|
|
private var authHeader: String?
|
|
|
|
// Use a shared URLSession configuration for performance
|
|
private lazy var session: URLSession = {
|
|
let config = URLSessionConfiguration.default
|
|
config.timeoutIntervalForRequest = 30
|
|
config.timeoutIntervalForResource = 30
|
|
config.waitsForConnectivity = true
|
|
config.requestCachePolicy = .returnCacheDataElseLoad
|
|
return URLSession(configuration: config)
|
|
}()
|
|
|
|
// Session for quick connectivity checks
|
|
private lazy var quickSession: URLSession = {
|
|
let config = URLSessionConfiguration.default
|
|
config.timeoutIntervalForRequest = 10
|
|
config.timeoutIntervalForResource = 10
|
|
config.waitsForConnectivity = false
|
|
return URLSession(configuration: config)
|
|
}()
|
|
|
|
// 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 {
|
|
guard let data = value.data(using: .utf8) else { return false }
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return
|
|
}
|
|
|
|
// 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 refreshData()
|
|
}
|
|
}
|
|
|
|
private func clearCredentialsFromKeychain() {
|
|
_ = deleteFromKeychain(key: KeychainConstants.usernameKey)
|
|
_ = deleteFromKeychain(key: KeychainConstants.passwordKey)
|
|
_ = deleteFromKeychain(key: KeychainConstants.serverURLKey)
|
|
_ = deleteFromKeychain(key: KeychainConstants.authHeaderKey)
|
|
}
|
|
|
|
// 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) {
|
|
self.isAuthenticated = value
|
|
}
|
|
|
|
func login(credentials: PDSCredentials) async -> Bool {
|
|
self.credentials = credentials
|
|
self.isLoading = true
|
|
defer { self.isLoading = false }
|
|
|
|
// Clear any previous errors
|
|
self.errorMessage = nil
|
|
|
|
// Validate URL
|
|
guard let url = validateServerURL(credentials.serverURL) else {
|
|
return false
|
|
}
|
|
|
|
// Test internet connectivity
|
|
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
|
|
guard let authHeader = createAuthHeader(username: credentials.username, password: credentials.password) else {
|
|
setError("Invalid authentication data")
|
|
return false
|
|
}
|
|
|
|
self.authHeader = authHeader
|
|
|
|
// Test server connectivity
|
|
guard let testURL = URL(string: "\(credentials.serverURL)/xrpc/com.atproto.server.describeServer") else {
|
|
setError("Invalid server URL")
|
|
return false
|
|
}
|
|
|
|
var request = URLRequest(url: testURL)
|
|
request.httpMethod = "GET"
|
|
|
|
do {
|
|
let (data, response) = try await quickSession.data(for: request)
|
|
|
|
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
|
|
if httpResponse.statusCode == 200 {
|
|
setAuthenticated(true)
|
|
|
|
// Save credentials securely
|
|
saveCredentialsToKeychain()
|
|
|
|
self.errorMessage = nil
|
|
|
|
// Fetch initial data
|
|
await refreshData()
|
|
|
|
return true
|
|
} else {
|
|
setError("Server error: \(httpResponse.statusCode)")
|
|
return false
|
|
}
|
|
} catch let error as NSError {
|
|
handleNetworkError(error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func refreshData() async {
|
|
await fetchInviteCodes()
|
|
await fetchUsers()
|
|
}
|
|
|
|
private func validateServerURL(_ urlString: String) -> URL? {
|
|
// Validate the URL format
|
|
guard let components = URLComponents(string: urlString) else {
|
|
setError("Invalid URL format: The server URL is not properly formatted")
|
|
return nil
|
|
}
|
|
|
|
// Ensure scheme is https (we now assume all URLs use HTTPS)
|
|
guard let scheme = components.scheme, scheme == "https" else {
|
|
setError("Invalid URL scheme: URL must use HTTPS")
|
|
return nil
|
|
}
|
|
|
|
// Ensure there's a host
|
|
guard let host = components.host, !host.isEmpty else {
|
|
setError("Invalid URL: Missing or empty host")
|
|
return nil
|
|
}
|
|
|
|
return URL(string: urlString)
|
|
}
|
|
|
|
private func createAuthHeader(username: String, password: String) -> String? {
|
|
let authString = "\(username):\(password)"
|
|
guard let authData = authString.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
|
|
let base64Auth = authData.base64EncodedString()
|
|
return "Basic \(base64Auth)"
|
|
}
|
|
|
|
// Test basic internet connectivity by trying to reach a reliable server
|
|
private func testInternetConnectivity() async -> Bool {
|
|
// 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
|
|
}
|
|
|
|
do {
|
|
let (_, response) = try await quickSession.data(for: URLRequest(url: connectivityURL))
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
return false
|
|
}
|
|
|
|
return httpResponse.statusCode == 200
|
|
} catch {
|
|
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: - Network Request Helper
|
|
|
|
private func performRequest<T: Decodable>(
|
|
endpoint: String,
|
|
method: String = "GET",
|
|
queryItems: [URLQueryItem]? = nil,
|
|
body: [String: Any]? = nil,
|
|
requiresAuth: Bool = true
|
|
) async throws -> T {
|
|
guard let baseURL = baseURL else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
// Build URL with components
|
|
var components = URLComponents(string: "\(baseURL)/xrpc/\(endpoint)")
|
|
if let queryItems = queryItems {
|
|
components?.queryItems = queryItems
|
|
}
|
|
|
|
guard let url = components?.url else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = method
|
|
|
|
// Add authorization if required
|
|
if requiresAuth, let authHeader = authHeader {
|
|
request.addValue(authHeader, forHTTPHeaderField: "Authorization")
|
|
}
|
|
|
|
// Add body if provided
|
|
if let body = body {
|
|
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
}
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
|
|
guard 200...299 ~= httpResponse.statusCode else {
|
|
let errorMessage = String(data: data, encoding: .utf8) ?? "Unknown error"
|
|
throw NSError(
|
|
domain: "PDSServiceError",
|
|
code: httpResponse.statusCode,
|
|
userInfo: [NSLocalizedDescriptionKey: "Server returned \(httpResponse.statusCode): \(errorMessage)"]
|
|
)
|
|
}
|
|
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
return try decoder.decode(T.self, from: data)
|
|
}
|
|
|
|
// Simplified request for endpoints that return no content
|
|
private func performEmptyRequest(
|
|
endpoint: String,
|
|
method: String = "POST",
|
|
body: [String: Any]? = nil,
|
|
requiresAuth: Bool = true
|
|
) async throws {
|
|
guard let baseURL = baseURL else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
guard let url = URL(string: "\(baseURL)/xrpc/\(endpoint)") else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = method
|
|
|
|
// Add authorization if required
|
|
if requiresAuth, let authHeader = authHeader {
|
|
request.addValue(authHeader, forHTTPHeaderField: "Authorization")
|
|
}
|
|
|
|
// Add body if provided
|
|
if let body = body {
|
|
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
}
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
|
|
guard 200...299 ~= httpResponse.statusCode else {
|
|
let errorMessage = String(data: data, encoding: .utf8) ?? "Unknown error"
|
|
throw NSError(
|
|
domain: "PDSServiceError",
|
|
code: httpResponse.statusCode,
|
|
userInfo: [NSLocalizedDescriptionKey: "Server returned \(httpResponse.statusCode): \(errorMessage)"]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Handling
|
|
|
|
private func handleNetworkError(_ error: NSError) {
|
|
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)")
|
|
}
|
|
}
|
|
|
|
private func setError(_ message: String) {
|
|
self.errorMessage = message
|
|
}
|
|
|
|
// MARK: - Invite Codes
|
|
|
|
func fetchInviteCodes() async {
|
|
self.isLoading = true
|
|
defer { self.isLoading = false }
|
|
|
|
guard isAuthenticated else { return }
|
|
|
|
do {
|
|
let queryItems = [
|
|
URLQueryItem(name: "sort", value: "recent"),
|
|
URLQueryItem(name: "limit", value: "100"),
|
|
URLQueryItem(name: "includeDisabled", value: "true")
|
|
]
|
|
|
|
let codesResponse: InviteCodesResponse = try await performRequest(
|
|
endpoint: "com.atproto.admin.getInviteCodes",
|
|
queryItems: queryItems
|
|
)
|
|
|
|
// Map the response to our model
|
|
let parsedCodes = codesResponse.codes.map { codeResp -> InviteCode in
|
|
let createdDate = ISO8601DateFormatter.shared.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
|
|
)
|
|
}
|
|
|
|
// Update the inviteCodes property
|
|
self.inviteCodes = parsedCodes
|
|
self.objectWillChange.send()
|
|
|
|
} catch {
|
|
setError("Error fetching invite codes: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
func createInviteCode(maxUses: Int = 1) async -> InviteCode? {
|
|
guard isAuthenticated else { return nil }
|
|
|
|
do {
|
|
let createBody = ["useCount": maxUses]
|
|
|
|
let codeResponse: CreateCodeResponse = try await performRequest(
|
|
endpoint: "com.atproto.server.createInviteCode",
|
|
method: "POST",
|
|
body: createBody
|
|
)
|
|
|
|
// 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)
|
|
self.objectWillChange.send()
|
|
|
|
// Also refresh the full list to ensure we have the most up-to-date data
|
|
await fetchInviteCodes()
|
|
|
|
return newCode
|
|
} catch {
|
|
setError("Failed to create invite code: \(error.localizedDescription)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func disableInviteCode(_ code: String) async -> Bool {
|
|
guard isAuthenticated else { return false }
|
|
|
|
do {
|
|
// Create the request body
|
|
let disableBody = ["codes": [code]]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.disableInviteCodes",
|
|
body: disableBody
|
|
)
|
|
|
|
// Refresh the invite codes list
|
|
await fetchInviteCodes()
|
|
|
|
return true
|
|
} catch {
|
|
setError("Failed to disable code: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Users
|
|
|
|
func fetchUsers() async {
|
|
self.isLoading = true
|
|
defer { self.isLoading = false }
|
|
|
|
guard isAuthenticated else { return }
|
|
|
|
do {
|
|
// First, get a list of all repos (users) on the server
|
|
let reposResult: RepoResponse = try await performRequest(
|
|
endpoint: "com.atproto.sync.listRepos"
|
|
)
|
|
|
|
// Fetch individual user profiles
|
|
var fetchedUsers: [PDSUser] = []
|
|
|
|
// Use task groups for concurrent fetching of user profiles
|
|
await withTaskGroup(of: PDSUser?.self) { group in
|
|
for repo in reposResult.repos {
|
|
group.addTask {
|
|
await self.fetchUserProfile(did: repo.did, isActive: repo.active)
|
|
}
|
|
}
|
|
|
|
// Collect results as they complete
|
|
for await user in group {
|
|
if let user = user {
|
|
fetchedUsers.append(user)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort users by join date (newest first)
|
|
fetchedUsers.sort { $0.joinedAt > $1.joinedAt }
|
|
|
|
// Update the users property
|
|
self.users = fetchedUsers
|
|
self.objectWillChange.send()
|
|
|
|
} catch {
|
|
setError("Error fetching users: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private func fetchUserProfile(did: String, isActive: Bool = true) async -> PDSUser? {
|
|
guard isAuthenticated else { return nil }
|
|
|
|
do {
|
|
// 1. Fetch account info
|
|
let queryItems = [URLQueryItem(name: "did", value: did)]
|
|
|
|
let accountInfo: AccountInfo = try await performRequest(
|
|
endpoint: "com.atproto.admin.getAccountInfo",
|
|
queryItems: queryItems
|
|
)
|
|
|
|
// 2. Try to fetch profile data (optional)
|
|
var displayName = accountInfo.handle
|
|
var description = ""
|
|
var avatarURL: URL? = nil
|
|
|
|
// Try to fetch profile record
|
|
do {
|
|
let profileQueryItems = [
|
|
URLQueryItem(name: "collection", value: "app.bsky.actor.profile"),
|
|
URLQueryItem(name: "repo", value: did),
|
|
URLQueryItem(name: "rkey", value: "self")
|
|
]
|
|
|
|
let profileRecord: ProfileResponse = try await performRequest(
|
|
endpoint: "com.atproto.repo.getRecord",
|
|
queryItems: profileQueryItems
|
|
)
|
|
|
|
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, let baseURL = baseURL {
|
|
// Construct avatar URL
|
|
avatarURL = URL(string: "\(baseURL)/xrpc/com.atproto.sync.getBlob?did=\(did)&cid=\(avatar.ref.link)")
|
|
}
|
|
} catch {
|
|
// Continue with basic info if profile fetch fails
|
|
}
|
|
|
|
let joinedDate = ISO8601DateFormatter.shared.date(from: accountInfo.indexedAt) ?? Date()
|
|
|
|
return PDSUser(
|
|
id: accountInfo.did,
|
|
handle: accountInfo.handle,
|
|
displayName: displayName,
|
|
description: description,
|
|
joinedAt: joinedDate,
|
|
avatar: avatarURL,
|
|
isActive: isActive
|
|
)
|
|
|
|
} catch {
|
|
// If account info fetch fails, create a basic user
|
|
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 {
|
|
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 else { return false }
|
|
|
|
do {
|
|
// Create the request body
|
|
let updateBody: [String: String] = [
|
|
"did": userId,
|
|
"handle": newHandle
|
|
]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.updateAccountHandle",
|
|
body: updateBody
|
|
)
|
|
|
|
// Refresh the users list to show the updated handle
|
|
await fetchUsers()
|
|
return true
|
|
} catch {
|
|
setError("Failed to update handle: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func resetUserPassword(userId: String) async -> Bool {
|
|
guard isAuthenticated else { return false }
|
|
|
|
do {
|
|
let resetBody = ["did": userId]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.resetPassword",
|
|
body: resetBody
|
|
)
|
|
|
|
return true
|
|
} catch {
|
|
setError("Failed to reset password: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func sendResetEmail(userId: String) async -> Bool {
|
|
guard isAuthenticated else { return false }
|
|
|
|
do {
|
|
let emailBody = [
|
|
"recipientDid": userId,
|
|
"subject": "Password Reset",
|
|
"body": "Click the link to reset your password."
|
|
]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.sendEmail",
|
|
body: emailBody
|
|
)
|
|
|
|
return true
|
|
} catch {
|
|
setError("Failed to send reset email: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func deleteUser(userId: String) async -> Bool {
|
|
guard isAuthenticated else { return false }
|
|
|
|
do {
|
|
let deleteBody = ["did": userId]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.deleteAccount",
|
|
body: deleteBody
|
|
)
|
|
|
|
// Update the local list
|
|
self.users.removeAll { $0.id == userId }
|
|
return true
|
|
} catch {
|
|
setError("Failed to delete user: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func suspendUser(userId: String, reason: String) async -> Bool {
|
|
guard isAuthenticated else { return false }
|
|
|
|
do {
|
|
// Create the request body
|
|
let suspendBody: [String: Any] = [
|
|
"did": userId,
|
|
"reason": reason
|
|
]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.disableAccountByDid",
|
|
body: suspendBody
|
|
)
|
|
|
|
// Refresh the users list to show the updated status
|
|
await fetchUsers()
|
|
return true
|
|
} catch {
|
|
setError("Failed to suspend user: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func reactivateUser(userId: String) async -> Bool {
|
|
guard isAuthenticated else { return false }
|
|
|
|
do {
|
|
// Create the request body
|
|
let reactivateBody: [String: String] = [
|
|
"did": userId
|
|
]
|
|
|
|
try await performEmptyRequest(
|
|
endpoint: "com.atproto.admin.enableAccountByDid",
|
|
body: reactivateBody
|
|
)
|
|
|
|
// Refresh the users list to show the updated status
|
|
await fetchUsers()
|
|
return true
|
|
} catch {
|
|
setError("Failed to reactivate user: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
}
|