bugger/Sources/Services/Feishu/FeishuAuthService.swift

134 lines
5.2 KiB
Swift

import Foundation
final class FeishuAuthService {
private let baseURL = "https://open.feishu.cn/open-apis"
private let appId: String
private let appSecret: String
private let redirectURI = "bugger://oauth/callback"
private let session: URLSession
private let decoder: JSONDecoder
private var cachedTenantToken: String?
private var tenantTokenExpiry: Date?
init(session: URLSession = .shared) {
self.session = session
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder = decoder
let id = Bundle.main.object(forInfoDictionaryKey: "FEISHU_APP_ID") as? String ?? ""
let secret = Bundle.main.object(forInfoDictionaryKey: "FEISHU_APP_SECRET") as? String ?? ""
self.appId = id
self.appSecret = secret
}
var hasCredentials: Bool {
!appId.isEmpty && !appSecret.isEmpty && appId != "$(FEISHU_APP_ID)"
}
var authorizeURL: URL {
var components = URLComponents(string: "\(baseURL)/authen/v1/authorize")!
components.queryItems = [
URLQueryItem(name: "app_id", value: appId),
URLQueryItem(name: "redirect_uri", value: redirectURI),
URLQueryItem(name: "scope", value: "bitable:app:readonly")
]
return components.url!
}
func exchangeCode(_ code: String) async throws -> OAuthTokenData {
let tenantToken = try await tenantAccessToken()
let url = URL(string: "\(baseURL)/authen/v1/oidc/access_token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(tenantToken)", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode([
"grant_type": "authorization_code",
"code": code
])
return try await decodeAPIResponse(request)
}
func refreshAccessToken(_ refreshToken: String) async throws -> OAuthTokenData {
let tenantToken = try await tenantAccessToken()
let url = URL(string: "\(baseURL)/authen/v1/oidc/refresh_access_token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(tenantToken)", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode([
"grant_type": "refresh_token",
"refresh_token": refreshToken
])
return try await decodeAPIResponse(request)
}
func fetchCurrentUserName(accessToken: String) async throws -> String {
let url = URL(string: "\(baseURL)/authen/v1/user_info")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
let response: FeishuAPIResponse<UserInfoData> = try await send(request)
guard let data = response.data else {
throw FeishuError.apiError(code: response.code, message: response.msg)
}
return data.name ?? data.enName ?? "Unknown"
}
private func tenantAccessToken() async throws -> String {
if let cachedTenantToken,
let tenantTokenExpiry,
tenantTokenExpiry > Date().addingTimeInterval(60) {
return cachedTenantToken
}
guard hasCredentials else {
throw FeishuError.missingCredentials
}
let url = URL(string: "\(baseURL)/auth/v3/tenant_access_token/internal")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode([
"app_id": appId,
"app_secret": appSecret
])
let response: FeishuAPIResponse<TenantAccessTokenData> = try await send(request)
guard let data = response.data else {
throw FeishuError.apiError(code: response.code, message: response.msg)
}
cachedTenantToken = data.tenantAccessToken
tenantTokenExpiry = Date().addingTimeInterval(TimeInterval(data.expire))
return data.tenantAccessToken
}
private func decodeAPIResponse<T: Decodable>(_ request: URLRequest) async throws -> T {
let response: FeishuAPIResponse<T> = try await send(request)
guard response.code == 0, let data = response.data else {
throw FeishuError.apiError(code: response.code, message: response.msg)
}
return data
}
private func send<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw FeishuError.networkError(nil)
}
if httpResponse.statusCode == 401 {
throw FeishuError.unauthorized
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw FeishuError.decodingError(error)
}
}
}