SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%
7.1 KB · 168 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KAID.swift — connexion KA ID NATIVE : ASWebAuthenticationSession vers le hub3// groupe-ka.com (SSO officiel de l'écosystème, client « ka-ios »), retour par4// le scheme ka-ios://auth, puis ÉCHANGE VÉRIFIÉ côté serveur (api-ka valide la5// signature du jeton — aucun secret dans l'app) qui renvoie le profil.6import AuthenticationServices7import SwiftUI89struct KAIDProfile: Codable, Equatable {10    var kaID: String11    var name: String?12    var email: String?13    var picture: String?14    var roleLabel: String?15    var city: String?16    var bio: String?17}1819@MainActor20final class KAIDManager: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {21    static let shared = KAIDManager()22    @Published var profile: KAIDProfile?23    @Published var busy = false24    @Published var lastError: String?2526    private let storeKey = "ka.id.profile"27    private var session: ASWebAuthenticationSession?2829    override init() {30        super.init()31        if let data = UserDefaults.standard.data(forKey: storeKey),32           let p = try? JSONDecoder().decode(KAIDProfile.self, from: data) {33            profile = p34        }35    }3637    func login() {38        guard !busy else { return }39        busy = true40        lastError = nil41        let state = UUID().uuidString42        var comps = URLComponents(string: "https://www.groupe-ka.com/sso/authorize")!43        comps.queryItems = [44            .init(name: "client_id", value: "ka-ios"),45            .init(name: "redirect_uri", value: "ka-ios://auth/callback"),46            .init(name: "state", value: state),47        ]48        let s = ASWebAuthenticationSession(url: comps.url!, callbackURLScheme: "ka-ios") { [weak self] url, error in49            Task { @MainActor in50                guard let self else { return }51                defer { self.busy = false }52                guard error == nil, let url else {53                    if let e = error as? ASWebAuthenticationSessionError, e.code == .canceledLogin { return }54                    self.lastError = "Connexion annulée ou impossible."55                    return56                }57                let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []58                guard items.first(where: { $0.name == "state" })?.value == state,59                      let token = items.first(where: { $0.name == "ka_token" })?.value else {60                    self.lastError = "Réponse du hub invalide."61                    return62                }63                await self.exchange(token)64            }65        }66        s.presentationContextProvider = self67        s.prefersEphemeralWebBrowserSession = false // garde la session hub (Google/courriel)68        session = s69        s.start()70    }7172    private func exchange(_ token: String) async {73        do {74            var req = URLRequest(url: URL(string: "https://www.api-ka.com/api/ios/auth/exchange")!)75            req.httpMethod = "POST"76            req.setValue("application/json", forHTTPHeaderField: "Content-Type")77            req.httpBody = try JSONSerialization.data(withJSONObject: ["ka_token": token])78            let (data, resp) = try await URLSession.shared.data(for: req)79            guard (resp as? HTTPURLResponse)?.statusCode == 200,80                  let obj = try JSONSerialization.jsonObject(with: data) as? [String: Any],81                  let kaID = obj["ka_id"] as? String else {82                lastError = "Vérification du compte impossible."83                return84            }85            let hub = obj["profile"] as? [String: Any]86            let p = KAIDProfile(87                kaID: kaID,88                name: obj["name"] as? String ?? hub?["name"] as? String,89                email: obj["email"] as? String,90                picture: obj["picture"] as? String ?? hub?["picture"] as? String,91                roleLabel: hub?["role_label"] as? String,92                city: hub?["city"] as? String,93                bio: hub?["bio"] as? String94            )95            profile = p96            if let d = try? JSONEncoder().encode(p) {97                UserDefaults.standard.set(d, forKey: storeKey)98            }99            Haptics.success()100        } catch {101            lastError = "Réseau indisponible — réessayez."102        }103    }104105    func logout() {106        profile = nil107        UserDefaults.standard.removeObject(forKey: storeKey)108        Haptics.tap()109    }110111    nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {112        MainActor.assumeIsolated {113            UIApplication.shared.connectedScenes114                .compactMap { ($0 as? UIWindowScene)?.keyWindow }115                .first ?? ASPresentationAnchor()116        }117    }118}119120// MARK: - Carte de membre Groupe KA121122struct KAIDCard: View {123    let profile: KAIDProfile124    var body: some View {125        VStack(alignment: .leading, spacing: 10) {126            HStack {127                Text("Groupe").font(.system(.subheadline, design: .rounded).weight(.bold))128                    .foregroundStyle(Color(hex: "#f5f3ee"))129                Text("KA").font(.system(.caption, design: .rounded).weight(.bold))130                    .foregroundStyle(KATheme.lime)131                    .padding(.horizontal, 6).padding(.vertical, 2)132                    .background(Color(hex: "#f5f3ee").opacity(0.14), in: RoundedRectangle(cornerRadius: 6))133                Spacer()134                Text("MEMBRE")135                    .font(.system(size: 9, design: .monospaced).weight(.bold))136                    .foregroundStyle(KATheme.lime)137            }138            HStack(spacing: 12) {139                AsyncImage(url: profile.picture.flatMap(URL.init(string:))) { phase in140                    if case .success(let img) = phase { img.resizable() }141                    else { KATheme.lime.opacity(0.25).overlay(142                        Text(String(profile.name?.prefix(1) ?? "K")).font(.title2.weight(.bold)).foregroundStyle(KATheme.lime)) }143                }144                .frame(width: 54, height: 54)145                .clipShape(Circle())146                .overlay(Circle().strokeBorder(KATheme.lime, lineWidth: 1.6))147                VStack(alignment: .leading, spacing: 2) {148                    Text(profile.name ?? "Membre KA")149                        .font(.headline).foregroundStyle(Color(hex: "#f5f3ee"))150                    Text(profile.kaID)151                        .font(.system(.caption, design: .monospaced).weight(.bold))152                        .foregroundStyle(KATheme.lime)153                    if let r = profile.roleLabel ?? profile.city {154                        Text(r).font(.caption2).foregroundStyle(Color(hex: "#f5f3ee").opacity(0.6))155                    }156                }157                Spacer()158            }159            Text("Un seul compte pour les 13 plateformes de l'écosystème.")160                .font(.caption2).foregroundStyle(Color(hex: "#f5f3ee").opacity(0.55))161        }162        .padding(16)163        .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 16, style: .continuous))164        .overlay(RoundedRectangle(cornerRadius: 16, style: .continuous)165            .strokeBorder(KATheme.lime.opacity(0.5), lineWidth: 1.2))166    }167}168