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 f079310f..59fb8e8a 100644 Binary files a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate and b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate differ 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