Replace manual backend config with auto JWT auth
Some checks failed
CI / build-and-deploy (push) Failing after 6s

Device auto-registers on first launch using hardcoded backend URL
and anon key (MarksAuth.swift). 90-day JWT stored in UserDefaults,
refreshed transparently. No settings fields needed — ClaudeService
is always active and non-optional throughout the app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-05-22 14:14:54 -05:00
parent 9f0a10d228
commit 525e6557c8
11 changed files with 143 additions and 137 deletions

View File

@@ -11,23 +11,6 @@ struct PodcastStatus: Decodable {
}
struct ClaudeService: Sendable {
let backendUrl: String
let apiKey: String
static let urlKey = "marksBackendUrl"
static let apiKeyKey = "marksApiKey"
static func load() -> ClaudeService? {
guard let url = UserDefaults.standard.string(forKey: urlKey), !url.isEmpty,
let key = UserDefaults.standard.string(forKey: apiKeyKey), !key.isEmpty
else { return nil }
return ClaudeService(backendUrl: url, apiKey: key)
}
static func save(backendUrl: String, apiKey: String) {
UserDefaults.standard.set(backendUrl, forKey: urlKey)
UserDefaults.standard.set(apiKey, forKey: apiKeyKey)
}
// MARK: - Bookmark AI
@@ -94,15 +77,12 @@ struct ClaudeService: Sendable {
// MARK: - HTTP primitives
private var base: String {
backendUrl.hasSuffix("/") ? String(backendUrl.dropLast()) : backendUrl
}
private func post(_ path: String, body: [String: Any]) async throws -> Data {
guard let url = URL(string: base + path) else { throw APIError.invalidUrl }
let token = try await MarksAuth.validToken()
guard let url = URL(string: MarksAuth.baseURL + path) else { throw APIError.invalidUrl }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
print("[AI] POST \(path)")
@@ -117,9 +97,10 @@ struct ClaudeService: Sendable {
}
private func get(_ path: String) async throws -> Data {
guard let url = URL(string: base + path) else { throw APIError.invalidUrl }
let token = try await MarksAuth.validToken()
guard let url = URL(string: MarksAuth.baseURL + path) else { throw APIError.invalidUrl }
var request = URLRequest(url: url)
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
print("[AI] GET \(path)")
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0

View File

@@ -0,0 +1,52 @@
import Foundation
import UIKit
// Hardcoded backend config no user-facing settings needed
private let backendBaseURL = "https://magicive-api-production.up.railway.app"
private let marksAnonKey = "marks-anon-2025"
private let tokenKey = "marksJWT"
private let tokenExpiryKey = "marksJWTExpiry"
enum MarksAuth {
static var baseURL: String { backendBaseURL }
/// Returns a valid JWT, registering if missing or within 1 hour of expiry.
static func validToken() async throws -> String {
if let token = cached(), !token.isEmpty { return token }
return try await register()
}
private static func cached() -> String? {
guard
let token = UserDefaults.standard.string(forKey: tokenKey),
let expiry = UserDefaults.standard.object(forKey: tokenExpiryKey) as? Date,
expiry > Date().addingTimeInterval(3600)
else { return nil }
return token
}
private static func register() async throws -> String {
guard let url = URL(string: "\(backendBaseURL)/api/auth/register") else {
throw URLError(.badURL)
}
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue(marksAnonKey, forHTTPHeaderField: "X-API-Key")
req.httpBody = try JSONSerialization.data(withJSONObject: ["device_id": deviceId])
let (data, response) = try await URLSession.shared.data(for: req)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
guard (200...299).contains(status) else {
throw APIError.badStatus(status)
}
struct Resp: Decodable { let access_token: String; let expires_in: Int }
let resp = try JSONDecoder().decode(Resp.self, from: data)
UserDefaults.standard.set(resp.access_token, forKey: tokenKey)
let expiry = Date().addingTimeInterval(TimeInterval(resp.expires_in))
UserDefaults.standard.set(expiry, forKey: tokenExpiryKey)
print("[Auth] Registered device \(deviceId.prefix(8))")
return resp.access_token
}
}