Login and Home
This commit is contained in:
104
pedi-foods/Sources/PediFoods/Services/ApiClient.swift
Normal file
104
pedi-foods/Sources/PediFoods/Services/ApiClient.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
enum NetworkError: Error, LocalizedError {
|
||||
case invalidURL
|
||||
case invalidResponse
|
||||
case httpError(Int, String?)
|
||||
case decodeError
|
||||
case rateLimited
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL: return "URL invalida"
|
||||
case .invalidResponse: return "Resposta invalida do servidor"
|
||||
case .httpError(let code, let message):
|
||||
return message ?? "Erro HTTP (\(code))"
|
||||
case .decodeError: return "Erro ao interpretar dados"
|
||||
case .rateLimited: return "Muitas requisicoes. Tente novamente."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ApiRequest {
|
||||
let path: String
|
||||
let method: String
|
||||
let module: ApiModule
|
||||
let requiresAuth: Bool
|
||||
let queryItems: [URLQueryItem]
|
||||
let body: Data?
|
||||
|
||||
init(path: String,
|
||||
method: String = "GET",
|
||||
module: ApiModule = .none,
|
||||
requiresAuth: Bool = true,
|
||||
queryItems: [URLQueryItem] = [],
|
||||
body: Data? = nil) {
|
||||
self.path = path
|
||||
self.method = method
|
||||
self.module = module
|
||||
self.requiresAuth = requiresAuth
|
||||
self.queryItems = queryItems
|
||||
self.body = body
|
||||
}
|
||||
}
|
||||
|
||||
final class ApiClient {
|
||||
private let session: URLSession
|
||||
private let tokenStore: TokenStore
|
||||
|
||||
init(session: URLSession = .shared, tokenStore: TokenStore = DefaultTokenStore()) {
|
||||
self.session = session
|
||||
self.tokenStore = tokenStore
|
||||
}
|
||||
|
||||
func send<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||
let url = try buildURL(path: request.path, query: request.queryItems)
|
||||
var urlRequest = URLRequest(url: url)
|
||||
urlRequest.httpMethod = request.method
|
||||
urlRequest.httpBody = request.body
|
||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token = ApiConfig.token(for: request.module) {
|
||||
urlRequest.setValue(token, forHTTPHeaderField: "Atomenta-Token")
|
||||
}
|
||||
if request.requiresAuth, let jwt = tokenStore.jwt {
|
||||
urlRequest.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
let (data, response) = try await session.data(for: urlRequest)
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
if http.statusCode == 429 {
|
||||
throw NetworkError.rateLimited
|
||||
}
|
||||
|
||||
if !(200...299).contains(http.statusCode) {
|
||||
let message = String(data: data, encoding: .utf8)
|
||||
throw NetworkError.httpError(http.statusCode, message)
|
||||
}
|
||||
|
||||
do {
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
} catch {
|
||||
throw NetworkError.decodeError
|
||||
}
|
||||
}
|
||||
|
||||
private func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
|
||||
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
components.path = components.path.appending(path)
|
||||
if !query.isEmpty {
|
||||
components.queryItems = query
|
||||
}
|
||||
guard let url = components.url else { throw NetworkError.invalidURL }
|
||||
return url
|
||||
}
|
||||
}
|
||||
30
pedi-foods/Sources/PediFoods/Services/ApiConfig.swift
Normal file
30
pedi-foods/Sources/PediFoods/Services/ApiConfig.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
enum ApiModule {
|
||||
case app
|
||||
case customer
|
||||
case store
|
||||
case resource
|
||||
case none
|
||||
}
|
||||
|
||||
enum ApiConfig {
|
||||
static var baseURL: URL {
|
||||
let raw = ProcessInfo.processInfo.environment["ATOMENTA_API_URL"] ?? "http://127.0.0.1:3001"
|
||||
return URL(string: raw) ?? URL(string: "http://127.0.0.1:3001")!
|
||||
}
|
||||
|
||||
// Tokens provided by backend modules
|
||||
static let storeToken = "550e8400-e29b-41d4-a716-446655440008"
|
||||
static let customerToken = "550e8400-e29b-41d4-a716-44665544000a"
|
||||
static let resourceToken = "550e8400-e29b-41d4-a716-446655440009"
|
||||
|
||||
static func token(for module: ApiModule) -> String? {
|
||||
switch module {
|
||||
case .store: return storeToken
|
||||
case .customer: return customerToken
|
||||
case .resource: return resourceToken
|
||||
case .app, .none: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
88
pedi-foods/Sources/PediFoods/Services/ApiService.swift
Normal file
88
pedi-foods/Sources/PediFoods/Services/ApiService.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
|
||||
struct ApiEnvelope<T: Decodable>: Decodable {
|
||||
let error: Bool
|
||||
let code: String?
|
||||
let message: String?
|
||||
let result: T?
|
||||
}
|
||||
|
||||
final class ApiService {
|
||||
private let client: ApiClient
|
||||
private var tokenStore: TokenStore
|
||||
|
||||
init(client: ApiClient = ApiClient(), tokenStore: TokenStore = DefaultTokenStore()) {
|
||||
self.client = client
|
||||
self.tokenStore = tokenStore
|
||||
}
|
||||
|
||||
// MARK: - Auth
|
||||
|
||||
func requestOtp(email: String) async throws -> ApiEnvelope<EmptyResult> {
|
||||
let body = try JSONEncoder().encode(["email": email])
|
||||
let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body)
|
||||
return try await client.send(req)
|
||||
}
|
||||
|
||||
func validateOtp(email: String, otp: String) async throws -> ApiEnvelope<LoginResult> {
|
||||
let body = try JSONEncoder().encode(["email": email, "otp": otp])
|
||||
let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body)
|
||||
let response: ApiEnvelope<LoginResult> = try await client.send(req)
|
||||
if let token = response.result?.token {
|
||||
tokenStore.jwt = token
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func profile() async throws -> ApiEnvelope<CustomerProfile> {
|
||||
let req = ApiRequest(path: "/api/customer/profile", method: "GET", module: .customer, requiresAuth: true)
|
||||
return try await client.send(req)
|
||||
}
|
||||
|
||||
// MARK: - Stores
|
||||
|
||||
func listStores(lat: Double, lng: Double, category: String? = nil, search: String? = nil) async throws -> ApiEnvelope<[StoreSummary]> {
|
||||
var items = [
|
||||
URLQueryItem(name: "lat", value: String(lat)),
|
||||
URLQueryItem(name: "lng", value: String(lng))
|
||||
]
|
||||
if let category {
|
||||
items.append(URLQueryItem(name: "category", value: category))
|
||||
}
|
||||
if let search {
|
||||
items.append(URLQueryItem(name: "search", value: search))
|
||||
}
|
||||
let req = ApiRequest(path: "/api/app/stores", method: "GET", module: .app, requiresAuth: true, queryItems: items)
|
||||
return try await client.send(req)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DTOs
|
||||
|
||||
struct EmptyResult: Decodable {}
|
||||
|
||||
struct LoginResult: Decodable {
|
||||
let token: String
|
||||
let customer: CustomerProfile?
|
||||
}
|
||||
|
||||
struct CustomerProfile: Decodable {
|
||||
let id: String
|
||||
let name: String
|
||||
let email: String
|
||||
let phoneNumber: String?
|
||||
}
|
||||
|
||||
struct StoreSummary: Decodable {
|
||||
let id: String
|
||||
let name: String
|
||||
let logo: String?
|
||||
let cover: String?
|
||||
let category: String?
|
||||
let rating: Double?
|
||||
let deliveryTime: String?
|
||||
let deliveryFee: Double?
|
||||
let distance: Double?
|
||||
let isOpen: Bool?
|
||||
let statusLabel: String?
|
||||
}
|
||||
20
pedi-foods/Sources/PediFoods/Services/TokenStore.swift
Normal file
20
pedi-foods/Sources/PediFoods/Services/TokenStore.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
protocol TokenStore: AnyObject {
|
||||
var jwt: String? { get set }
|
||||
func clear()
|
||||
}
|
||||
|
||||
final class DefaultTokenStore: TokenStore {
|
||||
private let key = "auth_jwt"
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
var jwt: String? {
|
||||
get { defaults.string(forKey: key) }
|
||||
set { defaults.set(newValue, forKey: key) }
|
||||
}
|
||||
|
||||
func clear() {
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user