108 lines
3.4 KiB
Swift
108 lines
3.4 KiB
Swift
//
|
|
// AuthService.swift
|
|
// furumi_macos
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct AuthService {
|
|
|
|
private let urlSession = URLSession.shared
|
|
|
|
private let decoder: JSONDecoder = {
|
|
let d = JSONDecoder()
|
|
d.keyDecodingStrategy = .convertFromSnakeCase
|
|
return d
|
|
}()
|
|
|
|
private var deviceName: String {
|
|
ProcessInfo.processInfo.hostName.components(separatedBy: ".").first ?? "macOS"
|
|
}
|
|
|
|
// MARK: - URL builders
|
|
|
|
func ssoStartURL(baseUrl: String) -> URL? {
|
|
guard var components = URLComponents(string: "\(baseUrl)/auth/mobile/oidc/start") else { return nil }
|
|
components.queryItems = [URLQueryItem(name: "redirect_uri", value: "furumi://auth/callback")]
|
|
return components.url
|
|
}
|
|
|
|
// MARK: - API
|
|
|
|
func login(baseUrl: String, username: String, password: String) async throws -> LoginResponse {
|
|
struct Body: Encodable {
|
|
let username: String
|
|
let password: String
|
|
let device_name: String
|
|
}
|
|
return try await post(
|
|
urlString: "\(baseUrl)/api/auth/password",
|
|
body: Body(username: username, password: password, device_name: deviceName)
|
|
)
|
|
}
|
|
|
|
func ssoExchange(baseUrl: String, code: String) async throws -> LoginResponse {
|
|
struct Body: Encodable {
|
|
let code: String
|
|
let device_name: String
|
|
}
|
|
return try await post(
|
|
urlString: "\(baseUrl)/api/auth/sso/exchange",
|
|
body: Body(code: code, device_name: deviceName)
|
|
)
|
|
}
|
|
|
|
func refresh(baseUrl: String, refreshToken: String) async throws -> TokenResponse {
|
|
struct Body: Encodable { let refresh_token: String }
|
|
return try await post(
|
|
urlString: "\(baseUrl)/api/auth/refresh",
|
|
body: Body(refresh_token: refreshToken)
|
|
)
|
|
}
|
|
|
|
func logout(baseUrl: String, authorizationHeader: String, refreshToken: String) async throws {
|
|
struct Body: Encodable { let refresh_token: String }
|
|
let _: LogoutResponse = try await post(
|
|
urlString: "\(baseUrl)/api/auth/logout",
|
|
body: Body(refresh_token: refreshToken),
|
|
authHeader: authorizationHeader
|
|
)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func post<B: Encodable, R: Decodable>(
|
|
urlString: String,
|
|
body: B,
|
|
authHeader: String? = nil
|
|
) async throws -> R {
|
|
guard let url = URL(string: urlString) else {
|
|
throw AuthError.invalidServerUrl("Invalid URL: \(urlString)")
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
if let authHeader { request.setValue(authHeader, forHTTPHeaderField: "Authorization") }
|
|
request.httpBody = try JSONEncoder().encode(body)
|
|
|
|
let (data, response) = try await urlSession.data(for: request)
|
|
|
|
guard let http = response as? HTTPURLResponse else {
|
|
throw AuthError.invalidResponse
|
|
}
|
|
|
|
guard (200..<300).contains(http.statusCode) else {
|
|
let msg = (try? decoder.decode(ErrorResponse.self, from: data))?.error ?? ""
|
|
throw AuthError.serverError(http.statusCode, msg)
|
|
}
|
|
|
|
do {
|
|
return try decoder.decode(R.self, from: data)
|
|
} catch {
|
|
throw AuthError.invalidResponse
|
|
}
|
|
}
|
|
}
|