Commit 90ac1840bb

90ac1840bb737629cbee8366af487c7ace300f87

parent: 3337af385a

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-14 08:04 UTC

API tokens: a mint cannot be lost; the copy stays here briefly

Cancel waits while a token is minted, which would otherwise be made and
never shown. A copied token stays on this device and leaves the
clipboard after two minutes. New tokens default to read, as the server
mints them; the revoke prompt says that revoking what a token created
can sign this app out too.

Ref #34

Layout: unified · split

gitbay/Views/Account/TokensView.swift +13 −3
@@ -1,5 +1,6 @@
1import SwiftUI 1import SwiftUI
2import UIKit 2import UIKit
3import UniformTypeIdentifiers
3 4
4/// Your API tokens: what each may do, when it was used and stops 5/// Your API tokens: what each may do, when it was used and stops
5/// working, and who minted it. Mint one from the + button; swipe to revoke. 6/// working, and who minted it. Mint one from the + button; swipe to revoke.
@@ -62,7 +63,7 @@ struct TokensView: View {
62 Button("Revoke with what it created", role: .destructive) { revoke(token, withCreated: true) } 63 Button("Revoke with what it created", role: .destructive) { revoke(token, withCreated: true) }
63 Button("Cancel", role: .cancel) {} 64 Button("Cancel", role: .cancel) {}
64 } message: { _ in 65 } message: { _ in
65 Text("Anything using it stops working at once. If this app signed in with it, the app signs out. What it created, tokens and keys, stays unless you revoke it too.") 66 Text("Anything using it stops working at once. What it created, tokens and keys, stays unless you revoke it too. If this app signed in with it, or with one of those, the app signs out.")
66 } 67 }
67 .sheet(isPresented: $minting, onDismiss: { minted = nil }) { 68 .sheet(isPresented: $minting, onDismiss: { minted = nil }) {
68 MintTokenSheet(model: model, minted: $minted) 69 MintTokenSheet(model: model, minted: $minted)
@@ -132,7 +133,8 @@ private struct MintTokenSheet: View {
132 @Binding var minted: String? 133 @Binding var minted: String?
133 134
134 @State private var name = "" 135 @State private var name = ""
135 @State private var scope = "full" 136 /// read, as the server mints by default; full is a choice.
137 @State private var scope = "read"
136 @State private var ttl: String? 138 @State private var ttl: String?
137 @State private var sent = false 139 @State private var sent = false
138 @State private var copied = false 140 @State private var copied = false
@@ -148,7 +150,12 @@ private struct MintTokenSheet: View {
148 .textSelection(.enabled) 150 .textSelection(.enabled)
149 .accessibilityIdentifier("token-secret") 151 .accessibilityIdentifier("token-secret")
150 Button(copied ? "Copied" : "Copy") { 152 Button(copied ? "Copied" : "Copy") {
151 UIPasteboard.general.string = minted 153 // This device only, and gone in two minutes: a
154 // token is not for Universal Clipboard or for
155 // whatever is pasted next week.
156 UIPasteboard.general.setItems(
157 [[UTType.utf8PlainText.identifier: minted]],
158 options: [.localOnly: true, .expirationDate: Date.now.addingTimeInterval(120)])
152 copied = true 159 copied = true
153 } 160 }
154 .accessibilityIdentifier("token-copy") 161 .accessibilityIdentifier("token-copy")
@@ -194,7 +201,10 @@ private struct MintTokenSheet: View {
194 } 201 }
195 } else { 202 } else {
196 ToolbarItem(placement: .cancellationAction) { 203 ToolbarItem(placement: .cancellationAction) {
204 // Not while minting: the token would be made and
205 // never shown.
197 Button("Cancel") { dismiss() } 206 Button("Cancel") { dismiss() }
207 .disabled(model.working)
198 } 208 }
199 ToolbarItem(placement: .confirmationAction) { 209 ToolbarItem(placement: .confirmationAction) {
200 if model.working { 210 if model.working {