Files
Vorrania/ios/Sources/Session.swift
Scarriffle 62a1e7c469 iOS-App heisst jetzt Vorrania
Web, Backend, Deploy und Docs trugen den neuen Namen bereits - iOS war der
letzte Rest. Umgestellt sind Projekt- und Targetname, die Bundle-ID
(com.scarriffle.vorrania), das URL-Schema, die Schnellaktionen, der
Keychain-Service und die Anleitungen. ProjectGoodApp.swift heisst
VorraniaApp.swift.

Zwei Nebenwirkungen, die sich nicht vermeiden lassen: Die neue Bundle-ID
macht die App fuer iOS zu einer anderen App. Die alte bleibt auf dem
Home-Bildschirm liegen und muss von Hand weg, und das Token im Keychain
haengt an der alten Bundle-ID - die neue kommt nicht daran, also einmal neu
anmelden. Wer sich Kurzbefehle mit projectgood://checkin angelegt hat, traegt
dort vorrania://checkin ein.

Das Xcode-Projekt ist ab jetzt eingecheckt statt ignoriert, damit man ohne
XcodeGen bauen kann; nur xcuserdata und DerivedData bleiben draussen.
Gegengeprueft mit einem Simulator-Build: Bundle-ID, Anzeigename, Schema und
Schnellaktionen stehen im fertigen Bundle richtig drin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:35:45 +02:00

115 lines
4.2 KiB
Swift

import Foundation
import Security
/// Hält Server-Adresse und Anmeldedaten. Das Token liegt im Keychain,
/// die Server-URL in den UserDefaults (nicht geheim).
final class Session: ObservableObject {
static let shared = Session()
private let urlKey = "server_url"
private let usernameKey = "username"
private let stayKey = "stay_logged_in"
private let keychainAccount = "vorrania-token"
@Published private(set) var baseURL: URL?
@Published private(set) var token: String?
@Published var username: String = ""
@Published var isAdmin: Bool = false
/// Merkt die letzte Wahl, damit der Haken im Login richtig steht.
@Published private(set) var stayLoggedIn: Bool = true
var isLoggedIn: Bool { token != nil && baseURL != nil }
private init() {
if let stored = UserDefaults.standard.string(forKey: urlKey) {
baseURL = Session.normalize(stored)
}
// Ohne bisherige Wahl bleibt man angemeldet - das ist der Alltagsfall.
stayLoggedIn = UserDefaults.standard.object(forKey: stayKey) as? Bool ?? true
token = Keychain.read(account: keychainAccount)
username = UserDefaults.standard.string(forKey: usernameKey) ?? ""
}
/// Sorgt für eine URL mit Schema und abschließendem "/", damit relative
/// Pfade ("api/...") korrekt aufgelöst werden.
static func normalize(_ raw: String) -> URL? {
var text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return nil }
if !text.contains("://") { text = "http://" + text }
if !text.hasSuffix("/") { text += "/" }
return URL(string: text)
}
func setServer(_ raw: String) {
guard let url = Session.normalize(raw) else { return }
baseURL = url
UserDefaults.standard.set(url.absoluteString, forKey: urlKey)
}
/// `persist == false` heisst: die Anmeldung gilt nur, solange die App laeuft.
/// Auf dem Geraet bleibt dann nichts zurueck.
func store(token newToken: String, username name: String, isAdmin admin: Bool, persist: Bool) {
token = newToken
username = name
isAdmin = admin
stayLoggedIn = persist
UserDefaults.standard.set(persist, forKey: stayKey)
if persist {
Keychain.write(newToken, account: keychainAccount)
UserDefaults.standard.set(name, forKey: usernameKey)
} else {
Keychain.delete(account: keychainAccount)
UserDefaults.standard.removeObject(forKey: usernameKey)
}
}
func logout() {
token = nil
username = ""
isAdmin = false
Keychain.delete(account: keychainAccount)
UserDefaults.standard.removeObject(forKey: usernameKey)
}
}
/// Minimaler Keychain-Zugriff für ein einzelnes Token.
enum Keychain {
private static let service = "com.scarriffle.vorrania"
static func write(_ value: String, account: String) {
delete(account: account)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: Data(value.utf8),
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
]
SecItemAdd(query as CFDictionary, nil)
}
static func read(account: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
static func delete(account: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
SecItemDelete(query as CFDictionary)
}
}