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
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:
@@ -7,6 +7,8 @@ import { Avatar } from '../ui/Avatar';
|
||||
import { api } from '../../api/client';
|
||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||
import { OverviewPanel } from './spaceSettingsPanels/OverviewPanel';
|
||||
import { AuditLogPanel } from './spaceSettingsPanels/AuditLogPanel';
|
||||
import { useT } from '../../i18n';
|
||||
import { MembersPanel } from './spaceSettingsPanels/MembersPanel';
|
||||
import { RolesPanel } from './spaceSettingsPanels/RolesPanel';
|
||||
import { BansPanel } from './spaceSettingsPanels/BansPanel';
|
||||
@@ -266,7 +268,8 @@ export function SpaceSettingsModal() {
|
||||
const spaces = useSpaceStore((s) => s.spaces);
|
||||
const spacePermissions = useSpaceStore((s) => s.spacePermissions);
|
||||
|
||||
const [tab, setTab] = useState<'overview' | 'discovery' | 'members' | 'roles' | 'bans'>('overview');
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<'overview' | 'discovery' | 'members' | 'roles' | 'bans' | 'audit'>('overview');
|
||||
const [mobileView, setMobileView] = useState<'tabs' | 'content'>('tabs');
|
||||
|
||||
const isOpen = activeModal === 'spaceSettings';
|
||||
@@ -331,6 +334,9 @@ export function SpaceSettingsModal() {
|
||||
{canBanMembers && (
|
||||
<button onClick={() => handleTabClick('bans')} className={tabClass('bans')}>Bans</button>
|
||||
)}
|
||||
{canManageSpace && (
|
||||
<button onClick={() => handleTabClick('audit')} className={tabClass('audit')}>{t('audit.title')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -366,6 +372,9 @@ export function SpaceSettingsModal() {
|
||||
{canBanMembers && (
|
||||
<button onClick={() => handleTabClick('bans')} className={tabClass('bans')}>Bans</button>
|
||||
)}
|
||||
{canManageSpace && (
|
||||
<button onClick={() => handleTabClick('audit')} className={tabClass('audit')}>{t('audit.title')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -392,6 +401,7 @@ export function SpaceSettingsModal() {
|
||||
{tab === 'members' && <MembersPanel spaceId={currentSpaceId} />}
|
||||
{tab === 'roles' && canManageRoles && <RolesPanel spaceId={currentSpaceId} />}
|
||||
{tab === 'bans' && canBanMembers && <BansPanel spaceId={currentSpaceId} />}
|
||||
{tab === 'audit' && canManageSpace && <AuditLogPanel spaceId={currentSpaceId} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user