align iOS with Android: auth, SSO login, course selection, phone binding, 4 tabs

This commit is contained in:
2026-08-31 20:45:04 +08:00
parent f9f65988b7
commit b5934f21f5
5 changed files with 867 additions and 18 deletions

View 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)
}
}