Add owner-only logging and Brickset API call tracking
LIM configured no logging at all, so the instruction fetcher had no trail to follow. Adds a rotating file handler on the root logger plus two owner-only admin pages. - Admin > Logs: filter by level, source and text; tracebacks kept with their error; auto-refresh, raw download, rotated-file browsing and rotate-now - Admin > Logs > Brickset API calls: every outbound call recorded with method, params, caller, duration, result count and error, plus daily quota accounting (only getSets counts) and a comparison against getKeyUsageStats - New owner concept (services/ownership.py): a single account, distinct from is_admin, since these pages expose other admins activity. Defaults to the first admin; flask owner show/set to change - Services now log through module loggers; retention setting and pruning for call records - New CLI: flask owner show/set, flask logs path/prune - New table brickset_api_calls, created automatically at boot; LOG_* env vars, /data/logs in Docker
This commit is contained in:
1 parent
89f65e2d74
commit
f7c20de5ac
27 files changed
+1914
-35
No files matched your search
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Who owns this LIM instance.
|
||||
|
||||
LIM only had is_admin, which is fine for day-to-day administration but too
|
||||
broad for things that expose the whole instance: the log viewer shows every
|
||||
request, every error and every API call, including other admins' activity.
|
||||
|
||||
The owner is a single account:
|
||||
|
||||
* whatever `owner_user_id` in site_settings points at, if that user exists;
|
||||
* otherwise the lowest-numbered admin (in practice the account created by
|
||||
`flask init_db`), which is then remembered.
|
||||
|
||||
Change it with `flask owner set <username>`.
|
||||
|
||||
Created by: Jess Rogerson (yelling commands at Claude.AI)
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask import flash, redirect, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models.site_setting import SiteSetting
|
||||
from app.models.user import User
|
||||
|
||||
OWNER_SETTING = 'owner_user_id'
|
||||
|
||||
|
||||
def get_owner():
|
||||
"""The User who owns this instance, or None on an empty database."""
|
||||
raw = SiteSetting.get(OWNER_SETTING, '')
|
||||
if raw and str(raw).strip().isdigit():
|
||||
owner = db.session.get(User, int(str(raw).strip()))
|
||||
if owner is not None:
|
||||
return owner
|
||||
|
||||
# Nothing stored (or it points at a deleted account): fall back to the
|
||||
# first admin and remember the answer.
|
||||
owner = (User.query.filter_by(is_admin=True).order_by(User.id).first()
|
||||
or User.query.order_by(User.id).first())
|
||||
if owner is not None:
|
||||
try:
|
||||
SiteSetting.set(OWNER_SETTING, str(owner.id))
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
return owner
|
||||
|
||||
|
||||
def get_owner_id():
|
||||
owner = get_owner()
|
||||
return owner.id if owner else None
|
||||
|
||||
|
||||
def set_owner(user):
|
||||
"""Hand ownership to a user. Returns the user."""
|
||||
SiteSetting.set(OWNER_SETTING, str(user.id))
|
||||
if not user.is_admin:
|
||||
user.is_admin = True
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
def is_owner(user=None):
|
||||
"""Is this user (default: the logged-in one) the owner?"""
|
||||
user = user or current_user
|
||||
if user is None or not getattr(user, 'is_authenticated', False):
|
||||
return False
|
||||
owner_id = get_owner_id()
|
||||
return owner_id is not None and user.id == owner_id
|
||||
|
||||
|
||||
def owner_required(f):
|
||||
"""Stricter than admin_required: this account only."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Please log in to access this page.', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
if not is_owner(current_user):
|
||||
flash('That area is restricted to the instance owner.', 'danger')
|
||||
return redirect(url_for('main.index'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
Reference in new issue
Block a user