Audit trail
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user