Files
boc/aamos-admin-upgrade/ios/AamosAdmin/Services/APIService.swift
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

255 lines
7.0 KiB
Swift

import Foundation
import Security
import Combine
// MARK: - Models
struct User: Codable, Identifiable {
let id: String
let email: String
let name: String
let role: UserRole
let createdAt: Date
let lastLoginAt: Date?
let active: Bool
enum UserRole: String, Codable {
case admin
case `operator`
case viewer
}
}
struct Module: Codable, Identifiable {
let id: String
let name: String
let description: String
var enabled: Bool
let version: String
let status: ModuleStatus
enum ModuleStatus: String, Codable {
case running, stopped, error, degraded
}
}
struct SystemMetrics: Codable {
let cpuPercent: Double
let memoryPercent: Double
let diskPercent: Double
let requestsPerMinute: Int
let activeConnections: Int
let errorRatePercent: Double
let timestamp: Date
}
struct ServerSettings: Codable, Equatable {
var systemName: String
var timezone: String
var jwtExpiryHours: Int
var twoFactorEnabled: Bool
var maintenanceMode: Bool
var allowRegistration: Bool
var webhookUrl: String?
}
struct APIError: Decodable, Error, LocalizedError {
let message: String
let code: String?
var errorDescription: String? { message }
}
private struct LoginPayload: Encodable {
let email: String
let password: String
}
private struct LoginResponse: Decodable {
let token: String
let user: User
let expiresAt: Date?
}
private struct TogglePayload: Encodable {
let enabled: Bool
}
// MARK: - APIService
@MainActor
final class APIService: ObservableObject {
static let shared = APIService()
@Published private(set) var isAuthenticated: Bool
@Published private(set) var currentUser: User?
private let auth = AuthService.shared
private let session: URLSession = {
let cfg = URLSessionConfiguration.default
cfg.timeoutIntervalForRequest = 30
cfg.timeoutIntervalForResource = 90
cfg.waitsForConnectivity = true
return URLSession(configuration: cfg)
}()
private let decoder: JSONDecoder = {
let d = JSONDecoder()
d.keyDecodingStrategy = .convertFromSnakeCase
d.dateDecodingStrategy = .iso8601
return d
}()
private let encoder: JSONEncoder = {
let e = JSONEncoder()
e.keyEncodingStrategy = .convertToSnakeCase
e.dateEncodingStrategy = .iso8601
return e
}()
private static let baseURLKey = "com.aamos.admin.baseURL"
static let defaultBaseURL = "https://amos.wavult.com"
var baseURL: String {
get { UserDefaults.standard.string(forKey: Self.baseURLKey) ?? Self.defaultBaseURL }
set { UserDefaults.standard.set(newValue, forKey: Self.baseURLKey) }
}
private init() {
isAuthenticated = AuthService.shared.isAuthenticated
}
// MARK: - Request building
private func makeRequest(_ path: String, method: String = "GET", body: Data? = nil) throws -> URLRequest {
guard let url = URL(string: baseURL + path) else {
throw URLError(.badURL)
}
var req = URLRequest(url: url)
req.httpMethod = method
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("application/json", forHTTPHeaderField: "Accept")
if let token = auth.load() {
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
req.httpBody = body
return req
}
// MARK: - Response dispatch
private func send<T: Decodable>(_ req: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: req)
guard let http = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
if http.statusCode == 401 {
await logout()
throw APIError(message: "Session utgången. Logga in igen.", code: "UNAUTHORIZED")
}
guard (200..<300).contains(http.statusCode) else {
if let err = try? decoder.decode(APIError.self, from: data) { throw err }
throw APIError(
message: "Servern svarade med statuskod \(http.statusCode).",
code: "HTTP_\(http.statusCode)"
)
}
return try decoder.decode(T.self, from: data)
}
// MARK: - Auth
func login(email: String, password: String) async throws {
let body = try encoder.encode(LoginPayload(email: email, password: password))
let req = try makeRequest("/api/v1/auth/login", method: "POST", body: body)
let resp: LoginResponse = try await send(req)
try auth.save(token: resp.token)
currentUser = resp.user
isAuthenticated = true
}
func logout() async {
if isAuthenticated, let req = try? makeRequest("/api/v1/auth/logout", method: "POST") {
_ = try? await session.data(for: req)
}
auth.logout()
currentUser = nil
isAuthenticated = false
}
// MARK: - Users
func getMe() async throws -> User {
let req = try makeRequest("/api/v1/users/me")
let user: User = try await send(req)
currentUser = user
return user
}
func getUsers() async throws -> [User] {
let req = try makeRequest("/api/v1/users")
return try await send(req)
}
// MARK: - Dashboard
func getDashboard() async throws -> DashboardMetrics {
let req = try makeRequest("/api/v1/dashboard")
return try await send(req)
}
// MARK: - Modules
func getModules() async throws -> [Module] {
let req = try makeRequest("/api/v1/modules")
return try await send(req)
}
func toggleModule(id: String, enabled: Bool) async throws -> Module {
let body = try encoder.encode(TogglePayload(enabled: enabled))
let req = try makeRequest("/api/v1/modules/\(id)", method: "PATCH", body: body)
return try await send(req)
}
// MARK: - Audit
func getAudit(page: Int = 1, limit: Int = 50) async throws -> [AuditEntry] {
let req = try makeRequest("/api/v1/audit?page=\(page)&limit=\(limit)")
return try await send(req)
}
// MARK: - Metrics
func getMetrics() async throws -> SystemMetrics {
let req = try makeRequest("/api/v1/metrics")
return try await send(req)
}
// MARK: - Settings
func getSettings() async throws -> ServerSettings {
let req = try makeRequest("/api/v1/settings")
return try await send(req)
}
func updateSettings(_ settings: ServerSettings) async throws -> ServerSettings {
let body = try encoder.encode(settings)
let req = try makeRequest("/api/v1/settings", method: "PUT", body: body)
return try await send(req)
}
}
// MARK: - APIServiceProtocol conformance
extension APIService: APIServiceProtocol {
func fetchMetrics() async throws -> DashboardMetrics {
try await getDashboard()
}
func fetchRecentAudit(limit: Int) async throws -> [AuditEntry] {
try await getAudit(limit: limit)
}
}