import Foundation /// Thin wrapper around the `/api/auth/*` endpoints. `AppState` is the only /// caller that should hold state derived from these — feature screens call /// through `AppState`, not this type directly, so there is exactly one place /// that decides what "signed in" means. enum AuthAPI { struct SignupBody: Encodable { let name: String? let email: String let password: String let lang: String } struct SignupResponse: Decodable { let status: String /// Only ever present against a local dev backend with no SMTP /// configured (see src/app/api/auth/signup/route.ts) — never in /// production. Useful for the simulator during development. let devLink: String? } struct LoginBody: Encodable { let email: String let password: String let remember: Bool } struct AppleSignInBody: Encodable { let identityToken: String let fullName: String? } /// `POST /api/auth/apple` — native Sign in with Apple. `identityToken` is /// `ASAuthorizationAppleIDCredential.identityToken` decoded to a UTF-8 /// string; `fullName` is `credential.fullName` formatted for display, /// which Apple only ever supplies on the user's FIRST authorization with /// this app (pass `nil` on every subsequent sign-in — there is nothing to /// send). The backend verifies the token itself; nothing here is trusted /// data, it's just what gets forwarded for verification. static func appleSignIn(identityToken: String, fullName: String?) async throws -> LoginResponse { let request = try APIRequest.json( path: "/api/auth/apple", method: .post, body: AppleSignInBody(identityToken: identityToken, fullName: fullName), requiresAuth: false ) return try await APIClient.shared.send(request) } /// `POST /api/auth/signup`. No session is created — the account is inert /// until the emailed confirmation link is opened, exactly like the web /// flow. The response is deliberately neutral (see the route's doc /// comment): callers cannot tell a new signup apart from "already /// registered" from this response alone. static func signup(name: String?, email: String, password: String) async throws -> SignupResponse { let request = try APIRequest.json( path: "/api/auth/signup", method: .post, body: SignupBody(name: name, email: email, password: password, lang: "de"), requiresAuth: false ) return try await APIClient.shared.send(request) } /// `POST /api/auth/login`. On success the backend returns the raw session /// token in the body (because the request carries `X-Client: ios` — see /// APIClient) instead of only a Set-Cookie header. The caller is /// responsible for persisting it via `KeychainTokenStore`. static func login(email: String, password: String, remember: Bool = true) async throws -> LoginResponse { let request = try APIRequest.json( path: "/api/auth/login", method: .post, body: LoginBody(email: email, password: password, remember: remember), requiresAuth: false ) return try await APIClient.shared.send(request) } /// `GET /api/auth/session` — "who am I". Never throws for "signed out"; /// that's `{ user: null }`, HTTP 200. static func session() async throws -> SessionResponse { try await APIClient.shared.send(APIRequest(path: "/api/auth/session", method: .get)) } /// `POST /api/auth/logout`. Best-effort: the caller should clear the /// local token and navigate to the login screen regardless of whether /// this succeeds (mirrors the web route's own "logout always proceeds" /// philosophy). static func logout() async throws { try await APIClient.shared.sendDiscardingResponse( APIRequest(path: "/api/auth/logout", method: .post) ) } }