Commit 1b9856d2d1

1b9856d2d124a1032b6d3d12786a4a6edea5b832

parent: d7fac80c5f

Unsigned

cmc <hello@cleberg.net> · 2026-09-30 02:12 UTC

Sign-in: link to the web token page; say what full scope allows (!136)

- `GitbayInstance.tokenPageURL`: `<origin>/settings#tokens`, unit-tested.
- Sign-in footer: first option is a link to Settings → API tokens on the typed instance (hidden while the instance field doesn't parse), then the SSH hint `gitbay auth token create --name iphone --scope full --ttl 90d`. States that full scope can comment and merge, and on an admin account, administer the instance.
- AccountView, AccountViewModel, README: tokens are minted on the web or over SSH, no longer "SSH-only by design".

Closes #18

Layout: unified · split

README.org +9 −6
@@ -26,20 +26,23 @@ not at a keyboard.
26| Explore, feed and profiles | Discovery, activity, contribution graph | 26| Explore, feed and profiles | Discovery, activity, contribution graph |
27| Markup | Bodies and comments are written and rendered in the format they were stored in. Org renders natively through [[https://gitbay.org/krz/org-swift][OrgSwiftUI]] — selectable text, Dynamic Type, VoiceOver — rather than in a web view | 27| Markup | Bodies and comments are written and rendered in the format they were stored in. Org renders natively through [[https://gitbay.org/krz/org-swift][OrgSwiftUI]] — selectable text, Dynamic Type, VoiceOver — rather than in a web view |
28 28
29Everything the CLI can do reaches the app, except what is SSH-only by 29Everything the CLI can do reaches the app, except minting an API token,
30design: minting an API token, and deleting or transferring a repository, 30which is done on the web or over SSH, and deleting or transferring a
31both of which want a typed confirmation or carry a credential. 31repository, which is SSH-only by design since it wants a typed
32confirmation.
32 33
33* Signing in 34* Signing in
34 35
35As =gitbay= requires, your SSH key is your identity. To login to the mobile app, 36To sign in to the mobile app you need a bearer token. Create one with full
36you will need to mint a bearer token: 37scope under Settings → API tokens on the instance's website
38(=https://<instance>/settings#tokens=), or over SSH:
37 39
38#+begin_src sh 40#+begin_src sh
39gitbay auth token create --name iphone --scope full --ttl 90d 41gitbay auth token create --name iphone --scope full --ttl 90d
40#+end_src 42#+end_src
41 43
42Alternatively, =--scope read= works too, but a read-only token cannot comment or 44Full scope can comment and merge, and on an admin account, administer the
45instance. =--scope read= works too, but a read-only token cannot comment or
43merge. 46merge.
44 47
45Any gitbay instance works. You just need to enter the host at sign-in. 48Any gitbay instance works. You just need to enter the host at sign-in.
gitbay/Account/AccountViewModel.swift +1 −1
@@ -2,7 +2,7 @@ import Foundation
2import Observation 2import Observation
3 3
4/// SSH keys, PGP keys, and email verification — the account rows the web 4/// SSH keys, PGP keys, and email verification — the account rows the web
5/// has. Tokens stay SSH-only by design and get no UI here. 5/// has. Tokens are minted on the web or over SSH and get no UI here.
6@Observable 6@Observable
7@MainActor 7@MainActor
8final class AccountViewModel { 8final class AccountViewModel {
gitbay/Networking/GitbayInstance.swift +8
@@ -67,6 +67,14 @@ nonisolated struct GitbayInstance: Sendable, Hashable, Codable {
67 baseURL.appending(path: "/api/v1/cmd") 67 baseURL.appending(path: "/api/v1/cmd")
68 } 68 }
69 69
70 /// The web Settings → API tokens section.
71 var tokenPageURL: URL {
72 var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
73 components.path = "/settings"
74 components.fragment = "tokens"
75 return components.url!
76 }
77
70 /// Whether a URL is on this instance. Checked before the Authorization 78 /// Whether a URL is on this instance. Checked before the Authorization
71 /// header goes on a request, and again on every redirect. 79 /// header goes on a request, and again on every redirect.
72 func isOwn(_ url: URL) -> Bool { 80 func isOwn(_ url: URL) -> Bool {
gitbay/Views/Account/AccountView.swift +2 −2
@@ -1,7 +1,7 @@
1import SwiftUI 1import SwiftUI
2 2
3/// SSH keys, PGP keys, and email verification. Tokens are minted over 3/// SSH keys, PGP keys, and email verification. Tokens are minted on the
4/// SSH only, by design — no UI implies otherwise. 4/// web settings page or over SSH, not here.
5struct AccountView: View { 5struct AccountView: View {
6 6
7 @State private var model: AccountViewModel 7 @State private var model: AccountViewModel
gitbay/Views/SignInView.swift +13 −4
@@ -1,8 +1,8 @@
1import SwiftUI 1import SwiftUI
2 2
3/// Paste a token, name the instance, done. There is no browser flow and no 3/// Paste a token, name the instance, done. There is no browser flow and no
4/// password anywhere in the system: tokens are minted over SSH on a 4/// password in the app: tokens are minted on the instance's web settings
5/// machine that has it. 5/// page or over SSH.
6struct SignInView: View { 6struct SignInView: View {
7 7
8 @Environment(SessionStore.self) private var session 8 @Environment(SessionStore.self) private var session
@@ -42,11 +42,20 @@ struct SignInView: View {
42 Text("Token") 42 Text("Token")
43 } footer: { 43 } footer: {
44 VStack(alignment: .leading, spacing: 8) { 44 VStack(alignment: .leading, spacing: 8) {
45 Text("Mint one over SSH on a machine that has your key:") 45 if let tokenPage = (try? GitbayInstance(url: instanceURL))?.tokenPageURL {
46 Link(destination: tokenPage) {
47 Text("Create one with full scope under Settings → API tokens")
48 .multilineTextAlignment(.leading)
49 .frame(maxWidth: .infinity, alignment: .leading)
50 }
51 Text("Or over SSH on a machine that has your key:")
52 } else {
53 Text("Create one over SSH on a machine that has your key:")
54 }
46 Text(verbatim: "gitbay auth token create --name iphone --scope full --ttl 90d") 55 Text(verbatim: "gitbay auth token create --name iphone --scope full --ttl 90d")
47 .font(.gbMono(.caption)) 56 .font(.gbMono(.caption))
48 .textSelection(.enabled) 57 .textSelection(.enabled)
49 Text("`--scope read` also works, but a read-only token cannot comment or merge.") 58 Text("Full scope can comment and merge, and on an admin account, administer the instance. A read-only token cannot comment or merge.")
50 } 59 }
51 } 60 }
52 61
gitbayTests/GitbayClientTests.swift +7
@@ -338,6 +338,13 @@ struct GitbayInstanceTests {
338 #expect(url.query() == "argv=repo&argv=cat&argv=krz/gitbay&argv=cmd/main.go") 338 #expect(url.query() == "argv=repo&argv=cat&argv=krz/gitbay&argv=cmd/main.go")
339 } 339 }
340 340
341 @Test func tokenPageURLIsSettingsTokensOnTheInstance() throws {
342 let instance = try GitbayInstance(url: "gitbay.org/krz/solar?x=1")
343 #expect(instance.tokenPageURL.absoluteString == "https://gitbay.org/settings#tokens")
344 let local = try GitbayInstance(url: "http://localhost:3000")
345 #expect(local.tokenPageURL.absoluteString == "http://localhost:3000/settings#tokens")
346 }
347
341 @Test func ownHostCheckCoversSchemeHostAndPort() throws { 348 @Test func ownHostCheckCoversSchemeHostAndPort() throws {
342 let instance = try GitbayInstance(url: "https://gitbay.org") 349 let instance = try GitbayInstance(url: "https://gitbay.org")
343 #expect(instance.isOwn(URL(string: "https://gitbay.org/api/v1/read")!)) 350 #expect(instance.isOwn(URL(string: "https://gitbay.org/api/v1/read")!))