feat(audit): append-only audit log for spaces
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s

Records who changed what, and is the mechanism statistics will read — one
event table rather than two logs that drift apart.

The table is deliberately generic (action + target + JSON metadata) so a new
action needs no migration. Writes never throw: a kick must not fail because
its log entry could not be written, since the kick already happened.

Leaving is recorded as a different action from being removed. The same route
serves both, and a log that conflates them misleads exactly when it matters.

Actor is nullable with ON DELETE SET NULL: the event outlives the account, and
a log that vanished with its actor would be worthless. Reads are gated on
MANAGE_SPACE rather than a new permission bit, which would default to nobody
until every role was re-edited. Paging uses the snowflake id, stable even for
two events in the same millisecond, and an action this build does not know
still renders a row.
This commit is contained in:
2026-08-31 13:22:52 -03:00
parent fb662bfe12
commit bbb190cbda
16 changed files with 4604 additions and 2 deletions
+21
View File
@@ -33,6 +33,27 @@ export const en = {
'settings.voice.micTest.idle': 'Test your mic without joining a call.',
'settings.voice.micTest.failed': 'Could not open the microphone. Check the device and its permission.',
// Audit log
'audit.title': 'Audit Log',
'audit.empty': 'Nothing recorded yet. Changes to the server show up here.',
'audit.loadMore': 'Load more',
'audit.unknownActor': 'Deleted account',
'audit.action.space.update': '{actor} updated the server settings',
'audit.action.space.transfer_ownership': '{actor} transferred ownership of the server',
'audit.action.channel.create': '{actor} created the channel {name}',
'audit.action.channel.update': '{actor} updated the channel {name}',
'audit.action.channel.delete': '{actor} deleted the channel {name}',
'audit.action.member.kick': '{actor} removed a member',
'audit.action.member.leave': '{actor} left the server',
'audit.action.member.ban': '{actor} banned a member',
'audit.action.member.unban': '{actor} unbanned a member',
'audit.action.role.create': '{actor} created the role {name}',
'audit.action.role.update': '{actor} updated the role {name}',
'audit.action.role.delete': '{actor} deleted a role',
'audit.action.invite.create': '{actor} created an invite',
'audit.action.message.delete': '{actor} deleted a message',
'audit.action.unknown': '{actor} performed an action',
// GIF picker
'gif.search': 'Search GIFs',
'gif.tab.favorites': 'Favorites',