Audit trail

This commit is contained in:
2026-08-26 19:52:22 +02:00
parent ed76b35600
commit 314959c7bf
10 changed files with 103 additions and 13 deletions
+15 -6
View File
@@ -22,6 +22,7 @@ from backend.app.services.email_verification import create_verification_token
from backend.app.services.theme_service import THEMES, get_enabled_themes, save_enabled_themes
from backend.app.services.secret_store import encrypt_secret
from backend.app.core.config import settings
from backend.app.services.audit_service import record_audit_event
router = APIRouter()
@@ -95,7 +96,7 @@ def list_users(_: dict = Depends(require_admin)):
@router.post('/users/{user_id}/otp/reset')
def reset_user_otp(user_id: str, _: dict = Depends(require_admin)):
def reset_user_otp(user_id: str, current_user: dict = Depends(require_admin)):
with get_connection() as conn:
target = conn.execute('SELECT id FROM users WHERE id = ?', (user_id,)).fetchone()
if target is None:
@@ -106,11 +107,12 @@ def reset_user_otp(user_id: str, _: dict = Depends(require_admin)):
)
conn.execute('DELETE FROM otp_recovery_codes WHERE user_id = ?', (user_id,))
conn.commit()
record_audit_event(current_user['id'], 'otp_reset', 'user', user_id)
return {'status': 'otp_reset', 'enabled': False, 'user_id': user_id}
@router.post('/users', status_code=201)
def create_user(payload: AdminUserCreate, _: dict = Depends(require_admin)):
def create_user(payload: AdminUserCreate, current_user: dict = Depends(require_admin)):
username = payload.username.strip()
email = payload.email.strip()
if not username or not email or len(payload.password) < 8:
@@ -141,6 +143,7 @@ def create_user(payload: AdminUserCreate, _: dict = Depends(require_admin)):
send_verification_email(row['email'], row['username'], verification_url)
except Exception as error:
raise HTTPException(status_code=503, detail=f'User created but verification email could not be sent: {error}') from error
record_audit_event(current_user['id'], 'user_created', 'user', row['id'], details={'is_admin': bool(payload.is_admin)})
return public_user(row)
@@ -166,19 +169,21 @@ def get_admin_themes(_: dict = Depends(require_admin)):
@router.put('/themes')
def update_admin_themes(payload: AdminThemesUpdate, _: dict = Depends(require_admin)):
def update_admin_themes(payload: AdminThemesUpdate, current_user: dict = Depends(require_admin)):
try:
enabled = save_enabled_themes(payload.themes)
except ValueError as error:
raise HTTPException(status_code=422, detail=str(error)) from error
record_audit_event(current_user['id'], 'themes_updated', 'application', details={'themes': enabled})
return {'themes': THEMES, 'enabled': enabled}
@router.put('/smtp')
def update_admin_smtp_settings(payload: AdminSmtpUpdate, _: dict = Depends(require_admin)):
def update_admin_smtp_settings(payload: AdminSmtpUpdate, current_user: dict = Depends(require_admin)):
current = get_smtp_settings()
values = validate_smtp_values(payload, current)
save_smtp_settings(values)
record_audit_event(current_user['id'], 'smtp_settings_updated', 'application', details={'host': values['smtp_host'], 'port': values['smtp_port'], 'username': values['smtp_username'], 'tls': values['smtp_use_tls']})
return public_smtp_settings(values)
@@ -259,6 +264,7 @@ def update_user_privileges(
'SELECT id, username, email, is_admin, avatar_url, bio, created_at, email_verified FROM users WHERE id = ?',
(user_id,),
).fetchone()
record_audit_event(current_user['id'], 'user_privileges_updated', 'user', user_id, details={'is_admin': bool(payload.is_admin)})
return public_user(row)
@@ -281,13 +287,15 @@ def delete_user(user_id: str, current_user: dict = Depends(require_admin)):
conn.execute('DELETE FROM links WHERE user_id = ?', (user_id,))
conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
record_audit_event(current_user['id'], 'user_deleted', 'user', user_id)
return {'status': 'deleted', 'id': user_id}
@router.delete('/labels/{label_id}')
def admin_delete_label(label_id: str, _: dict = Depends(require_admin)):
def admin_delete_label(label_id: str, current_user: dict = Depends(require_admin)):
if not delete_label(label_id, is_admin=True):
raise HTTPException(status_code=404, detail='Label not found')
record_audit_event(current_user['id'], 'label_deleted', 'label', label_id)
return {'status': 'deleted', 'id': label_id}
@@ -346,7 +354,7 @@ def get_plugin(plugin_name: str, _: dict = Depends(require_admin)):
def update_plugin(
plugin_name: str,
payload: AdminPluginUpdate,
_: dict = Depends(require_admin),
current_user: dict = Depends(require_admin),
):
with get_connection() as conn:
current = conn.execute(
@@ -375,6 +383,7 @@ def update_plugin(
)
conn.commit()
record_audit_event(current_user['id'], 'plugin_updated', 'plugin', plugin_name, details={'enabled': enabled})
return {
'name': plugin_name,
'enabled': enabled,
+3
View File
@@ -10,6 +10,7 @@ from backend.app.services.link_service import create_link, delete_link, find_own
from backend.app.database import get_connection
from backend.app.services.plugin_manager import plugin_manager
from backend.app.services.token_service import validate_token
from backend.app.services.audit_service import record_audit_event
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -127,6 +128,7 @@ def delete_link_endpoint(
raise HTTPException(status_code=502, detail=result.get('reason', 'Could not delete Mastodon posts'))
if not delete_link(link_id, info['user_id']):
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
record_audit_event(info['user_id'], 'link_deleted', 'link', link_id)
return {'status': 'deleted', 'id': link_id}
@@ -150,6 +152,7 @@ def post_link_to_mastodon(
result = plugin_manager.post_to_mastodon({'type': 'link_created', **event})
if result.get('status') == 'posted':
mark_mastodon_posted(link_id, info['user_id'], result.get('post_id'))
record_audit_event(info['user_id'], 'mastodon_posted', 'link', link_id)
return {'status': 'posted', 'post_id': result.get('post_id')}
if result.get('status') == 'skipped':
raise HTTPException(status_code=409, detail='Mastodon is not enabled or configured')
+9
View File
@@ -19,6 +19,7 @@ from backend.app.services.email_addresses import add_user_email_address, create_
from backend.app.services.email_service import send_verification_email, smtp_configured
from backend.app.core.config import settings
from backend.app.services.secret_store import decrypt_secret, encrypt_secret
from backend.app.services.audit_service import record_audit_event
router = APIRouter()
@@ -116,6 +117,7 @@ def update_password(payload: PasswordUpdate, user: dict = Depends(get_current_us
(hash_password(payload.new_password), user['id']),
)
conn.commit()
record_audit_event(user['id'], 'password_changed', 'user', user['id'])
return {'status': 'password_updated'}
@@ -132,6 +134,7 @@ def setup_otp(user: dict = Depends(get_current_user)):
with get_connection() as conn:
conn.execute('UPDATE users SET otp_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (encrypt_secret(secret), user['id']))
conn.commit()
record_audit_event(user['id'], 'otp_enrolled', 'user', user['id'])
return {
'secret': secret,
'otpauth_url': provisioning_uri(secret, user['username']),
@@ -157,6 +160,7 @@ def update_otp(payload: OtpUpdate, user: dict = Depends(get_current_user)):
else:
conn.execute('UPDATE users SET otp_enabled = 0, otp_secret = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],))
conn.commit()
record_audit_event(user['id'], f'otp_{payload.action}d', 'user', user['id'])
return {'status': 'updated', 'enabled': payload.action == 'enable'}
@@ -172,6 +176,7 @@ def recover_otp(payload: OtpRecovery, user: dict = Depends(get_current_user)):
(user['id'],),
)
conn.commit()
record_audit_event(user['id'], 'otp_recovered', 'user', user['id'])
return {'status': 'otp_recovered', 'enabled': False}
@@ -259,6 +264,7 @@ def remove_additional_email(address_id: str, user: dict = Depends(get_current_us
conn.commit()
if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail='Email address not found')
record_audit_event(user['id'], 'email_address_deleted', 'email_address', address_id)
return {'status': 'deleted', 'id': address_id}
@@ -287,6 +293,7 @@ def make_email_primary(address_id: str, user: dict = Depends(get_current_user)):
(address['email'], user['id']),
)
conn.commit()
record_audit_event(user['id'], 'primary_email_changed', 'user', user['id'])
return {'status': 'updated', 'email': address['email']}
@@ -318,6 +325,7 @@ def edit_label(label_id: str, payload: LabelUpdate, user: dict = Depends(get_cur
def remove_label(label_id: str, user: dict = Depends(get_current_user)):
if not delete_label(label_id, user['id']):
raise HTTPException(status_code=404, detail='Label not found or not owned by user')
record_audit_event(user['id'], 'label_deleted', 'label', label_id)
return {'status': 'deleted', 'id': label_id}
@@ -361,6 +369,7 @@ async def upload_avatar(
(avatar_url, user['id']),
)
conn.commit()
record_audit_event(user['id'], 'avatar_updated', 'user', user['id'])
return {'avatar_url': avatar_url}