feat: token
This commit is contained in:
parent
be25d07d83
commit
fdd2715ec8
Binary file not shown.
49
wake/Models/AuthState.swift
Normal file
49
wake/Models/AuthState.swift
Normal file
@ -0,0 +1,49 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// 管理用户认证状态的类
|
||||
public class AuthState: ObservableObject {
|
||||
@Published public var isAuthenticated: Bool = false {
|
||||
didSet {
|
||||
print("🔔 认证状态变更: \(isAuthenticated ? "已登录" : "已登出")")
|
||||
}
|
||||
}
|
||||
@Published public var isLoading = false
|
||||
@Published public var errorMessage: String?
|
||||
@Published public var user: User?
|
||||
|
||||
// 单例模式
|
||||
public static let shared = AuthState()
|
||||
|
||||
private init() {}
|
||||
|
||||
/// 登录成功时调用
|
||||
public func login(user: User? = nil) {
|
||||
if let user = user {
|
||||
self.user = user
|
||||
}
|
||||
isAuthenticated = true
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 登出时调用
|
||||
public func logout() {
|
||||
print("👋 用户登出")
|
||||
user = nil
|
||||
isAuthenticated = false
|
||||
|
||||
// 清除用户数据
|
||||
TokenManager.shared.clearTokens()
|
||||
UserDefaults.standard.removeObject(forKey: "lastLoginUser")
|
||||
}
|
||||
|
||||
/// 更新加载状态
|
||||
public func setLoading(_ loading: Bool) {
|
||||
isLoading = loading
|
||||
}
|
||||
|
||||
/// 设置错误信息
|
||||
public func setError(_ message: String) {
|
||||
errorMessage = message
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,10 @@ public enum APIConfig {
|
||||
<<<<<<< HEAD
|
||||
public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1"
|
||||
|
||||
/// 认证 token - 从 Keychain 中获取
|
||||
/// 获取认证token
|
||||
public static var authToken: String {
|
||||
let token = KeychainHelper.getAccessToken() ?? ""
|
||||
if !token.isEmpty {
|
||||
print("🔑 [APIConfig] 当前访问令牌: \(token.prefix(10))...") // 只打印前10个字符,避免敏感信息完全暴露
|
||||
} else {
|
||||
if token.isEmpty {
|
||||
print("⚠️ [APIConfig] 未找到访问令牌")
|
||||
}
|
||||
return token
|
||||
@ -42,11 +40,17 @@ public enum APIConfig {
|
||||
>>>>>>> 1814789 (feat: 登录接口联调)
|
||||
/// 认证请求头
|
||||
public static var authHeaders: [String: String] {
|
||||
return [
|
||||
"Authorization": "Bearer \(authToken)",
|
||||
let token = authToken
|
||||
var headers = [
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
]
|
||||
|
||||
if !token.isEmpty {
|
||||
headers["Authorization"] = "Bearer \(token)"
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
class Network: ObservableObject {
|
||||
@Published var users: [User] = []
|
||||
|
||||
func getUsers() {
|
||||
guard let url = URL(string: "http://192.168.31.156:31646/api/iam/login/password-login") else { fatalError("Missing URL") }
|
||||
|
||||
let urlRequest = URLRequest(url: url)
|
||||
|
||||
let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
|
||||
if let error = error {
|
||||
print("Request error: ", error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response as? HTTPURLResponse else { return }
|
||||
|
||||
if response.statusCode == 200 {
|
||||
guard let data = data else { return }
|
||||
DispatchQueue.main.async {
|
||||
do {
|
||||
let decodedUsers = try JSONDecoder().decode([User].self, from: data)
|
||||
self.users = decodedUsers
|
||||
} catch let error {
|
||||
print("Error decoding: ", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
}
|
||||
}
|
||||
207
wake/Utils/NetworkService.swift
Normal file
207
wake/Utils/NetworkService.swift
Normal file
@ -0,0 +1,207 @@
|
||||
import Foundation
|
||||
|
||||
enum NetworkError: Error {
|
||||
case invalidURL
|
||||
case noData
|
||||
case decodingError(Error)
|
||||
case serverError(String)
|
||||
case unauthorized
|
||||
case other(Error)
|
||||
case networkError(Error)
|
||||
case unknownError(Error)
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return "无效的URL"
|
||||
case .noData:
|
||||
return "没有收到数据"
|
||||
case .decodingError(let error):
|
||||
return "数据解析错误: \(error.localizedDescription)"
|
||||
case .serverError(let message):
|
||||
return "服务器错误: \(message)"
|
||||
case .unauthorized:
|
||||
return "未授权,请重新登录"
|
||||
case .other(let error):
|
||||
return error.localizedDescription
|
||||
case .networkError(let error):
|
||||
return "网络请求错误: \(error.localizedDescription)"
|
||||
case .unknownError(let error):
|
||||
return "未知错误: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkService {
|
||||
static let shared = NetworkService()
|
||||
|
||||
// 默认请求头
|
||||
private let defaultHeaders: [String: String] = [
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
]
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - 基础请求方法
|
||||
private func request<T: Decodable>(
|
||||
_ method: String,
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
// 构建URL
|
||||
guard let url = URL(string: APIConfig.baseURL + path) else {
|
||||
completion(.failure(.invalidURL))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
|
||||
// 设置请求头 - 合并默认头、认证头和自定义头
|
||||
defaultHeaders.forEach { key, value in
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// 添加认证头
|
||||
APIConfig.authHeaders.forEach { key, value in
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// 添加自定义头(如果提供)
|
||||
headers?.forEach { key, value in
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// 设置请求体(如果是POST/PUT请求)
|
||||
if let parameters = parameters, (method == "POST" || method == "PUT") {
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
||||
} catch {
|
||||
completion(.failure(.other(error)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 打印请求信息(调试用)
|
||||
print("🌐 [Network] \(method) \(url.absoluteString)")
|
||||
if let headers = request.allHTTPHeaderFields {
|
||||
print("📤 Headers: \(headers)")
|
||||
}
|
||||
if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) {
|
||||
print("📦 Body: \(bodyString)")
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
||||
// 处理响应
|
||||
self?.handleResponse(data: data, response: response, error: error, completion: completion)
|
||||
}
|
||||
|
||||
// 开始请求
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private func handleResponse<T: Decodable>(
|
||||
data: Data?,
|
||||
response: URLResponse?,
|
||||
error: Error?,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
// 打印响应信息(调试用)
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
print("📥 [Network] Status: \(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))")
|
||||
if let headers = httpResponse.allHeaderFields as? [String: Any] {
|
||||
print("📥 Headers: \(headers)")
|
||||
}
|
||||
|
||||
// 检查状态码
|
||||
if !(200...299).contains(httpResponse.statusCode) {
|
||||
print("❌ [Network] 请求失败,状态码: \(httpResponse.statusCode)")
|
||||
if let data = data, let errorResponse = String(data: data, encoding: .utf8) {
|
||||
print("❌ [Network] 错误响应: \(errorResponse)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理网络错误
|
||||
if let error = error {
|
||||
print("❌ [Network] 网络请求错误: \(error.localizedDescription)")
|
||||
completion(.failure(.networkError(error)))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数据是否存在
|
||||
guard let data = data else {
|
||||
print("❌ [Network] 没有收到数据")
|
||||
completion(.failure(.noData))
|
||||
return
|
||||
}
|
||||
|
||||
// 打印响应数据(调试用)
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
print("📥 [Network] 响应数据: \(responseString)")
|
||||
}
|
||||
|
||||
do {
|
||||
// 解析JSON数据
|
||||
let decoder = JSONDecoder()
|
||||
let result = try decoder.decode(T.self, from: data)
|
||||
completion(.success(result))
|
||||
} catch let decodingError as DecodingError {
|
||||
print("❌ [Network] JSON解析失败: \(decodingError.localizedDescription)")
|
||||
if let dataString = String(data: data, encoding: .utf8) {
|
||||
print("📋 [Network] 原始响应: \(dataString)")
|
||||
}
|
||||
completion(.failure(.decodingError(decodingError)))
|
||||
} catch {
|
||||
print("❌ [Network] 未知错误: \(error.localizedDescription)")
|
||||
completion(.failure(.unknownError(error)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 公共方法
|
||||
|
||||
/// GET 请求
|
||||
func get<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
request("GET", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
/// POST 请求
|
||||
func post<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
request("POST", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
/// DELETE 请求
|
||||
func delete<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
request("DELETE", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
/// PUT 请求
|
||||
func put<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
request("PUT", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
}
|
||||
424
wake/Utils/TokenManager.swift
Normal file
424
wake/Utils/TokenManager.swift
Normal file
@ -0,0 +1,424 @@
|
||||
import Foundation
|
||||
|
||||
/// Token管理器
|
||||
/// 负责管理应用的认证令牌,包括验证、刷新和过期处理
|
||||
class TokenManager {
|
||||
/// 单例实例
|
||||
static let shared = TokenManager()
|
||||
|
||||
/// token有效期阈值(秒),在token即将过期前进行刷新
|
||||
/// 例如:设置为300表示在token过期前5分钟开始刷新
|
||||
private let tokenValidityThreshold: TimeInterval = 300
|
||||
|
||||
/// 私有化初始化方法,确保单例模式
|
||||
private init() {}
|
||||
|
||||
// MARK: - Token 状态检查
|
||||
|
||||
/// 检查是否存在有效的访问令牌
|
||||
var hasToken: Bool {
|
||||
return KeychainHelper.getAccessToken()?.isEmpty == false
|
||||
}
|
||||
|
||||
// MARK: - Token 验证
|
||||
|
||||
/// 验证并刷新token(如果需要)
|
||||
/// - 检查token是否存在
|
||||
/// - 检查token是否有效
|
||||
/// - 在token即将过期时自动刷新
|
||||
/// - Parameter completion: 完成回调,返回验证/刷新结果
|
||||
/// - isValid: token是否有效
|
||||
/// - error: 错误信息(如果有)
|
||||
func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool, Error?) -> Void) {
|
||||
// 1. 检查token是否存在
|
||||
guard let token = KeychainHelper.getAccessToken(), !token.isEmpty else {
|
||||
// token不存在,返回未授权错误
|
||||
let error = NSError(
|
||||
domain: "TokenManager",
|
||||
code: 401,
|
||||
userInfo: [NSLocalizedDescriptionKey: "未找到访问令牌"]
|
||||
)
|
||||
completion(false, error)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查token是否有效
|
||||
if isTokenValid(token) {
|
||||
// token有效,直接返回成功
|
||||
completion(true, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. token无效或即将过期,尝试刷新
|
||||
refreshToken { [weak self] success, error in
|
||||
if success {
|
||||
// 刷新成功,返回成功
|
||||
completion(true, nil)
|
||||
} else {
|
||||
// 刷新失败,返回错误信息
|
||||
let finalError = error ?? NSError(
|
||||
domain: "TokenManager",
|
||||
code: 401,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Token刷新失败"]
|
||||
)
|
||||
completion(false, finalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查token是否有效
|
||||
/// - Parameter token: 要检查的token字符串
|
||||
/// - Returns: 如果token有效返回true,否则返回false
|
||||
///
|
||||
/// 该方法会检查token的有效性,包括检查token是否为空、是否过期以及通过网络请求验证token。
|
||||
///
|
||||
/// - Note: 该方法会打印一些调试信息,包括token验证开始、token过期时间等。
|
||||
public func isTokenValid(_ token: String) -> Bool {
|
||||
print("🔍 TokenManager: 开始验证token...")
|
||||
|
||||
// 1. 基础验证:检查token是否为空
|
||||
guard !token.isEmpty else {
|
||||
print("❌ TokenManager: Token为空")
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 检查token是否过期(如果可能)
|
||||
if let expiryDate = getTokenExpiryDate(token) {
|
||||
print("⏰ TokenManager: Token过期时间: \(expiryDate)")
|
||||
if Date() > expiryDate {
|
||||
print("❌ TokenManager: Token已过期")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建信号量用于同步网络请求
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var isValid = false
|
||||
var requestCompleted = false
|
||||
|
||||
print("🌐 TokenManager: 发送验证请求到服务器...")
|
||||
|
||||
// 4. 发送验证请求
|
||||
let task = URLSession.shared.dataTask(with: createValidationRequest(token: token)) { data, response, error in
|
||||
defer {
|
||||
requestCompleted = true
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
// 检查网络错误
|
||||
if let error = error {
|
||||
print("❌ TokenManager: 验证请求错误: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查响应状态码
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
print("❌ TokenManager: 无效的服务器响应")
|
||||
return
|
||||
}
|
||||
|
||||
print("📡 TokenManager: 服务器响应状态码: \(httpResponse.statusCode)")
|
||||
|
||||
// 检查状态码
|
||||
guard (200...299).contains(httpResponse.statusCode) else {
|
||||
print("❌ TokenManager: 服务器返回错误状态码: \(httpResponse.statusCode)")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有数据
|
||||
if let data = data, !data.isEmpty {
|
||||
do {
|
||||
// 尝试解析响应数据
|
||||
let response = try JSONDecoder().decode(IdentityCheckResponse.self, from: data)
|
||||
isValid = response.isValid
|
||||
print("✅ TokenManager: Token验证\(isValid ? "成功" : "失败")")
|
||||
} catch {
|
||||
print("❌ TokenManager: 解析响应数据失败: \(error.localizedDescription)")
|
||||
// 如果解析失败但状态码是200,我们假设token是有效的
|
||||
isValid = true
|
||||
print("ℹ️ TokenManager: 状态码200,假设token有效")
|
||||
}
|
||||
} else {
|
||||
// 如果没有返回数据但状态码是200,我们假设token是有效的
|
||||
print("ℹ️ TokenManager: 没有返回数据,但状态码为200,假设token有效")
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
// 5. 设置超时时间(10秒)
|
||||
let timeoutResult = semaphore.wait(timeout: .now() + 15)
|
||||
|
||||
// 检查是否超时
|
||||
if !requestCompleted && timeoutResult == .timedOut {
|
||||
print("⚠️ TokenManager: 验证请求超时")
|
||||
task.cancel()
|
||||
return false
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
/// 创建验证请求
|
||||
private func createValidationRequest(token: String) -> URLRequest {
|
||||
let url = URL(string: APIConfig.baseURL + "/iam/identity-check")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
return request
|
||||
}
|
||||
|
||||
/// 从token中提取过期时间(示例实现)
|
||||
private func getTokenExpiryDate(_ token: String) -> Date? {
|
||||
// 这里需要根据实际的JWT或其他token格式来解析过期时间
|
||||
// 以下是JWT token的示例解析
|
||||
let parts = token.components(separatedBy: ".")
|
||||
guard parts.count > 1, let payloadData = base64UrlDecode(parts[1]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
if let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
|
||||
let exp = payload["exp"] as? TimeInterval {
|
||||
return Date(timeIntervalSince1970: exp)
|
||||
}
|
||||
} catch {
|
||||
print("❌ TokenManager: 解析token过期时间失败: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func base64UrlDecode(_ base64Url: String) -> Data? {
|
||||
var base64 = base64Url
|
||||
.replacingOccurrences(of: "-", with: "+")
|
||||
.replacingOccurrences(of: "_", with: "/")
|
||||
|
||||
// 添加必要的填充
|
||||
let length = Double(base64.lengthOfBytes(using: .utf8))
|
||||
let requiredLength = 4 * ceil(length / 4.0)
|
||||
let paddingLength = requiredLength - length
|
||||
if paddingLength > 0 {
|
||||
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
|
||||
base64 += padding
|
||||
}
|
||||
|
||||
return Data(base64Encoded: base64)
|
||||
}
|
||||
|
||||
/// 刷新token
|
||||
/// - Parameter completion: 刷新完成回调
|
||||
/// - success: 是否刷新成功
|
||||
/// - error: 错误信息(如果有)
|
||||
private func refreshToken(completion: @escaping (Bool, Error?) -> Void) {
|
||||
// 获取刷新令牌
|
||||
guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else {
|
||||
// 没有可用的刷新令牌
|
||||
let error = NSError(
|
||||
domain: "TokenManager",
|
||||
code: 401,
|
||||
userInfo: [NSLocalizedDescriptionKey: "未找到刷新令牌"]
|
||||
)
|
||||
completion(false, error)
|
||||
return
|
||||
}
|
||||
|
||||
// 准备刷新请求参数
|
||||
let parameters: [String: Any] = [
|
||||
"refresh_token": refreshToken,
|
||||
"grant_type": "refresh_token"
|
||||
]
|
||||
|
||||
// 发送刷新请求
|
||||
NetworkService.shared.postWithToken(path: "/v1/iam/access-token-refresh", parameters: parameters) {
|
||||
(result: Result<TokenResponse, NetworkError>) in
|
||||
|
||||
switch result {
|
||||
case .success(let tokenResponse):
|
||||
// 1. 保存新的访问令牌
|
||||
KeychainHelper.saveAccessToken(tokenResponse.accessToken)
|
||||
|
||||
// 2. 如果返回了新的刷新令牌,也保存起来
|
||||
if let newRefreshToken = tokenResponse.refreshToken {
|
||||
KeychainHelper.saveRefreshToken(newRefreshToken)
|
||||
}
|
||||
|
||||
print("✅ Token刷新成功")
|
||||
completion(true, nil)
|
||||
|
||||
case .failure(let error):
|
||||
print("❌ Token刷新失败: \(error.localizedDescription)")
|
||||
|
||||
// 刷新失败,清除本地token,需要用户重新登录
|
||||
KeychainHelper.clearTokens()
|
||||
|
||||
completion(false, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除所有存储的 token
|
||||
func clearTokens() {
|
||||
print("🗑️ TokenManager: 清除所有 token")
|
||||
KeychainHelper.clearTokens()
|
||||
// 清除其他与 token 相关的存储
|
||||
UserDefaults.standard.removeObject(forKey: "tokenExpiryDate")
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Token响应模型
|
||||
/// 用于解析token刷新接口的响应数据
|
||||
private struct TokenResponse: Codable {
|
||||
/// 访问令牌
|
||||
let accessToken: String
|
||||
|
||||
/// 刷新令牌(可选)
|
||||
let refreshToken: String?
|
||||
|
||||
/// 过期时间(秒)
|
||||
let expiresIn: TimeInterval?
|
||||
|
||||
/// 令牌类型(如:Bearer)
|
||||
let tokenType: String?
|
||||
|
||||
// 使用CodingKeys自定义键名映射
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case refreshToken = "refresh_token"
|
||||
case expiresIn = "expires_in"
|
||||
case tokenType = "token_type"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 身份验证响应模型
|
||||
/// 用于解析身份验证接口的响应数据
|
||||
private struct IdentityCheckResponse: Codable {
|
||||
/// 是否有效
|
||||
let isValid: Bool
|
||||
|
||||
/// 用户ID(可选)
|
||||
let userId: String?
|
||||
|
||||
/// 过期时间(可选)
|
||||
let expiresAt: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case isValid = "is_valid"
|
||||
case userId = "user_id"
|
||||
case expiresAt = "expires_at"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NetworkService扩展
|
||||
/// 为NetworkService添加带token验证的请求方法
|
||||
extension NetworkService {
|
||||
// MARK: - 核心请求方法
|
||||
|
||||
/// 带token验证的网络请求
|
||||
/// - Parameters:
|
||||
/// - method: HTTP方法(GET/POST/PUT/DELETE等)
|
||||
/// - path: 接口路径
|
||||
/// - parameters: 请求参数
|
||||
/// - headers: 自定义请求头
|
||||
/// - completion: 完成回调
|
||||
func requestWithToken<T: Decodable>(
|
||||
_ method: String,
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
// 1. 验证并刷新token
|
||||
TokenManager.shared.validateAndRefreshTokenIfNeeded { [weak self] isValid, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if isValid {
|
||||
// 2. token有效,继续发送原始请求
|
||||
switch method.uppercased() {
|
||||
case "GET":
|
||||
self.get(path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
case "POST":
|
||||
self.post(path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
case "PUT":
|
||||
self.put(path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
case "DELETE":
|
||||
self.delete(path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
default:
|
||||
let error = NSError(
|
||||
domain: "NetworkService",
|
||||
code: 400,
|
||||
userInfo: [NSLocalizedDescriptionKey: "不支持的HTTP方法: \(method)"]
|
||||
)
|
||||
completion(.failure(.other(error)))
|
||||
}
|
||||
} else {
|
||||
// 3. token无效,返回未授权错误
|
||||
let error = error ?? NSError(
|
||||
domain: "NetworkService",
|
||||
code: 401,
|
||||
userInfo: [NSLocalizedDescriptionKey: "未授权访问"]
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.unauthorized))
|
||||
}
|
||||
|
||||
// 4. 发送登出通知,让应用处理未授权情况
|
||||
NotificationCenter.default.post(name: .userDidLogout, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 便捷方法
|
||||
|
||||
/// 带token验证的GET请求
|
||||
func getWithToken<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
requestWithToken("GET", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
/// 带token验证的POST请求
|
||||
func postWithToken<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
requestWithToken("POST", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
/// 带token验证的PUT请求
|
||||
func putWithToken<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
requestWithToken("PUT", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
|
||||
/// 带token验证的DELETE请求
|
||||
func deleteWithToken<T: Decodable>(
|
||||
path: String,
|
||||
parameters: [String: Any]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||
) {
|
||||
requestWithToken("DELETE", path: path, parameters: parameters, headers: headers, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 通知名称
|
||||
/// 定义应用中使用的通知名称
|
||||
extension Notification.Name {
|
||||
/// 用户登出通知
|
||||
/// 当token失效或用户主动登出时发送
|
||||
static let userDidLogout = Notification.Name("UserDidLogoutNotification")
|
||||
}
|
||||
@ -1,31 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
struct User: Identifiable, Decodable {
|
||||
var id: Int
|
||||
var name: String
|
||||
var username: String
|
||||
var email: String
|
||||
var address: Address
|
||||
var phone: String
|
||||
var website: String
|
||||
var company: Company
|
||||
public struct User: Identifiable, Decodable {
|
||||
public var id: Int
|
||||
public var name: String
|
||||
public var username: String
|
||||
public var email: String
|
||||
public var address: Address
|
||||
public var phone: String
|
||||
public var website: String
|
||||
public var company: Company
|
||||
|
||||
struct Address: Decodable {
|
||||
var street: String
|
||||
var suite: String
|
||||
var city: String
|
||||
var zipcode: String
|
||||
var geo: Geo
|
||||
public init(id: Int, name: String, username: String, email: String, address: Address, phone: String, website: String, company: Company) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.address = address
|
||||
self.phone = phone
|
||||
self.website = website
|
||||
self.company = company
|
||||
}
|
||||
|
||||
struct Geo: Decodable {
|
||||
var lat: String
|
||||
var lng: String
|
||||
public struct Address: Decodable {
|
||||
public var street: String
|
||||
public var suite: String
|
||||
public var city: String
|
||||
public var zipcode: String
|
||||
public var geo: Geo
|
||||
|
||||
public init(street: String, suite: String, city: String, zipcode: String, geo: Geo) {
|
||||
self.street = street
|
||||
self.suite = suite
|
||||
self.city = city
|
||||
self.zipcode = zipcode
|
||||
self.geo = geo
|
||||
}
|
||||
|
||||
public struct Geo: Decodable {
|
||||
public var lat: String
|
||||
public var lng: String
|
||||
|
||||
public init(lat: String, lng: String) {
|
||||
self.lat = lat
|
||||
self.lng = lng
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Company: Decodable {
|
||||
var name: String
|
||||
var catchPhrase: String
|
||||
var bs: String
|
||||
public struct Company: Decodable {
|
||||
public var name: String
|
||||
public var catchPhrase: String
|
||||
public var bs: String
|
||||
|
||||
public init(name: String, catchPhrase: String, bs: String) {
|
||||
self.name = name
|
||||
self.catchPhrase = catchPhrase
|
||||
self.bs = bs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ struct UserInfo: View {
|
||||
@State private var darkModeEnabled = false
|
||||
@State private var showLogoutAlert = false
|
||||
@State private var avatarImage: UIImage? // Add this line
|
||||
@State private var name: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@ -39,53 +40,49 @@ struct UserInfo: View {
|
||||
.font(Typography.font(for: .title))
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
// Avatar
|
||||
ZStack {
|
||||
// Show either the SVG or the uploaded image
|
||||
if let avatarImage = avatarImage {
|
||||
Image(uiImage: avatarImage)
|
||||
// Avatar section
|
||||
VStack {
|
||||
Text("your name")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
// Avatar image or placeholder
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 100, height: 100)
|
||||
.overlay(
|
||||
Image(systemName: "person.fill")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 200, height: 200)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
SVGImage(svgName: "Avatar")
|
||||
.frame(width: 200, height: 200)
|
||||
.scaledToFit()
|
||||
.foregroundColor(.white)
|
||||
.padding(30)
|
||||
)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
|
||||
// Make sure the AvatarUploader is on top and covers the entire area
|
||||
AvatarUploader(selectedImage: $avatarImage, size: 200)
|
||||
.contentShape(Rectangle()) // This makes the entire area tappable
|
||||
}
|
||||
.frame(width: 200, height: 200)
|
||||
.padding(.vertical, 20)
|
||||
// Name input field
|
||||
TextField("Enter your name", text: $name)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding(.horizontal, 40)
|
||||
.padding(.top, 20)
|
||||
|
||||
// Buttons
|
||||
Spacer()
|
||||
|
||||
// Next/Open button
|
||||
Button(action: {
|
||||
// Action for first button
|
||||
// Action for open button
|
||||
}) {
|
||||
Text("Upload from Gallery")
|
||||
Text("Open")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.foregroundColor(.black)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
|
||||
)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
// Action for second button
|
||||
}) {
|
||||
Text("Take a Photo")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.foregroundColor(.black)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
|
||||
.fill(Color(red: 1.0, green: 0.714, blue: 0.271))
|
||||
)
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.white))
|
||||
|
||||
92
wake/View/Welcome/SplashView.swift
Normal file
92
wake/View/Welcome/SplashView.swift
Normal file
@ -0,0 +1,92 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SplashView: View {
|
||||
@State private var isAnimating = false
|
||||
@State private var showLogin = false
|
||||
@EnvironmentObject private var authState: AuthState
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
// 背景渐变
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Theme.Colors.primary, // Primary color with some transparency
|
||||
Theme.Colors.primaryDark, // Darker shade of the primary color
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
VStack(spacing: 50) {
|
||||
Spacer()
|
||||
// 欢迎文字动画
|
||||
Text("Welcome")
|
||||
.font(.system(size: 40, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
.scaleEffect(isAnimating ? 1.1 : 0.9)
|
||||
.opacity(isAnimating ? 1 : 0.3)
|
||||
.animation(
|
||||
.easeInOut(duration: 1.5)
|
||||
.repeatForever(autoreverses: true),
|
||||
value: isAnimating
|
||||
)
|
||||
|
||||
// 动画图标
|
||||
Image(systemName: "moon.stars.fill")
|
||||
.font(.system(size: 120))
|
||||
.foregroundColor(.accentColor)
|
||||
.rotationEffect(.degrees(isAnimating ? 360 : 0))
|
||||
.scaleEffect(isAnimating ? 1.2 : 0.8)
|
||||
.animation(
|
||||
.easeInOut(duration: 2)
|
||||
.repeatForever(autoreverses: true),
|
||||
value: isAnimating
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 圆形按钮
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showLogin = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 140, height: 140)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.accentColor.opacity(0.7)) // 80% opacity
|
||||
.shadow(radius: 10)
|
||||
)
|
||||
}
|
||||
.padding(.bottom, 40)
|
||||
.background(
|
||||
NavigationLink(destination: LoginView().environmentObject(authState), isActive: $showLogin) {
|
||||
EmptyView()
|
||||
}
|
||||
.hidden()
|
||||
)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
isAnimating = true
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
// 预览
|
||||
#if DEBUG
|
||||
struct SplashView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SplashView()
|
||||
.environmentObject(AuthState.shared)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,20 +1,12 @@
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import SwiftData
|
||||
|
||||
@main
|
||||
struct WakeApp: App {
|
||||
// init() {
|
||||
// // 打印所有可用的字体
|
||||
// print("\n=== 所有可用的字体 ===")
|
||||
// for family in UIFont.familyNames.sorted() {
|
||||
// print("\n\(family):")
|
||||
// for name in UIFont.fontNames(forFamilyName: family).sorted() {
|
||||
// print(" - \(name)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@StateObject private var authState = AuthState.shared
|
||||
@State private var showSplash = true
|
||||
|
||||
// 使用更简单的方式创建模型容器
|
||||
let container: ModelContainer
|
||||
|
||||
@ -32,26 +24,77 @@ struct WakeApp: App {
|
||||
// 3. 重新创建容器
|
||||
container = try! ModelContainer(for: Login.self)
|
||||
}
|
||||
}
|
||||
|
||||
// 配置网络层
|
||||
configureNetwork()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ZStack {
|
||||
if showSplash {
|
||||
// 显示启动页
|
||||
SplashView()
|
||||
.environmentObject(authState)
|
||||
.onAppear {
|
||||
// 启动页显示时检查token有效性
|
||||
checkTokenValidity()
|
||||
}
|
||||
} else {
|
||||
// 根据登录状态显示不同视图
|
||||
if authState.isAuthenticated {
|
||||
// 已登录:显示主界面
|
||||
ContentView()
|
||||
// SettingsView()
|
||||
// 导航栏按钮
|
||||
// TabView{
|
||||
// ContentView()
|
||||
// .tabItem{
|
||||
// Label("wake", systemImage: "book")
|
||||
// }
|
||||
// SettingView()
|
||||
// .tabItem{
|
||||
// Label("setting", systemImage: "gear")
|
||||
.environmentObject(authState)
|
||||
} else {
|
||||
// 未登录:显示登录界面
|
||||
ContentView()
|
||||
.environmentObject(authState)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// 3秒后自动隐藏启动页
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
// withAnimation {
|
||||
// showSplash = false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// 注入模型容器到环境中
|
||||
}
|
||||
.modelContainer(container)
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
/// 配置网络层
|
||||
private func configureNetwork() {
|
||||
// 配置网络请求超时时间等
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = 30
|
||||
configuration.timeoutIntervalForResource = 60
|
||||
|
||||
// 可以在这里添加其他网络配置
|
||||
}
|
||||
|
||||
/// 检查token有效性
|
||||
private func checkTokenValidity() {
|
||||
guard TokenManager.shared.hasToken,
|
||||
let token = KeychainHelper.getAccessToken() else {
|
||||
showSplash = false
|
||||
return
|
||||
}
|
||||
|
||||
// 检查token是否有效
|
||||
if TokenManager.shared.isTokenValid(token) {
|
||||
authState.isAuthenticated = true
|
||||
}
|
||||
|
||||
// 3秒后自动隐藏启动页
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
// withAnimation {
|
||||
// showSplash = false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user