6de2455917
- 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
255 lines
7.0 KiB
Swift
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)
|
|
}
|
|
}
|