import Foundation // MARK: - Models struct AuthUser: Codable { let id: String let username: String let nickname: String } struct AuthSession: Codable { let token: String let user: AuthUser } struct SsoExchangeResult: Codable { let token: String let user: AuthUser let phoneBound: Bool let isNewUser: Bool private enum CodingKeys: String, CodingKey { case token, user case phoneBound = "phone_bound" case isNewUser = "is_new_user" } } struct Course: Identifiable, Codable { let videoHash: String let title: String let durationMs: Int64? let language: String? let sentenceCount: Int let streamUrl: String let enrolled: Bool var id: String { videoHash } private enum CodingKeys: String, CodingKey { case title, language, enrolled case videoHash = "video_hash" case durationMs = "duration_ms" case sentenceCount = "sentence_count" case streamUrl = "stream_url" } func toVideoSummary() -> TrainingVideoSummary { TrainingVideoSummary( videoHash: videoHash, title: title, streamUrl: streamUrl, durationMs: durationMs, sizeBytes: 0, language: language, sentenceCount: sentenceCount, status: "ready" ) } } // MARK: - Auth Error enum AuthError: LocalizedError { case badStatus(Int, String) var errorDescription: String? { switch self { case .badStatus(let code, let body): return "HTTP \(code): \(body.prefix(200))" } } } // MARK: - Auth API enum AuthAPI { private static var baseURL: String { AppConfig.serverBaseURL } // MARK: Login / Register static func login(username: String, password: String) async throws -> AuthSession { try await authRequest(path: "/api/v1/auth/login", body: [ "username": username, "password": password, ]) } static func register( username: String, password: String, nickname: String, phone: String ) async throws -> AuthSession { try await authRequest(path: "/api/v1/auth/register", body: [ "username": username, "password": password, "nickname": nickname, "phone": phone, ]) } // MARK: SSO static func ssoExchange(jwtToken: String) async throws -> SsoExchangeResult { let url = URL(string: "\(baseURL)/api/v1/auth/sso/exchange")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: ["access_token": jwtToken]) let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { let status = (response as? HTTPURLResponse)?.statusCode ?? 0 let body = String(data: data, encoding: .utf8) ?? "" throw AuthError.badStatus(status, body) } return try JSONDecoder().decode(SsoExchangeResult.self, from: data) } static func ssoBindPhone(token: String, phone: String) async throws { let url = URL(string: "\(baseURL)/api/v1/auth/sso/bind-phone")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.httpBody = try JSONSerialization.data(withJSONObject: ["phone": phone]) let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { let status = (response as? HTTPURLResponse)?.statusCode ?? 0 let body = String(data: data, encoding: .utf8) ?? "" throw AuthError.badStatus(status, body) } } // MARK: Courses static func courses(token: String) async throws -> [Course] { let url = URL(string: "\(baseURL)/api/v1/courses")! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { let status = (response as? HTTPURLResponse)?.statusCode ?? 0 let body = String(data: data, encoding: .utf8) ?? "" throw AuthError.badStatus(status, body) } return try JSONDecoder().decode([Course].self, from: data) } // MARK: Private private static func authRequest(path: String, body: [String: Any]) async throws -> AuthSession { let url = URL(string: "\(baseURL)\(path)")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { let status = (response as? HTTPURLResponse)?.statusCode ?? 0 let body = String(data: data, encoding: .utf8) ?? "" throw AuthError.badStatus(status, body) } return try JSONDecoder().decode(AuthSession.self, from: data) } }