diff options
| author | Christian Cleberg <[email protected]> | 2026-07-15 23:52:45 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-15 23:52:45 -0500 |
| commit | f3627deef0848b95a083d9e16468b0f8484d784e (patch) | |
| tree | a30ee4dc46bd2e575820316b857dd08cfa9a53c2 /Hutch/Views/More/ProfileView.swift | |
| parent | c4930f31ffc5d7c5de5eeafd3da184c7691f8ab7 (diff) | |
| download | hutch-f3627deef0848b95a083d9e16468b0f8484d784e.tar.gz hutch-f3627deef0848b95a083d9e16468b0f8484d784e.tar.bz2 hutch-f3627deef0848b95a083d9e16468b0f8484d784e.zip | |
feat: show the meta.sr.ht audit log
auditLog existed in the API but was never called, so the record of what has
happened to your account — logins, key changes, the addresses they came from —
was web-only.
Sits under the tokens in Profile and loads on demand for the same reason they
do: an audit log is something you go looking for, not something worth a request
on every profile view. One page, newest first; the archive stays on meta.sr.ht.
Its errors are kept separate from the shared `error` so a failed audit fetch
cannot bury a profile save failure, and vice versa.
Diffstat (limited to 'Hutch/Views/More/ProfileView.swift')
| -rw-r--r-- | Hutch/Views/More/ProfileView.swift | 56 |
1 files changed, 56 insertions, 0 deletions
diff --git a/Hutch/Views/More/ProfileView.swift b/Hutch/Views/More/ProfileView.swift index 45f7d06..fc1ed2f 100644 --- a/Hutch/Views/More/ProfileView.swift +++ b/Hutch/Views/More/ProfileView.swift @@ -84,6 +84,7 @@ struct ProfileView: View { sshKeysSection(viewModel) pgpKeysSection(viewModel) patSection(viewModel) + auditLogSection(viewModel) } } .themedList() @@ -432,6 +433,61 @@ struct ProfileView: View { } } } + + /// Loaded on demand, like the tokens above — an audit log is something you go + /// looking for, not something worth a request on every profile view. + @ViewBuilder + private func auditLogSection(_ viewModel: SettingsViewModel) -> some View { + Section { + if viewModel.isLoadingAuditLog { + HStack { + Spacer() + ProgressView() + Spacer() + } + .themedRow() + } else if let error = viewModel.auditLogError { + Text(error) + .font(.caption) + .foregroundStyle(.red) + .themedRow() + } else if viewModel.auditLog.isEmpty { + Button("Load Audit Log") { + Task { await viewModel.loadAuditLog() } + } + .themedRow() + } else { + ForEach(viewModel.auditLog) { entry in + VStack(alignment: .leading, spacing: 4) { + Text(entry.eventType) + .font(.subheadline) + + if let details = entry.details, !details.isEmpty { + Text(details) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(3) + } + + HStack(spacing: 12) { + Text(entry.created.relativeDescription) + Text(entry.ipAddress) + .monospaced() + } + .font(.caption2) + .foregroundStyle(.tertiary) + } + .accessibilityElement(children: .combine) + .accessibilityLabel("\(entry.eventType), \(entry.created.relativeDescription), from \(entry.ipAddress)") + } + .themedRow() + } + } header: { + Text("Audit Log") + } footer: { + Text("Recent security-relevant activity on your account, newest first. The full log lives on meta.sr.ht.") + } + } } private struct ProfileBioView: View { |
