element-ios/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift

147 lines
6.5 KiB
Swift

//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
@objc protocol DeactivateAccountServiceDelegate: AnyObject {
/// The service encountered an error.
/// - Parameter error: The error that occurred.
func deactivateAccountServiceDidEncounterError(_ error: Error)
/// The service successfully completed the account deactivation.
func deactivateAccountServiceDidCompleteDeactivation()
}
/// The kind of authentication needed to deactivate an account.
@objc enum DeactivateAccountAuthentication: Int {
/// The deactivation endpoint is already authenticated. This is unlikely to be the case.
case authenticated
/// The deactivation endpoint requires a password for authentication.
case requiresPassword
/// The deactivation endpoint requires fallback for authentication.
case requiresFallback
}
/// An error that occurred in the `DeactivateAccountService`
enum DeactivateAccountServiceError: Error {
/// The next stage in the flow wasn't found.
case missingStage
/// The URL needed to present fallback authentication wasn't found.
case missingFallbackURL
}
/// A service that helps handle interactive authentication when deactivating an account.
@objcMembers class DeactivateAccountService: NSObject {
private let session: MXSession
private let uiaService: UserInteractiveAuthenticationService
private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST", params: [:])
/// The authentication session's ID if interactive authentication has begun, otherwise `nil`.
private var sessionID: String?
weak var delegate: DeactivateAccountServiceDelegate?
/// Creates a new service for the supplied session.
/// - Parameter session: The session with the account to be deactivated.
init(session: MXSession) {
self.session = session
self.uiaService = UserInteractiveAuthenticationService(session: session)
}
/// Checks the authentication required for deactivation.
/// - Parameters:
/// - success: A closure called containing information about the kind of authentication required (and a fallback URL if needed).
/// - failure: A closure called then the check failed.
func checkAuthentication(success: @escaping (DeactivateAccountAuthentication, URL?) -> Void,
failure: @escaping (Error) -> Void) {
uiaService.authenticatedEndpointStatus(for: request) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let status):
switch status {
case .authenticationNotNeeded:
success(.authenticated, nil)
case .authenticationNeeded(let session):
do {
let (authentication, fallbackURL) = try self.handleAuthenticationSession(session)
success(authentication, fallbackURL)
} catch {
failure(error)
}
}
case .failure(let error):
failure(error)
}
}
}
/// Extracts the authentication information out of an `MXAuthenticationSession`.
/// - Parameter authenticationSession: The authentication session to be used.
/// - Returns: A tuple containing the required authentication method along with a URL for fallback if necessary.
private func handleAuthenticationSession(_ authenticationSession: MXAuthenticationSession) throws -> (DeactivateAccountAuthentication, URL?) {
guard let nextStage = uiaService.firstUncompletedFlowIdentifier(in: authenticationSession) else {
MXLog.error("[DeactivateAccountService] handleAuthenticationSession: Failed to determine the next stage.")
throw DeactivateAccountServiceError.missingStage
}
switch nextStage {
case kMXLoginFlowTypePassword:
sessionID = authenticationSession.session
return (.requiresPassword, nil)
default:
guard let fallbackURL = uiaService.firstUncompletedStageAuthenticationFallbackURL(for: authenticationSession) else {
MXLog.error("[DeactivateAccountService] handleAuthenticationSession: Failed to determine fallback URL.")
throw DeactivateAccountServiceError.missingFallbackURL
}
sessionID = authenticationSession.session
return (.requiresFallback, fallbackURL)
}
}
/// Deactivates the account with the supplied password.
/// - Parameters:
/// - password: The password for the account.
/// - eraseAccount: Whether or not to erase all of the data from the account too.
func deactivate(with password: String, eraseAccount: Bool) {
do {
let parameters = try DeactivateAccountPasswordParameters(user: session.myUserId, password: password).dictionary()
deactivateAccount(parameters: parameters, eraseAccount: eraseAccount)
} catch {
self.delegate?.deactivateAccountServiceDidEncounterError(error)
}
}
/// Deactivates the account when authentication has already been completed.
/// - Parameter eraseAccount: Whether or not to erase all of the data from the account too.
func deactivate(eraseAccount: Bool) {
do {
let parameters = try DeactivateAccountDummyParameters(user: session.myUserId, session: sessionID ?? "").dictionary()
deactivateAccount(parameters: parameters, eraseAccount: eraseAccount)
} catch {
self.delegate?.deactivateAccountServiceDidEncounterError(error)
}
}
/// Deactivated the account using the specified parameters.
/// - Parameters:
/// - parameters: The parameters for account deactivation.
/// - eraseAccount: Whether or not to erase all of the data from the account too.
private func deactivateAccount(parameters: [String: Any], eraseAccount: Bool) {
session.deactivateAccount(withAuthParameters: parameters, eraseAccount: eraseAccount) { [weak self] response in
switch response {
case .success:
self?.delegate?.deactivateAccountServiceDidCompleteDeactivation()
case .failure(let error):
self?.delegate?.deactivateAccountServiceDidEncounterError(error)
}
}
}
}