Login and Home

This commit is contained in:
Daniel Arantes Loverde
2026-02-04 16:55:29 -03:00
parent 78daaf1927
commit 33fc111459
90 changed files with 1707 additions and 303 deletions

View 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
}
}

View 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
}
}
}

View 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?
}

View 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)
}
}