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 | |
| 29 | Everything the CLI can do reaches the app, except what is SSH-only by |
29 | Everything the CLI can do reaches the app, except minting an API token, |
| 30 | design: minting an API token, and deleting or transferring a repository, |
30 | which is done on the web or over SSH, and deleting or transferring a |
| 31 | both of which want a typed confirmation or carry a credential. |
31 | repository, which is SSH-only by design since it wants a typed |
| |
32 | confirmation. |
| 32 | |
33 | |
| 33 | * Signing in |
34 | * Signing in |
| 34 | |
35 | |
| 35 | As =gitbay= requires, your SSH key is your identity. To login to the mobile app, |
36 | To sign in to the mobile app you need a bearer token. Create one with full |
| 36 | you will need to mint a bearer token: |
37 | scope 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 |
| 39 | gitbay auth token create --name iphone --scope full --ttl 90d |
41 | gitbay auth token create --name iphone --scope full --ttl 90d |
| 40 | #+end_src |
42 | #+end_src |
| 41 | |
43 | |
| 42 | Alternatively, =--scope read= works too, but a read-only token cannot comment or |
44 | Full scope can comment and merge, and on an admin account, administer the |
| |
45 | instance. =--scope read= works too, but a read-only token cannot comment or |
| 43 | merge. |
46 | merge. |
| 44 | |
47 | |
| 45 | Any gitbay instance works. You just need to enter the host at sign-in. |
48 | Any 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 |
| 2 | import Observation |
2 | import 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 |
| 8 | final class AccountViewModel { |
8 | final 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 @@ |
| 1 | import SwiftUI |
1 | import 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. |
| 5 | struct AccountView: View { |
5 | struct 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 @@ |
| 1 | import SwiftUI |
1 | import 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. |
| 6 | struct SignInView: View { |
6 | struct 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")!)) |