Log details obfuscated to not leak info

This commit is contained in:
2026-08-26 20:51:29 +02:00
parent b4b40e5c2c
commit 04b8a5a8b9
10 changed files with 109 additions and 22 deletions
+10 -5
View File
@@ -5,7 +5,7 @@ import json
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from backend.app.api.dependencies import require_admin
@@ -23,8 +23,11 @@ from backend.app.services.theme_service import THEMES, get_enabled_themes, save_
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
from backend.app.core.errors import public_error, redacted_error, request_id
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
class AdminPluginUpdate(BaseModel):
@@ -112,7 +115,7 @@ def reset_user_otp(user_id: str, current_user: dict = Depends(require_admin)):
@router.post('/users', status_code=201)
def create_user(payload: AdminUserCreate, current_user: dict = Depends(require_admin)):
def create_user(payload: AdminUserCreate, request: Request, 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:
@@ -142,7 +145,8 @@ def create_user(payload: AdminUserCreate, current_user: dict = Depends(require_a
try:
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
logger.error('User verification email failed request_id=%s error=%s', request_id(request), redacted_error(error))
raise HTTPException(status_code=503, detail=public_error(request, 'User created but verification email could not be sent.')) from error
record_audit_event(current_user['id'], 'user_created', 'user', row['id'], details={'is_admin': bool(payload.is_admin)})
return public_user(row)
@@ -188,7 +192,7 @@ def update_admin_smtp_settings(payload: AdminSmtpUpdate, current_user: dict = De
@router.post('/smtp/test')
def validate_admin_smtp(payload: AdminSmtpUpdate, current_user: dict = Depends(require_admin)):
def validate_admin_smtp(payload: AdminSmtpUpdate, request: Request, current_user: dict = Depends(require_admin)):
values = validate_smtp_values(payload, get_smtp_settings())
now = datetime.now(timezone.utc)
with get_connection() as conn:
@@ -209,7 +213,8 @@ def validate_admin_smtp(payload: AdminSmtpUpdate, current_user: dict = Depends(r
try:
send_test_email(current_user['email'], values)
except Exception as error:
raise HTTPException(status_code=503, detail=f'SMTP validation failed: {error}') from error
logger.error('SMTP validation failed request_id=%s error=%s', request_id(request), redacted_error(error))
raise HTTPException(status_code=503, detail=public_error(request, 'SMTP validation failed.')) from error
sends = int(rate.get('sends', 0)) + 1
updated_rate = {'sends': sends, 'last_sent': now.isoformat()}
if sends >= 5:
+8 -3
View File
@@ -10,12 +10,15 @@ from starlette.requests import Request
from backend.app.api.dependencies import get_current_user
from backend.app.services.mastodon_oauth import finish_authorization, start_authorization
from backend.app.core.errors import public_error, redacted_error, request_id
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
@router.get('/oauth/start')
def oauth_start(instance: str = 'mastodon.social', user: dict = Depends(get_current_user)):
def oauth_start(request: Request, instance: str = 'mastodon.social', user: dict = Depends(get_current_user)):
try:
authorization_url = start_authorization(user['id'], instance)
except HTTPError as error:
@@ -27,7 +30,8 @@ def oauth_start(instance: str = 'mastodon.social', user: dict = Depends(get_curr
headers=headers,
) from error
except Exception as error:
raise HTTPException(status_code=502, detail=f'Could not register with Mastodon: {error}') from error
logger.error('Mastodon registration failed request_id=%s error=%s', request_id(request), redacted_error(error))
raise HTTPException(status_code=502, detail=public_error(request, 'Could not register with Mastodon.')) from error
return {'authorization_url': authorization_url}
@@ -38,5 +42,6 @@ def oauth_callback(request: Request, code: str | None = None, state: str | None
try:
finish_authorization(code, state)
except Exception as callback_error:
return RedirectResponse(f'/profile?mastodon_error={quote(str(callback_error))}')
logger.error('Mastodon callback failed request_id=%s error=%s', request_id(request), redacted_error(callback_error))
return RedirectResponse(f'/profile?mastodon_error={quote(public_error(request, "Could not complete Mastodon authorization."))}')
return RedirectResponse('/profile?mastodon=connected')
+7 -3
View File
@@ -5,14 +5,17 @@ from datetime import datetime, timedelta, timezone
import json
from uuid import uuid4
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from backend.app.core.config import settings
from backend.app.database import get_connection, hash_password
from backend.app.services.email_service import get_smtp_settings, save_smtp_settings, send_test_email
from backend.app.core.errors import public_error, redacted_error, request_id
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
class SetupRequest(BaseModel):
@@ -92,7 +95,7 @@ def save_configuration(payload: SetupRequest):
@router.post('/test-mail')
def test_mail(payload: TestMailRequest | None = None):
def test_mail(request: Request, payload: TestMailRequest | None = None):
if has_administrator():
raise HTTPException(status_code=409, detail='LinkLog is already configured')
pending = get_pending_setup()
@@ -119,7 +122,8 @@ def test_mail(payload: TestMailRequest | None = None):
try:
send_test_email(payload.email if payload and payload.email else pending['email'])
except Exception as error:
raise HTTPException(status_code=503, detail=f'SMTP test mail could not be sent: {error}') from error
logger.error('SMTP test mail failed request_id=%s error=%s', request_id(request), redacted_error(error))
raise HTTPException(status_code=503, detail=public_error(request, 'SMTP test mail could not be sent.')) from error
sends = int(rate.get('sends', 0)) + 1
updated_rate = {'sends': sends, 'last_sent': now.isoformat()}
+10 -5
View File
@@ -7,7 +7,7 @@ from io import BytesIO
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from PIL import Image, UnidentifiedImageError
from pydantic import BaseModel
@@ -20,8 +20,11 @@ from backend.app.services.email_service import send_verification_email, smtp_con
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
from backend.app.core.errors import public_error, redacted_error, request_id
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
MAX_AVATAR_BYTES = 2 * 1024 * 1024
MAX_AVATAR_PIXELS = 25_000_000
@@ -186,7 +189,7 @@ def get_additional_emails(user: dict = Depends(get_current_user)):
@router.post('/emails', status_code=201)
def add_additional_email(payload: AdditionalEmail, user: dict = Depends(get_current_user)):
def add_additional_email(payload: AdditionalEmail, request: Request, user: dict = Depends(get_current_user)):
email = payload.email.strip().lower()
if email == user['email'].lower():
raise HTTPException(status_code=409, detail='This is already the primary email address')
@@ -209,7 +212,8 @@ def add_additional_email(payload: AdditionalEmail, user: dict = Depends(get_curr
try:
send_verification_email(email_address, user['username'], verification_url)
except Exception as error:
raise HTTPException(status_code=503, detail=f'Email address added but verification email could not be sent: {error}') from error
logger.error('Additional email verification failed request_id=%s error=%s', request_id(request), redacted_error(error))
raise HTTPException(status_code=503, detail=public_error(request, 'Email address added but verification email could not be sent.')) from error
with get_connection() as conn:
conn.execute(
'''INSERT INTO app_settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
@@ -221,7 +225,7 @@ def add_additional_email(payload: AdditionalEmail, user: dict = Depends(get_curr
@router.post('/emails/{address_id}/resend')
def resend_additional_email(address_id: str, user: dict = Depends(get_current_user)):
def resend_additional_email(address_id: str, request: Request, user: dict = Depends(get_current_user)):
now = datetime.now(timezone.utc)
setting_name = f'email_verify_rate:{address_id}'
with get_connection() as conn:
@@ -245,7 +249,8 @@ def resend_additional_email(address_id: str, user: dict = Depends(get_current_us
try:
send_verification_email(email, user['username'], verification_url)
except Exception as error:
raise HTTPException(status_code=503, detail=f'Verification email could not be sent: {error}') from error
logger.error('Verification email resend failed request_id=%s error=%s', request_id(request), redacted_error(error))
raise HTTPException(status_code=503, detail=public_error(request, 'Verification email could not be sent.')) from error
sends = int(rate.get('sends', 0)) + 1
updated = {'sends': sends, 'last_sent': now.isoformat()}
if sends >= 5:
+23
View File
@@ -0,0 +1,23 @@
import re
from uuid import uuid4
from fastapi import Request
SENSITIVE_PATTERN = re.compile(
r'(?i)(authorization\s*[:=]\s*bearer\s+[^\s,;]+|'
r'(?:token|password|secret|otp|code)(?:[_-](?:token|password|secret|code))?\s*[:=]\s*[^\s,;&]+|'
r'([?&](?:token|code|password|secret|otp)=[^&#\s]+))'
)
def request_id(request: Request) -> str:
return getattr(request.state, 'request_id', None) or str(uuid4())
def redacted_error(error: Exception) -> str:
return SENSITIVE_PATTERN.sub('[REDACTED]', str(error))
def public_error(request: Request, message: str) -> str:
return f'{message} Reference: {request_id(request)}'
+11 -1
View File
@@ -1,8 +1,9 @@
## Copyright © 2026 Olaf Kolkman
## SPDX-License-Identifier: GPL-3.0-or-later
from fastapi import FastAPI
from fastapi import FastAPI, Request
import logging
from uuid import uuid4
from fastapi.responses import HTMLResponse
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
@@ -24,6 +25,15 @@ from backend.app.services.link_service import get_public_profile, list_public_li
logging.basicConfig(level=getattr(logging, settings.log_level, logging.INFO))
app = FastAPI(title='LinkLog API', version=settings.version)
@app.middleware('http')
async def add_request_id(request: Request, call_next):
request.state.request_id = request.headers.get('X-Request-ID') or str(uuid4())
response = await call_next(request)
response.headers['X-Request-ID'] = request.state.request_id
return response
app.mount('/static', StaticFiles(directory='frontend/static'), name='static')
app.mount('/media', StaticFiles(directory=AVATARS_DIR), name='media')
app.include_router(auth_router, prefix='/api/auth')