docs: Thêm tài liệu kiến trúc ARCHITECTURE.md và cập nhật README.md.

This commit is contained in:
Ho Ngoc Hai
2026-01-16 10:53:16 +07:00
parent f62464bc36
commit c437ea4c9f
5 changed files with 642 additions and 116 deletions

View File

@@ -14,19 +14,28 @@ import SwiftUI
/// API configuration constants
/// Các hng s cu hình API
enum APIConfig {
/// Base URL for API requests
/// URL gc cho các request API
static let baseURL = "https://api.goodgo.vn"
/// Base URL for API requests (Traefik gateway)
/// URL gc cho các request API (Traefik gateway)
static let baseURL = "http://localhost"
/// API version prefix
/// Tin t phiên bn API
static let apiVersion = "/api/v1"
/// OAuth2 token endpoint (no version prefix)
/// OAuth2 token endpoint (không có version prefix)
static let tokenEndpoint = "/connect/token"
/// OAuth2 scope for authentication
/// OAuth2 scope cho xác thc
static let oauthScope = "openid profile email offline_access"
/// Request timeout in seconds
/// Thi gian timeout request (giây)
static let timeout: TimeInterval = 30.0
}
// MARK: - App Constants
// Hng s ng dng

View File

@@ -51,6 +51,39 @@ enum APIError: Error, LocalizedError {
}
}
// MARK: - OAuth2 Response Models
// Models response OAuth2
/// OAuth2 token response
/// Response token OAuth2
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 li OAuth2
struct OAuthErrorResponse: Decodable {
let error: String
let errorDescription: String?
enum CodingKeys: String, CodingKey {
case error
case errorDescription = "error_description"
}
}
// MARK: - HTTP Method
// Phương thc HTTP
@@ -232,4 +265,61 @@ final class APIService: APIServiceProtocol {
func delete<T: Decodable>(endpoint: String) async throws -> T {
try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil)
}
// MARK: - OAuth2 Methods
// Các phương thc OAuth2
/// POST form-urlencoded request (for OAuth2 token endpoint)
/// Request POST dng form-urlencoded (cho OAuth2 token endpoint)
/// - Parameters:
/// - endpoint: Token endpoint path / Đưng dn token endpoint
/// - formData: Form data dictionary / Dictionary form data
/// - Returns: Decoded response / Response đã gii mã
func postForm<T: Decodable>(endpoint: String, formData: [String: String]) async throws -> T {
guard let url = URL(string: APIConfig.baseURL + endpoint) else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = APIConfig.timeout
// Encode form data
// Mã hóa form data
let formBody = formData.map { key, value in
let escapedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
return "\(key)=\(escapedValue)"
}.joined(separator: "&")
request.httpBody = formBody.data(using: .utf8)
// Perform request
// Thc hin request
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.unknown
}
// Handle OAuth2 error responses
// X lý OAuth2 error responses
if httpResponse.statusCode != 200 {
if let errorResponse = try? decoder.decode(OAuthErrorResponse.self, from: data) {
throw APIError.serverError(
statusCode: httpResponse.statusCode,
message: errorResponse.errorDescription ?? errorResponse.error
)
}
let message = String(data: data, encoding: .utf8)
throw APIError.serverError(statusCode: httpResponse.statusCode, message: message)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw APIError.decodingError(error)
}
}
try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil)
}
}