align iOS with Android: auth, SSO login, course selection, phone binding, 4 tabs
This commit is contained in:
172
ios/OralTrainer/Services/AuthAPI.swift
Normal file
172
ios/OralTrainer/Services/AuthAPI.swift
Normal file
@@ -0,0 +1,172 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user