Initial commit: nightly iOS app + Supabase backend
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>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user