summaryrefslogtreecommitdiff
path: root/Hutch/Views/More/ProfileView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-15 23:52:45 -0500
committerChristian Cleberg <[email protected]>2026-07-15 23:52:45 -0500
commitf3627deef0848b95a083d9e16468b0f8484d784e (patch)
treea30ee4dc46bd2e575820316b857dd08cfa9a53c2 /Hutch/Views/More/ProfileView.swift
parentc4930f31ffc5d7c5de5eeafd3da184c7691f8ab7 (diff)
downloadhutch-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.swift56
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 {