Files
scan-receipts/app/ios/ScanReceipts/Networking/AuthAPI.swift

96 lines
4.0 KiB
Swift

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)
)
}
}