feat: Enhance User model with roles and improved decoding, and add an API debugger to the profile view.
This commit is contained in:
@@ -72,3 +72,56 @@ struct ListResponse<T: Decodable>: Decodable {
|
||||
let error: String?
|
||||
let pagination: Pagination?
|
||||
}
|
||||
// MARK: - APIResponse Extensions
|
||||
// Extensions cho APIResponse
|
||||
|
||||
extension APIResponse {
|
||||
/// Unwrap data or throw error
|
||||
/// Unwrap data hoặc throw error
|
||||
/// - Returns: Unwrapped data / Dữ liệu đã unwrap
|
||||
/// - Throws: APIError if not successful / APIError nếu không thành công
|
||||
func unwrap() throws -> T {
|
||||
guard success else {
|
||||
throw APIError.serverError(
|
||||
statusCode: 400,
|
||||
message: error ?? "API request failed"
|
||||
)
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
throw APIError.noData
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/// Check if response is successful with data
|
||||
/// Kiểm tra response có thành công và có data không
|
||||
var isSuccessful: Bool {
|
||||
success && data != nil
|
||||
}
|
||||
}
|
||||
|
||||
extension ListResponse {
|
||||
/// Unwrap data or throw error
|
||||
/// Unwrap data hoặc throw error
|
||||
/// - Returns: Unwrapped data array / Mảng dữ liệu đã unwrap
|
||||
/// - Throws: APIError if not successful / APIError nếu không thành công
|
||||
func unwrap() throws -> [T] {
|
||||
guard success else {
|
||||
throw APIError.serverError(
|
||||
statusCode: 400,
|
||||
message: error ?? "API request failed"
|
||||
)
|
||||
}
|
||||
|
||||
return data ?? []
|
||||
}
|
||||
|
||||
/// Check if response is successful
|
||||
/// Kiểm tra response có thành công không
|
||||
var isSuccessful: Bool {
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -214,23 +214,58 @@ struct APITestView: View {
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
do {
|
||||
let user = try decoder.decode(User.self, from: data)
|
||||
testResult += """
|
||||
✅ Decoded User Successfully:
|
||||
ID: \(user.id)
|
||||
Email: \(user.email)
|
||||
Name: \(user.name)
|
||||
Avatar: \(user.avatarUrl ?? "nil")
|
||||
Phone: \(user.phoneNumber ?? "nil")
|
||||
Email Verified: \(user.isEmailVerified)
|
||||
"""
|
||||
} catch let decodingError {
|
||||
testResult += """
|
||||
❌ Decoding Failed:
|
||||
\(decodingError.localizedDescription)
|
||||
// First try to decode with wrapper
|
||||
// Thử decode với wrapper trước
|
||||
let response = try decoder.decode(APIResponse<User>.self, from: data)
|
||||
|
||||
if response.success, let user = response.data {
|
||||
testResult += """
|
||||
✅ Decoded User Successfully (with wrapper):
|
||||
Success: \(response.success)
|
||||
ID: \(user.id)
|
||||
Email: \(user.email)
|
||||
Name: \(user.name)
|
||||
Avatar: \(user.avatarUrl ?? "nil")
|
||||
Phone: \(user.phoneNumber ?? "nil")
|
||||
Email Verified: \(user.isEmailVerified)
|
||||
Roles: \(user.roles?.joined(separator: ", ") ?? "none")
|
||||
"""
|
||||
} else {
|
||||
testResult += """
|
||||
⚠️ Response decoded but no user data:
|
||||
Success: \(response.success)
|
||||
Error: \(response.error ?? "nil")
|
||||
"""
|
||||
}
|
||||
} catch let wrapperError {
|
||||
testResult += """
|
||||
❌ Decoding with wrapper failed, trying direct decode...
|
||||
Wrapper Error: \(wrapperError.localizedDescription)
|
||||
|
||||
💡 Check Raw JSON below để xem field nào bị thiếu hoặc sai tên
|
||||
"""
|
||||
|
||||
// Try direct decode without wrapper
|
||||
// Thử decode trực tiếp không có wrapper
|
||||
do {
|
||||
let user = try decoder.decode(User.self, from: data)
|
||||
testResult += """
|
||||
✅ Decoded User Successfully (direct, no wrapper):
|
||||
ID: \(user.id)
|
||||
Email: \(user.email)
|
||||
Name: \(user.name)
|
||||
Avatar: \(user.avatarUrl ?? "nil")
|
||||
Phone: \(user.phoneNumber ?? "nil")
|
||||
Email Verified: \(user.isEmailVerified)
|
||||
Roles: \(user.roles?.joined(separator: ", ") ?? "none")
|
||||
"""
|
||||
} catch let directError {
|
||||
testResult += """
|
||||
❌ Both decoding methods failed:
|
||||
Direct decode error: \(directError.localizedDescription)
|
||||
|
||||
💡 Check Raw JSON below để xem field nào bị thiếu hoặc sai tên
|
||||
"""
|
||||
}
|
||||
}
|
||||
} else if httpResponse.statusCode == 401 {
|
||||
testResult += "❌ Status: UNAUTHORIZED (Token invalid or expired)\n"
|
||||
|
||||
@@ -6,8 +6,19 @@
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
901364A42F19F3040097E0A7 /* DebugLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901364A32F19F3040097E0A7 /* DebugLogger.swift */; };
|
||||
901364A82F19F4830097E0A7 /* APITestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901364A72F19F4830097E0A7 /* APITestView.swift */; };
|
||||
901364AA2F19F5060097E0A7 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901364A92F19F5060097E0A7 /* APIResponse.swift */; };
|
||||
901364AE2F19F5D00097E0A7 /* APIUsageExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901364AD2F19F5D00097E0A7 /* APIUsageExamples.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
901363EA2F19DDEB0097E0A7 /* AppClientBaseSwift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppClientBaseSwift.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
901364A32F19F3040097E0A7 /* DebugLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLogger.swift; sourceTree = "<group>"; };
|
||||
901364A72F19F4830097E0A7 /* APITestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITestView.swift; sourceTree = "<group>"; };
|
||||
901364A92F19F5060097E0A7 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = "<group>"; };
|
||||
901364AD2F19F5D00097E0A7 /* APIUsageExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIUsageExamples.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
@@ -34,6 +45,10 @@
|
||||
children = (
|
||||
901363EC2F19DDEB0097E0A7 /* AppClientBaseSwift */,
|
||||
901363EB2F19DDEB0097E0A7 /* Products */,
|
||||
901364A32F19F3040097E0A7 /* DebugLogger.swift */,
|
||||
901364A72F19F4830097E0A7 /* APITestView.swift */,
|
||||
901364A92F19F5060097E0A7 /* APIResponse.swift */,
|
||||
901364AD2F19F5D00097E0A7 /* APIUsageExamples.swift */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -120,6 +135,10 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
901364A42F19F3040097E0A7 /* DebugLogger.swift in Sources */,
|
||||
901364A82F19F4830097E0A7 /* APITestView.swift in Sources */,
|
||||
901364AA2F19F5060097E0A7 /* APIResponse.swift in Sources */,
|
||||
901364AE2F19F5D00097E0A7 /* APIUsageExamples.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
Binary file not shown.
@@ -232,21 +232,6 @@ extension String {
|
||||
|
||||
return first + rest
|
||||
}
|
||||
|
||||
/// Pretty print JSON string
|
||||
/// In đẹp JSON string
|
||||
var prettyPrintedJSON: String? {
|
||||
guard let data = self.data(using: .utf8),
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let prettyData = try? JSONSerialization.data(
|
||||
withJSONObject: jsonObject,
|
||||
options: .prettyPrinted
|
||||
),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return prettyString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Example Usage
|
||||
|
||||
@@ -22,8 +22,8 @@ enum APIConfig {
|
||||
/// Tiền tố phiên bản API
|
||||
static let apiVersion = "/api/v1"
|
||||
|
||||
/// OAuth2 token endpoint (no version prefix)
|
||||
/// OAuth2 token endpoint (không có version prefix)
|
||||
/// OAuth2 token endpoint (no version prefix, routed through Traefik)
|
||||
/// OAuth2 token endpoint (không có version prefix, route qua Traefik)
|
||||
static let tokenEndpoint = "/connect/token"
|
||||
|
||||
/// OAuth2 client ID for password grant
|
||||
|
||||
@@ -46,6 +46,10 @@ struct User: Codable, Identifiable, Equatable {
|
||||
/// Last update date
|
||||
/// Ngày cập nhật cuối
|
||||
let updatedAt: Date?
|
||||
|
||||
/// User roles
|
||||
/// Các vai trò của người dùng
|
||||
let roles: [String]?
|
||||
|
||||
// MARK: - CodingKeys
|
||||
|
||||
@@ -60,6 +64,7 @@ struct User: Codable, Identifiable, Equatable {
|
||||
case isEmailVerified = "emailConfirmed"
|
||||
case createdAt
|
||||
case updatedAt
|
||||
case roles
|
||||
}
|
||||
|
||||
// MARK: - Custom Decoding
|
||||
@@ -69,8 +74,25 @@ struct User: Codable, Identifiable, Equatable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
email = try container.decode(String.self, forKey: .email)
|
||||
// Decode required fields with better error handling
|
||||
// Giải mã các field bắt buộc với xử lý lỗi tốt hơn
|
||||
guard let userId = try? container.decode(String.self, forKey: .id) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .id,
|
||||
in: container,
|
||||
debugDescription: "Missing or invalid 'id' field"
|
||||
)
|
||||
}
|
||||
id = userId
|
||||
|
||||
guard let userEmail = try? container.decode(String.self, forKey: .email) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .email,
|
||||
in: container,
|
||||
debugDescription: "Missing or invalid 'email' field"
|
||||
)
|
||||
}
|
||||
email = userEmail
|
||||
|
||||
// Handle name from either "name" field or "firstName" + "lastName"
|
||||
// Xử lý name từ field "name" hoặc "firstName" + "lastName"
|
||||
@@ -79,14 +101,25 @@ struct User: Codable, Identifiable, Equatable {
|
||||
} else {
|
||||
let firstName = try container.decodeIfPresent(String.self, forKey: .firstName) ?? ""
|
||||
let lastName = try container.decodeIfPresent(String.self, forKey: .lastName) ?? ""
|
||||
name = "\(firstName) \(lastName)".trimmingCharacters(in: .whitespaces)
|
||||
let combinedName = "\(firstName) \(lastName)".trimmingCharacters(in: .whitespaces)
|
||||
|
||||
// If both firstName and lastName are empty, use email as fallback
|
||||
// Nếu cả firstName và lastName đều rỗng, dùng email làm dự phòng
|
||||
name = combinedName.isEmpty ? email.components(separatedBy: "@").first ?? email : combinedName
|
||||
}
|
||||
|
||||
avatarUrl = try container.decodeIfPresent(String.self, forKey: .avatarUrl)
|
||||
phoneNumber = try container.decodeIfPresent(String.self, forKey: .phoneNumber)
|
||||
isEmailVerified = try container.decodeIfPresent(Bool.self, forKey: .isEmailVerified) ?? false
|
||||
|
||||
// Handle dates with multiple formats
|
||||
// Xử lý dates với nhiều format
|
||||
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt)
|
||||
updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt)
|
||||
|
||||
// Decode roles array
|
||||
// Giải mã mảng roles
|
||||
roles = try container.decodeIfPresent([String].self, forKey: .roles)
|
||||
}
|
||||
|
||||
// MARK: - Custom Encoding
|
||||
@@ -103,6 +136,7 @@ struct User: Codable, Identifiable, Equatable {
|
||||
try container.encode(isEmailVerified, forKey: .isEmailVerified)
|
||||
try container.encodeIfPresent(createdAt, forKey: .createdAt)
|
||||
try container.encodeIfPresent(updatedAt, forKey: .updatedAt)
|
||||
try container.encodeIfPresent(roles, forKey: .roles)
|
||||
}
|
||||
|
||||
// MARK: - Standard Init
|
||||
@@ -117,7 +151,8 @@ struct User: Codable, Identifiable, Equatable {
|
||||
phoneNumber: String? = nil,
|
||||
isEmailVerified: Bool = false,
|
||||
createdAt: Date? = nil,
|
||||
updatedAt: Date? = nil
|
||||
updatedAt: Date? = nil,
|
||||
roles: [String]? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.email = email
|
||||
@@ -127,6 +162,7 @@ struct User: Codable, Identifiable, Equatable {
|
||||
self.isEmailVerified = isEmailVerified
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
self.roles = roles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +197,18 @@ extension User {
|
||||
var maskedPhone: String? {
|
||||
phoneNumber?.maskedPhone
|
||||
}
|
||||
|
||||
/// Check if user has specific role
|
||||
/// Kiểm tra user có role cụ thể không
|
||||
func hasRole(_ role: String) -> Bool {
|
||||
roles?.contains(role) ?? false
|
||||
}
|
||||
|
||||
/// Check if user is admin
|
||||
/// Kiểm tra user có phải admin không
|
||||
var isAdmin: Bool {
|
||||
hasRole("admin") || hasRole("Admin") || hasRole("Administrator")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mock Data
|
||||
|
||||
@@ -56,32 +56,23 @@ enum APIError: Error, LocalizedError {
|
||||
|
||||
/// OAuth2 token response
|
||||
/// Response token OAuth2
|
||||
/// Note: No CodingKeys needed - decoder uses convertFromSnakeCase strategy
|
||||
/// Lưu ý: Không cần CodingKeys - decoder dùng convertFromSnakeCase tự động
|
||||
struct OAuthTokenResponse: Decodable {
|
||||
let accessToken: String
|
||||
let tokenType: String
|
||||
let expiresIn: Int
|
||||
let refreshToken: String?
|
||||
let scope: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case tokenType = "token_type"
|
||||
case expiresIn = "expires_in"
|
||||
case refreshToken = "refresh_token"
|
||||
case scope
|
||||
}
|
||||
}
|
||||
|
||||
/// OAuth2 error response
|
||||
/// Response lỗi OAuth2
|
||||
/// Note: No CodingKeys needed - decoder uses convertFromSnakeCase strategy
|
||||
/// Lưu ý: Không cần CodingKeys - decoder dùng convertFromSnakeCase tự động
|
||||
struct OAuthErrorResponse: Decodable {
|
||||
let error: String
|
||||
let errorDescription: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case error
|
||||
case errorDescription = "error_description"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HTTP Method
|
||||
@@ -205,6 +196,17 @@ final class APIService: APIServiceProtocol {
|
||||
request.httpBody = try encoder.encode(body)
|
||||
}
|
||||
|
||||
// Log request for debugging
|
||||
// Ghi log request để debug
|
||||
#if DEBUG
|
||||
DebugLogger.logRequest(
|
||||
method: method.rawValue,
|
||||
url: url.absoluteString,
|
||||
headers: request.allHTTPHeaderFields,
|
||||
body: request.httpBody
|
||||
)
|
||||
#endif
|
||||
|
||||
// Perform request
|
||||
// Thực hiện request
|
||||
let (data, response) = try await session.data(for: request)
|
||||
@@ -215,13 +217,46 @@ final class APIService: APIServiceProtocol {
|
||||
throw APIError.unknown
|
||||
}
|
||||
|
||||
// Log response for debugging
|
||||
// Ghi log response để debug
|
||||
#if DEBUG
|
||||
DebugLogger.logResponse(statusCode: httpResponse.statusCode, data: data)
|
||||
#endif
|
||||
|
||||
// Check status code
|
||||
// Kiểm tra status code
|
||||
switch httpResponse.statusCode {
|
||||
case 200...299:
|
||||
// Try to decode the response
|
||||
// Thử decode response
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
let result = try decoder.decode(T.self, from: data)
|
||||
|
||||
#if DEBUG
|
||||
print("✅ Decoded successfully: \(T.self)")
|
||||
#endif
|
||||
|
||||
return result
|
||||
} catch let decodingError as DecodingError {
|
||||
// Enhanced decoding error details
|
||||
// Chi tiết lỗi decoding được cải thiện
|
||||
let errorDetails = formatDecodingError(decodingError, data: data)
|
||||
|
||||
#if DEBUG
|
||||
print("❌ Decoding Error Details:")
|
||||
print(errorDetails)
|
||||
print("\n💡 Debugging Tips:")
|
||||
print("1. Check if server returns wrapper format: { success, data, error }")
|
||||
print("2. Verify all field names match between API and model")
|
||||
print("3. Use APITestView to see raw JSON response")
|
||||
print("4. Check if snake_case/camelCase conversion is working")
|
||||
#endif
|
||||
|
||||
throw APIError.decodingError(decodingError)
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("❌ Unknown decoding error: \(error)")
|
||||
#endif
|
||||
throw APIError.decodingError(error)
|
||||
}
|
||||
case 401:
|
||||
@@ -235,6 +270,9 @@ final class APIService: APIServiceProtocol {
|
||||
throw APIError.rateLimited
|
||||
default:
|
||||
let message = String(data: data, encoding: .utf8)
|
||||
#if DEBUG
|
||||
print("❌ Server Error (\(httpResponse.statusCode)): \(message ?? "No message")")
|
||||
#endif
|
||||
throw APIError.serverError(statusCode: httpResponse.statusCode, message: message)
|
||||
}
|
||||
}
|
||||
@@ -269,6 +307,59 @@ final class APIService: APIServiceProtocol {
|
||||
// MARK: - OAuth2 Methods
|
||||
// Các phương thức OAuth2
|
||||
|
||||
/// Format decoding error for debugging
|
||||
/// Format lỗi decoding cho debugging
|
||||
private func formatDecodingError(_ error: DecodingError, data: Data) -> String {
|
||||
var details = ""
|
||||
|
||||
switch error {
|
||||
case .typeMismatch(let type, let context):
|
||||
details = """
|
||||
🔴 Type Mismatch:
|
||||
- Expected type: \(type)
|
||||
- Coding path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> "))
|
||||
- Debug description: \(context.debugDescription)
|
||||
"""
|
||||
|
||||
case .valueNotFound(let type, let context):
|
||||
details = """
|
||||
🔴 Value Not Found:
|
||||
- Missing type: \(type)
|
||||
- Coding path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> "))
|
||||
- Debug description: \(context.debugDescription)
|
||||
"""
|
||||
|
||||
case .keyNotFound(let key, let context):
|
||||
details = """
|
||||
🔴 Key Not Found:
|
||||
- Missing key: \(key.stringValue)
|
||||
- Coding path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> "))
|
||||
- Debug description: \(context.debugDescription)
|
||||
"""
|
||||
|
||||
case .dataCorrupted(let context):
|
||||
details = """
|
||||
🔴 Data Corrupted:
|
||||
- Coding path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> "))
|
||||
- Debug description: \(context.debugDescription)
|
||||
"""
|
||||
|
||||
@unknown default:
|
||||
details = "🔴 Unknown decoding error: \(error)"
|
||||
}
|
||||
|
||||
// Add raw JSON response for debugging
|
||||
// Thêm raw JSON response cho debugging
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
details += "\n\n📄 Raw JSON Response:\n\(jsonString)"
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
// MARK: - OAuth2 Methods
|
||||
// Các phương thức OAuth2
|
||||
|
||||
/// POST form-urlencoded request (for OAuth2 token endpoint)
|
||||
/// Request POST dạng form-urlencoded (cho OAuth2 token endpoint)
|
||||
/// - Parameters:
|
||||
|
||||
@@ -160,11 +160,6 @@ final class AuthManager: ObservableObject {
|
||||
let password: String
|
||||
}
|
||||
|
||||
struct RegisterResponse: Decodable {
|
||||
let success: Bool
|
||||
let data: RegisterData?
|
||||
}
|
||||
|
||||
struct RegisterData: Decodable {
|
||||
let userId: String
|
||||
let email: String
|
||||
@@ -176,10 +171,17 @@ final class AuthManager: ObservableObject {
|
||||
email: email,
|
||||
password: password
|
||||
)
|
||||
let _: RegisterResponse = try await APIService.shared.post(
|
||||
|
||||
// Use APIResponse wrapper for register
|
||||
// Dùng APIResponse wrapper cho đăng ký
|
||||
let response: APIResponse<RegisterData> = try await APIService.shared.post(
|
||||
endpoint: "/auth/register",
|
||||
body: request
|
||||
)
|
||||
|
||||
// Unwrap response or throw error
|
||||
// Unwrap response hoặc throw error
|
||||
_ = try response.unwrap()
|
||||
|
||||
// Auto login after successful registration
|
||||
// Tự động đăng nhập sau khi đăng ký thành công
|
||||
@@ -218,7 +220,13 @@ final class AuthManager: ObservableObject {
|
||||
/// Lấy thông tin user hiện tại từ API
|
||||
@MainActor func fetchCurrentUser() async {
|
||||
do {
|
||||
let user: User = try await APIService.shared.get(endpoint: "/users/me")
|
||||
// Fetch user with wrapper response
|
||||
// Lấy user với wrapper response
|
||||
let response: APIResponse<User> = try await APIService.shared.get(endpoint: "/users/me")
|
||||
|
||||
// Unwrap response data
|
||||
// Unwrap dữ liệu response
|
||||
let user = try response.unwrap()
|
||||
|
||||
// Cache user data
|
||||
// Cache dữ liệu user
|
||||
@@ -227,8 +235,22 @@ final class AuthManager: ObservableObject {
|
||||
}
|
||||
|
||||
authState = .authenticated(user)
|
||||
print("✅ User fetched successfully: \(user.email)")
|
||||
|
||||
} catch let error as APIError {
|
||||
// Enhanced error logging
|
||||
// Ghi log lỗi chi tiết hơn
|
||||
print("❌ Failed to fetch user: \(error.localizedDescription)")
|
||||
|
||||
if case .decodingError(let decodingError) = error {
|
||||
print("💡 Suggestion: Check if the API response matches the APIResponse<User> structure")
|
||||
print(" Expected wrapper: { success: Bool, data: User, error: String?, pagination: Pagination? }")
|
||||
print(" Decoding error: \(decodingError)")
|
||||
}
|
||||
|
||||
authState = .unauthenticated
|
||||
} catch {
|
||||
print("Failed to fetch user: \(error)")
|
||||
print("❌ Failed to fetch user: \(error.localizedDescription)")
|
||||
authState = .unauthenticated
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,25 @@ final class AuthViewModel: ObservableObject {
|
||||
|
||||
do {
|
||||
try await AuthManager.shared.login(email: loginEmail, password: loginPassword)
|
||||
} catch let error as APIError {
|
||||
// More detailed error messages
|
||||
// Thông báo lỗi chi tiết hơn
|
||||
switch error {
|
||||
case .decodingError:
|
||||
errorMessage = "Đăng nhập thất bại: Lỗi dữ liệu từ server. Vui lòng liên hệ hỗ trợ."
|
||||
case .unauthorized:
|
||||
errorMessage = "Đăng nhập thất bại: Email hoặc mật khẩu không đúng"
|
||||
case .networkError:
|
||||
errorMessage = "Đăng nhập thất bại: Không thể kết nối đến server"
|
||||
case .serverError(let statusCode, let message):
|
||||
if statusCode == 400 {
|
||||
errorMessage = "Đăng nhập thất bại: Email hoặc mật khẩu không đúng"
|
||||
} else {
|
||||
errorMessage = "Đăng nhập thất bại: Lỗi server (\(statusCode))"
|
||||
}
|
||||
default:
|
||||
errorMessage = "Đăng nhập thất bại: \(error.localizedDescription)"
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Đăng nhập thất bại: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ enum ProfileMenuAction: String {
|
||||
case language
|
||||
case help
|
||||
case about
|
||||
case apiDebugger // Debug only
|
||||
case logout
|
||||
}
|
||||
|
||||
@@ -63,6 +64,10 @@ final class ProfileViewModel: ObservableObject {
|
||||
/// Show logout confirmation alert
|
||||
/// Hiển thị alert xác nhận đăng xuất
|
||||
@Published var showLogoutAlert: Bool = false
|
||||
|
||||
/// Show API debugger sheet
|
||||
/// Hiển thị sheet API debugger
|
||||
@Published var showAPIDebugger: Bool = false
|
||||
|
||||
/// Menu items for profile screen
|
||||
/// Các item menu cho màn hình profile
|
||||
@@ -147,6 +152,11 @@ final class ProfileViewModel: ObservableObject {
|
||||
// Navigate to about page
|
||||
// Điều hướng đến trang giới thiệu
|
||||
print("Navigate to about")
|
||||
|
||||
case .apiDebugger:
|
||||
// Show API debugger sheet
|
||||
// Hiển thị sheet API debugger
|
||||
showAPIDebugger = true
|
||||
|
||||
case .logout:
|
||||
// Show logout confirmation
|
||||
@@ -209,13 +219,32 @@ final class ProfileViewModel: ObservableObject {
|
||||
icon: "info.circle",
|
||||
action: .about
|
||||
),
|
||||
]
|
||||
|
||||
// Add API Debugger in DEBUG mode only
|
||||
// Chỉ thêm API Debugger trong chế độ DEBUG
|
||||
#if DEBUG
|
||||
menuItems.append(
|
||||
ProfileMenuItem(
|
||||
id: "api_debugger",
|
||||
title: "🔧 API Debugger",
|
||||
subtitle: "Debug only",
|
||||
icon: "wrench.and.screwdriver",
|
||||
action: .apiDebugger
|
||||
)
|
||||
)
|
||||
#endif
|
||||
|
||||
// Add logout at the end
|
||||
// Thêm logout ở cuối
|
||||
menuItems.append(
|
||||
ProfileMenuItem(
|
||||
id: "logout",
|
||||
title: "profile_logout".localized,
|
||||
subtitle: nil,
|
||||
icon: "rectangle.portrait.and.arrow.right",
|
||||
action: .logout
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ struct ProfileView: View {
|
||||
.refreshable {
|
||||
await viewModel.refreshUser()
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAPIDebugger) {
|
||||
APITestView()
|
||||
}
|
||||
.alert("profile_logout_title".localized, isPresented: $viewModel.showLogoutAlert) {
|
||||
Button("common_cancel".localized, role: .cancel) {}
|
||||
Button("profile_logout".localized, role: .destructive) {
|
||||
@@ -157,7 +160,11 @@ struct ProfileView: View {
|
||||
// Icon
|
||||
Image(systemName: item.icon)
|
||||
.font(.body)
|
||||
.foregroundStyle(item.action == .logout ? .red : .accentColor)
|
||||
.foregroundStyle(
|
||||
item.action == .logout ? .red :
|
||||
item.action == .apiDebugger ? .orange :
|
||||
.accentColor
|
||||
)
|
||||
.frame(width: 24)
|
||||
|
||||
// Title
|
||||
|
||||
20
note.md
20
note.md
@@ -23,6 +23,26 @@ curl -s -X POST "http://localhost/connect/token" \
|
||||
-d "password=Velik@2026" \
|
||||
-d "scope=openid profile email api offline_access" 2>&1 | jq .
|
||||
|
||||
|
||||
curl -X 'GET' \
|
||||
'http://localhost:5001/api/v1/users/me' \
|
||||
-H 'accept: text/plain' \
|
||||
-H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkY0NzI5RUQ2MDc2NzgwMjdBNEIzNUMxMDNFMzJBRERCIiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwOi8vaWFtLXNlcnZpY2UiLCJuYmYiOjE3Njg1MzczOTYsImlhdCI6MTc2ODUzNzM5NiwiZXhwIjoxNzY4NTM4Mjk2LCJhdWQiOlsiaWFtLWFwaSIsImh0dHA6Ly9pYW0tc2VydmljZS9yZXNvdXJjZXMiXSwic2NvcGUiOlsiYXBpIiwiZW1haWwiLCJvcGVuaWQiLCJwcm9maWxlIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbInB3ZCJdLCJjbGllbnRfaWQiOiJwYXNzd29yZC1jbGllbnQiLCJzdWIiOiIyYTFjOTUzNC02YTU5LTQ5NGEtOGFlMy1kNWNjMTU4NDg2ODYiLCJhdXRoX3RpbWUiOjE3Njg1MzczOTUsImlkcCI6ImxvY2FsIiwiZW1haWwiOiJob25nb2NoYWkxMEBpY2xvdWQuY29tIiwibmFtZSI6ImhvbmdvY2hhaTEwQGljbG91ZC5jb20iLCJqdGkiOiJCQkMxRkY1MzY5QjlENkI1QTFBQzZFMzY1NENFQzcyMCJ9.BcGgy8EayZ3P4NCCtAkoyRcSnzXsLyQVn6RcGKFS5_rqPp7_jP1BSZ7OtKAHK-RQopTa4jJxP5wUoYh41n6nAmugki58w8UtjxU5v-IoKzPc4jWsEUt5yEKMP9TunqakQlYSPHfzCiLutVYCZVPqpvmYUoK4j1nGObbdC8NzIoJiHTlQthmaVPd8Loe3aan783P_ed0TzownB5uJ2d_EUBAa47VTW3NtcniZYem4U797hY7lgLGwcuJJ5ybFyisWnmu-7MHz-QiVUozfWaK1NQTJqKsytDbmHjCIFdsu2b7UBHQIdw2wIrXNktoHWT_5B590oXBwbYPTeDKCuJRTtg'
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "2a1c9534-6a59-494a-8ae3-d5cc15848686",
|
||||
"email": "hongochai10@icloud.com",
|
||||
"name": "hongochai10@icloud.com",
|
||||
"roles": []
|
||||
},
|
||||
"error": null,
|
||||
"pagination": null
|
||||
}
|
||||
|
||||
|
||||
|
||||
1. Kiểm tra hỗ trợ cho MSSQL, PSQL, MongoDB
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user