From 070d1e94b39b86297e9df9dd4918ad369079825a Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Fri, 16 Jan 2026 18:44:18 +0700 Subject: [PATCH] feat: Enhance User model with roles and improved decoding, and add an API debugger to the profile view. --- .../AppClientBaseSwift/APIResponse.swift | 53 ++++++++ .../AppClientBaseSwift/APITestView.swift | 65 +++++++--- .../project.pbxproj | 19 +++ .../UserInterfaceState.xcuserstate | Bin 53153 -> 62710 bytes .../Core/Constants/APIResponseDebugger.swift | 15 --- .../Core/Constants/Constants.swift | 4 +- .../AppClientBaseSwift/Models/User.swift | 56 ++++++++- .../Services/APIService.swift | 119 +++++++++++++++--- .../Services/AuthManager.swift | 38 ++++-- .../ViewModels/AuthViewModel.swift | 19 +++ .../ViewModels/ProfileViewModel.swift | 33 ++++- .../Views/Screens/ProfileView.swift | 9 +- note.md | 20 +++ 13 files changed, 389 insertions(+), 61 deletions(-) diff --git a/apps/app-client-base-swift/AppClientBaseSwift/APIResponse.swift b/apps/app-client-base-swift/AppClientBaseSwift/APIResponse.swift index 3459d108..caf52ef6 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/APIResponse.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/APIResponse.swift @@ -72,3 +72,56 @@ struct ListResponse: 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 + } +} + diff --git a/apps/app-client-base-swift/AppClientBaseSwift/APITestView.swift b/apps/app-client-base-swift/AppClientBaseSwift/APITestView.swift index eee3c2ae..46bb38c1 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/APITestView.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/APITestView.swift @@ -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.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" diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.pbxproj b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.pbxproj index 617fd7fe..162b062c 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.pbxproj +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.pbxproj @@ -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 = ""; }; + 901364A72F19F4830097E0A7 /* APITestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITestView.swift; sourceTree = ""; }; + 901364A92F19F5060097E0A7 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = ""; }; + 901364AD2F19F5D00097E0A7 /* APIUsageExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIUsageExamples.swift; sourceTree = ""; }; /* 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 = ""; }; @@ -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; }; diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate index f079310fa51683fb5344d117b1ae5bc55d718fc2..59fb8e8aab478801a3f6bbf53065b08ec17d6835 100644 GIT binary patch literal 62710 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%0D4Pys;`2qr53z_X$LTcPza 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 D@g0cX diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/APIResponseDebugger.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/APIResponseDebugger.swift index 0845718e..fe112a9a 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/APIResponseDebugger.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/APIResponseDebugger.swift @@ -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 diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/Constants.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/Constants.swift index 28ab91ee..59d62427 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/Constants.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Core/Constants/Constants.swift @@ -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 diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift index 53a969a0..4b3565dd 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift @@ -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 diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift index da355fe1..de6b21b2 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift @@ -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: diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift index 6204c40b..d3429765 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift @@ -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 = 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 = 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 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 } } diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift index 825b6dbd..56a9407b 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift @@ -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)" } diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/ProfileViewModel.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/ProfileViewModel.swift index 88f44951..adfb5872 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/ProfileViewModel.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/ProfileViewModel.swift @@ -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 - ), - ] + ) + ) } } diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/ProfileView.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/ProfileView.swift index deecfe42..0c5428c9 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/ProfileView.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/ProfileView.swift @@ -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 diff --git a/note.md b/note.md index 597df68a..bd400a8f 100644 --- a/note.md +++ b/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