5bc81d5b3b
iOS SwiftUI app with Supabase auth/realtime, Node.js backend, Docker/Supabase self-hosted infrastructure, and APNs scheduler. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.3 KiB
Swift
128 lines
4.3 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
actor APIService {
|
|
static let shared = APIService()
|
|
|
|
// Change to your server URL
|
|
private let baseURL = URL(string: "https://api.nightly.app/")!
|
|
|
|
private var authToken: String? { KeychainService.shared.getToken() }
|
|
|
|
// MARK: - Auth
|
|
|
|
func login(username: String, password: String) async throws {
|
|
let r: AuthResponse = try await post("auth/login", body: [
|
|
"username": username, "password": password
|
|
])
|
|
KeychainService.shared.saveToken(r.token)
|
|
}
|
|
|
|
func register(username: String, password: String, displayName: String) async throws {
|
|
let r: AuthResponse = try await post("auth/register", body: [
|
|
"username": username, "password": password, "displayName": displayName
|
|
])
|
|
KeychainService.shared.saveToken(r.token)
|
|
}
|
|
|
|
func getCurrentUser() async throws -> User {
|
|
try await get("users/me")
|
|
}
|
|
|
|
// MARK: - Posts
|
|
|
|
func getFeed() async throws -> [Post] {
|
|
try await get("posts/feed")
|
|
}
|
|
|
|
func createPost(content: String, mood: Mood, isAnonymous: Bool) async throws {
|
|
let _: EmptyResponse = try await post("posts", body: [
|
|
"content": content,
|
|
"mood": mood.rawValue,
|
|
"isAnonymous": isAnonymous
|
|
])
|
|
}
|
|
|
|
func resonate(postId: String) async throws {
|
|
let _: EmptyResponse = try await post("posts/\(postId)/resonate", body: [:])
|
|
}
|
|
|
|
func unresoante(postId: String) async throws {
|
|
let _: EmptyResponse = try await delete("posts/\(postId)/resonate")
|
|
}
|
|
|
|
func sendWhisper(toUserId: String, content: String) async throws {
|
|
let _: EmptyResponse = try await post("users/\(toUserId)/whisper", body: ["content": content])
|
|
}
|
|
|
|
// MARK: - Users
|
|
|
|
func getUserPosts(userId: String) async throws -> [Post] {
|
|
try await get("users/\(userId)/posts")
|
|
}
|
|
|
|
func getUserStreak(userId: String) async throws -> Int {
|
|
let r: StreakResponse = try await get("users/\(userId)/streak")
|
|
return r.streak
|
|
}
|
|
|
|
func registerPushToken(_ token: String) async {
|
|
_ = try? await post("users/me/push-token", body: ["token": token]) as EmptyResponse
|
|
}
|
|
|
|
// MARK: - HTTP
|
|
|
|
private func get<T: Decodable>(_ path: String) async throws -> T {
|
|
try await perform(makeRequest("GET", path: path))
|
|
}
|
|
|
|
@discardableResult
|
|
private func post<T: Decodable>(_ path: String, body: [String: Any]) async throws -> T {
|
|
var req = makeRequest("POST", path: path)
|
|
req.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
return try await perform(req)
|
|
}
|
|
|
|
@discardableResult
|
|
private func delete<T: Decodable>(_ path: String) async throws -> T {
|
|
try await perform(makeRequest("DELETE", path: path))
|
|
}
|
|
|
|
private func makeRequest(_ method: String, path: String) -> URLRequest {
|
|
var req = URLRequest(url: baseURL.appendingPathComponent(path))
|
|
req.httpMethod = method
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
if let t = authToken { req.setValue("Bearer \(t)", forHTTPHeaderField: "Authorization") }
|
|
return req
|
|
}
|
|
|
|
private func perform<T: Decodable>(_ request: URLRequest) async throws -> T {
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
|
|
guard (200...299).contains(http.statusCode) else {
|
|
let msg = (try? JSONDecoder().decode(APIErrorBody.self, from: data))?.message
|
|
?? HTTPURLResponse.localizedString(forStatusCode: http.statusCode)
|
|
throw APIError.serverError(msg)
|
|
}
|
|
let dec = JSONDecoder()
|
|
dec.dateDecodingStrategy = .iso8601
|
|
return try dec.decode(T.self, from: data)
|
|
}
|
|
}
|
|
|
|
private struct AuthResponse: Decodable { let token: String }
|
|
private struct StreakResponse: Decodable { let streak: Int }
|
|
private struct APIErrorBody: Decodable { let message: String }
|
|
struct EmptyResponse: Decodable {}
|
|
|
|
enum APIError: LocalizedError {
|
|
case invalidResponse
|
|
case serverError(String)
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidResponse: return "Ungültige Serverantwort"
|
|
case .serverError(let m): return m
|
|
}
|
|
}
|
|
}
|