import Foundation import Observation /// One API token (`token list`): a name, what it may do, and when it was /// used and stops working. The secret is shown once, when it is minted. nonisolated struct APIToken: Decodable, Sendable, Hashable, Identifiable { let name: String let scope: String let createdAt: Date let expiresAt: Date? let lastUsedAt: Date? /// The token or key that minted it, when one did rather than you. let createdBy: String? enum CodingKeys: String, CodingKey { case name, scope case createdAt = "created_at" case expiresAt = "expires_at" case lastUsedAt = "last_used_at" case createdBy = "created_by" } var id: String { name } func isExpired(at now: Date) -> Bool { expiresAt.map { $0 <= now } ?? false } } /// `token list`, `token create` and `token revoke [--created]`. @Observable @MainActor final class TokensViewModel { /// What a revoked token had minted: tokens by name, keys by fingerprint. nonisolated struct Created: Decodable, Sendable, Hashable { let tokens: [String]? let keys: [String]? var isEmpty: Bool { (tokens ?? []).isEmpty && (keys ?? []).isEmpty } } nonisolated private struct Minted: Decodable, Sendable { let token: String } nonisolated private struct Revoked: Decodable, Sendable { let created: Created } private(set) var state: LoadState<[APIToken]> = .loading private(set) var actionError: String? private(set) var working = false private let client: GitbayClient init(client: GitbayClient) { self.client = client } func load() async { do { let tokens = try await client.readList(["token", "list"], of: APIToken.self) state = tokens.isEmpty ? .empty("No API tokens.") : .loaded(tokens) } catch { state = .from(error) } } /// `token create --name --scope [--ttl ]`. Returns the /// secret, which the server shows this once and never again. func create(name: String, scope: String, ttl: String?) async -> String? { var argv = ["token", "create", "--name", name.trimmingCharacters(in: .whitespaces), "--scope", scope] if let ttl { argv += ["--ttl", ttl] } let minted: Minted? = await perform { try await self.client.run(argv, as: Minted.self) } return minted?.token } /// `token revoke [--created]`. Returns what the token had /// minted: revoked with it, or still in place. func revoke(_ token: APIToken, withCreated: Bool) async -> Created? { var argv = ["token", "revoke", token.name] if withCreated { argv.append("--created") } let revoked: Revoked? = await perform { try await self.client.run(argv, as: Revoked.self) } return revoked?.created } private func perform(_ body: () async throws -> T?) async -> T? { working = true actionError = nil defer { working = false } do { let result = try await body() await load() return result } catch let error as GitbayError { // The name is typed, so a usage refusal of it is shown. actionError = error.messageForTypedInput } catch { actionError = GitbayError.transport(error).userFacingMessage } return nil } }