v1.5.0 — getProfile() + ProfileInfo

Apps können den 2FA-Status des eingeloggten Users lesen, damit
AccountView entscheidet ob "Zwei-Faktor aktivieren" oder
"Zwei-Faktor deaktivieren" angezeigt wird.

ProfileInfo (public struct) — id, email, name, emailVerified,
twoFactorEnabled.

AuthClient.getProfile() -> ProfileInfo — lädt das Profil vom
Server (GET /api/v1/auth/profile → Better Auths /api/auth/get-session).
Nutzt Session-Token als Bearer (Wire-Konvention).

4 neue Tests, 70/70 grün.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-14 01:06:50 +02:00
parent 0a79083b58
commit fe607c15d2
397 changed files with 2876 additions and 0 deletions

View file

@ -635,3 +635,101 @@ private struct TotpEnableResponse: Decodable {
let totpURI: String?
let backupCodes: [String]?
}
// MARK: - Profile
/// Subset des Server-User-Profiles, das die Apps für UI-Entscheidungen
/// brauchen. Wird von ``AuthClient/getProfile()`` geliefert.
///
/// Server-Quelle: `GET /api/v1/auth/profile` ( Better Auths
/// `/api/auth/get-session`). Returnt `{user: {}, session: {}}`.
/// Wir nehmen nur die UI-relevanten Felder mit.
public struct ProfileInfo: Sendable, Equatable {
public let id: String
public let email: String
public let name: String?
public let emailVerified: Bool
/// `true` wenn der User TOTP-2FA aktiviert hat. Apps zeigen
/// dann den Disable-/Regenerate-Pfad statt des Enroll-Wizards.
public let twoFactorEnabled: Bool
public init(
id: String,
email: String,
name: String?,
emailVerified: Bool,
twoFactorEnabled: Bool
) {
self.id = id
self.email = email
self.name = name
self.emailVerified = emailVerified
self.twoFactorEnabled = twoFactorEnabled
}
}
public extension AuthClient {
/// Lädt das aktuelle Profil des eingeloggten Users vom Server.
///
/// - Important: Nutzt den Session-Token als Bearer (Wire-Konvention
/// für mana-auth-Endpoints, siehe Doc-Header dieser Datei).
///
/// - Throws: ``AuthError/notSignedIn`` ohne Session,
/// ``AuthError/unauthorized`` wenn Server den Token rejected,
/// Netzwerk-Cases.
func getProfile() async throws -> ProfileInfo {
let token = try currentSessionToken()
let url = config.authBaseURL.appending(path: "/api/v1/auth/profile")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, http): (Data, HTTPURLResponse)
do {
let (d, r) = try await session.data(for: request)
guard let h = r as? HTTPURLResponse else {
throw AuthError.networkFailure("Keine HTTP-Antwort")
}
data = d
http = h
} catch let error as URLError {
throw AuthError.networkFailure(error.localizedDescription)
}
guard http.statusCode == 200 else {
throw AuthError.classify(
status: http.statusCode,
data: data,
retryAfterHeader: http.retryAfterSeconds
)
}
let envelope = try JSONDecoder().decode(ProfileEnvelope.self, from: data)
guard let user = envelope.user else {
throw AuthError.decoding("user-Feld fehlt in profile-Antwort")
}
return ProfileInfo(
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified ?? false,
twoFactorEnabled: user.twoFactorEnabled ?? false
)
}
}
/// Wire-Format-Hülle für `GET /api/v1/auth/profile`. Better-Auth-
/// `/api/auth/get-session` returnt `{user: {...}, session: {...}}`;
/// wir lesen `user` und ignorieren `session`.
private struct ProfileEnvelope: Decodable {
let user: ProfileUser?
}
private struct ProfileUser: Decodable {
let id: String
let email: String
let name: String?
let emailVerified: Bool?
let twoFactorEnabled: Bool?
}