fix(settings): API key create/delete need Outline's web session, not this app

Same limitation as Passkeys — apiKeys.create/apiKeys.delete need
Outline's cookie+CSRF web session, not this app's Bearer-token auth.
"New API Key…" and the per-row trash button now show an explanatory
popup instead of attempting a request that doesn't work.

The real create sheet, reveal-once flow, delete confirmation, and their
backing functions are untouched and still fully built/tested at the
OutlineKit layer — only the two trigger points were redirected, each
marked with a TODO pointing back to how to re-enable them (swap the
button action back) once there's a supported native auth path or
Outline adds Bearer support for these two endpoints.
This commit is contained in:
2026-08-18 16:53:46 +01:00
parent 1e541979e9
commit 64e970fc92
+28 -4
View File
@@ -53,6 +53,7 @@ struct SettingsView: View {
@State private var didCopyRevealedKey = false @State private var didCopyRevealedKey = false
@State private var apiKeyPendingDeletion: OutlineAPIKey? @State private var apiKeyPendingDeletion: OutlineAPIKey?
@State private var deletingApiKeyId: String? @State private var deletingApiKeyId: String?
@State private var isShowingApiKeyWebOnlyNotice = false
/// Full Local Sync and cache-clearing both need a real connection to be /// Full Local Sync and cache-clearing both need a real connection to be
/// safe clearing while offline (or letting Full Local Sync think it /// safe clearing while offline (or letting Full Local Sync think it
@@ -772,8 +773,14 @@ struct SettingsView: View {
Text("Personal keys") Text("Personal keys")
.font(.headline) .font(.headline)
Spacer() Spacer()
Button("New API Key…") { isShowingCreateApiKey = true } // TODO: apiKeys.create needs Outline's cookie+CSRF web
.disabled(!isEffectivelyOnline) // session, not this app's Bearer-token auth confirmed
// this app's requests to it don't work. Swap this back to
// `isShowingCreateApiKey = true` (the real create sheet
// below is fully built and untouched) once there's a
// supported native auth path, or Outline adds Bearer
// support for this endpoint.
Button("New API Key…") { isShowingApiKeyWebOnlyNotice = true }
} }
.frame(maxWidth: 480) .frame(maxWidth: 480)
@@ -807,6 +814,11 @@ struct SettingsView: View {
Text("Any scripts or integrations using \"\(name)\" will stop working immediately.") Text("Any scripts or integrations using \"\(name)\" will stop working immediately.")
} }
} }
.alert("Manage API Keys on the Web", isPresented: $isShowingApiKeyWebOnlyNotice) {
Button("OK") {}
} message: {
Text("Creating and deleting personal API keys currently requires Outline's web app, the same way Passkeys does — this app can only display your existing keys for now. We may add this here in the future if Outline supports it or we add the right kind of authentication.")
}
} }
/// Shown exactly once, immediately after creation Outline never /// Shown exactly once, immediately after creation Outline never
@@ -967,19 +979,28 @@ struct SettingsView: View {
if deletingApiKeyId == key.id { if deletingApiKeyId == key.id {
ProgressView().controlSize(.small) ProgressView().controlSize(.small)
} else { } else {
// TODO: apiKeys.delete same web-session-only limitation
// as create above, see that comment. Swap back to
// `apiKeyPendingDeletion = key` (the real confirmation
// dialog + deleteApiKey(_:) below are fully built and
// untouched) once native auth can actually call it.
Button { Button {
apiKeyPendingDeletion = key isShowingApiKeyWebOnlyNotice = true
} label: { } label: {
Image(systemName: "trash") Image(systemName: "trash")
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(.red) .foregroundStyle(.red)
.disabled(!isEffectivelyOnline)
} }
} }
.padding(.vertical, 8) .padding(.vertical, 8)
} }
// TODO: not currently reachable from the UI apiKeys.create needs
// Outline's cookie+CSRF web session, confirmed this app's Bearer-token
// requests to it don't work. Kept intact (and covered by OutlineKit
// tests) for when native auth can support it; see the "New API Key"
// button's own TODO for the reconnect point.
private func createApiKey() async { private func createApiKey() async {
guard let apiClient = session.apiClient else { return } guard let apiClient = session.apiClient else { return }
let trimmedName = newApiKeyName.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedName = newApiKeyName.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -1007,6 +1028,9 @@ struct SettingsView: View {
} }
} }
// TODO: not currently reachable from the UI same apiKeys.delete
// web-session-only limitation as createApiKey() above. Kept intact
// for the same reason; see the trash button's own TODO.
private func deleteApiKey(_ key: OutlineAPIKey) async { private func deleteApiKey(_ key: OutlineAPIKey) async {
guard let apiClient = session.apiClient else { return } guard let apiClient = session.apiClient else { return }
apiKeyPendingDeletion = nil apiKeyPendingDeletion = nil