System Settings: - Add SystemSettings.vue with Zabbix integration, SMTP/email config, SAML SSO settings - Add Setting model with key-value storage and typed values - Add settings API with caching Audit Logging: - Add AuditLog model tracking user, IP, action, entity changes - Add comprehensive audit logging to all CRUD operations: - Machines, Computers, Equipment, Network devices, VLANs, Subnets - Printers, USB devices (including checkout/checkin) - Applications, Settings, Users/Roles - Track old/new values for all field changes - Mask sensitive values (passwords, tokens) in logs User Management: - Add UsersList.vue with full user CRUD - Add Role management with granular permissions - Add 41 predefined permissions across 10 categories - Add users API with roles and permissions endpoints Reports: - Add TonerReport.vue for printer supply monitoring Dark Mode Fixes: - Fix map position section in PCForm, PrinterForm - Fix alert-warning in KnowledgeBaseDetail - All components now use CSS variables for theming CLI Commands: - Add flask seed permissions - Add flask seed settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
147 lines
4.4 KiB
Python
147 lines
4.4 KiB
Python
"""Audit log API routes."""
|
|
|
|
from flask import Blueprint, request
|
|
from flask_jwt_extended import jwt_required
|
|
|
|
from shopdb.core.models import AuditLog
|
|
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
|
|
|
auditlogs_bp = Blueprint('auditlogs', __name__)
|
|
|
|
|
|
@auditlogs_bp.route('', methods=['GET'])
|
|
@jwt_required()
|
|
def list_auditlogs():
|
|
"""
|
|
List audit logs with filtering and pagination.
|
|
|
|
Query params:
|
|
page: Page number (default 1)
|
|
perpage: Items per page (default 50, max 200)
|
|
action: Filter by action (created, updated, deleted)
|
|
entitytype: Filter by entity type
|
|
userid: Filter by user ID
|
|
search: Search in entityname or username
|
|
from_date: Filter from date (ISO format)
|
|
to_date: Filter to date (ISO format)
|
|
"""
|
|
page = request.args.get('page', 1, type=int)
|
|
perpage = min(request.args.get('perpage', 50, type=int), 200)
|
|
|
|
query = AuditLog.query
|
|
|
|
# Filters
|
|
action = request.args.get('action')
|
|
if action:
|
|
query = query.filter(AuditLog.action == action)
|
|
|
|
entitytype = request.args.get('entitytype')
|
|
if entitytype:
|
|
query = query.filter(AuditLog.entitytype == entitytype)
|
|
|
|
userid = request.args.get('userid', type=int)
|
|
if userid:
|
|
query = query.filter(AuditLog.userid == userid)
|
|
|
|
search = request.args.get('search')
|
|
if search:
|
|
search_term = f'%{search}%'
|
|
query = query.filter(
|
|
(AuditLog.entityname.ilike(search_term)) |
|
|
(AuditLog.username.ilike(search_term))
|
|
)
|
|
|
|
from_date = request.args.get('from_date')
|
|
if from_date:
|
|
from datetime import datetime
|
|
try:
|
|
dt = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
|
|
query = query.filter(AuditLog.timestamp >= dt)
|
|
except ValueError:
|
|
pass
|
|
|
|
to_date = request.args.get('to_date')
|
|
if to_date:
|
|
from datetime import datetime
|
|
try:
|
|
dt = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
|
|
query = query.filter(AuditLog.timestamp <= dt)
|
|
except ValueError:
|
|
pass
|
|
|
|
# Order by most recent first
|
|
query = query.order_by(AuditLog.timestamp.desc())
|
|
|
|
# Paginate
|
|
pagination = query.paginate(page=page, per_page=perpage, error_out=False)
|
|
|
|
return success_response(
|
|
[log.to_dict() for log in pagination.items],
|
|
meta={
|
|
'page': page,
|
|
'perpage': perpage,
|
|
'total': pagination.total,
|
|
'pages': pagination.pages
|
|
}
|
|
)
|
|
|
|
|
|
@auditlogs_bp.route('/entity/<entitytype>/<int:entityid>', methods=['GET'])
|
|
@jwt_required()
|
|
def get_entity_history(entitytype: str, entityid: int):
|
|
"""Get audit history for a specific entity."""
|
|
logs = AuditLog.query.filter_by(
|
|
entitytype=entitytype,
|
|
entityid=entityid
|
|
).order_by(AuditLog.timestamp.desc()).all()
|
|
|
|
return success_response([log.to_dict() for log in logs])
|
|
|
|
|
|
@auditlogs_bp.route('/stats', methods=['GET'])
|
|
@jwt_required()
|
|
def get_stats():
|
|
"""Get audit log statistics."""
|
|
from sqlalchemy import func
|
|
from datetime import datetime, timedelta
|
|
|
|
# Actions by type
|
|
actions = db_func_count_by(AuditLog.action)
|
|
|
|
# Entity types
|
|
entities = db_func_count_by(AuditLog.entitytype)
|
|
|
|
# Recent activity (last 7 days)
|
|
week_ago = datetime.utcnow() - timedelta(days=7)
|
|
recent_count = AuditLog.query.filter(AuditLog.timestamp >= week_ago).count()
|
|
|
|
# Most active users (last 7 days)
|
|
from shopdb.extensions import db
|
|
active_users = db.session.query(
|
|
AuditLog.username,
|
|
func.count(AuditLog.auditlogid).label('count')
|
|
).filter(
|
|
AuditLog.timestamp >= week_ago,
|
|
AuditLog.username.isnot(None)
|
|
).group_by(AuditLog.username).order_by(func.count(AuditLog.auditlogid).desc()).limit(5).all()
|
|
|
|
return success_response({
|
|
'actions': actions,
|
|
'entities': entities,
|
|
'recentCount': recent_count,
|
|
'activeUsers': [{'username': u[0], 'count': u[1]} for u in active_users]
|
|
})
|
|
|
|
|
|
def db_func_count_by(column):
|
|
"""Helper to count grouped by a column."""
|
|
from sqlalchemy import func
|
|
from shopdb.extensions import db
|
|
|
|
results = db.session.query(
|
|
column,
|
|
func.count().label('count')
|
|
).group_by(column).all()
|
|
|
|
return {r[0]: r[1] for r in results}
|