Files
shopdb-flask/shopdb/__init__.py
cproudlock e18c7c2d87 Add system settings, audit logging, user management, and dark mode fixes
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>
2026-02-04 22:16:56 -05:00

206 lines
6.4 KiB
Python

"""Flask application factory."""
import os
import logging
from flask import Flask, send_from_directory
from .config import config
from .extensions import db, migrate, jwt, cors, ma, init_extensions
from .plugins import plugin_manager
def create_app(config_name: str = None) -> Flask:
"""
Application factory.
Args:
config_name: Configuration name ('development', 'production', 'testing')
Returns:
Configured Flask application
"""
if config_name is None:
config_name = os.environ.get('FLASK_ENV', 'development')
app = Flask(__name__, instance_relative_config=True)
# Load configuration
app.config.from_object(config.get(config_name, config['default']))
# Load instance config if exists
app.config.from_pyfile('config.py', silent=True)
# Ensure instance folder exists
os.makedirs(app.instance_path, exist_ok=True)
# Configure logging
configure_logging(app)
# Initialize extensions
init_extensions(app)
# Initialize plugin manager
with app.app_context():
plugin_manager.init_app(app, db)
# Register core blueprints
register_blueprints(app)
# Register CLI commands
register_cli_commands(app)
# Register error handlers
register_error_handlers(app)
# Serve Vue frontend
register_frontend_routes(app)
# JWT user loader (identity is a string in JWT, convert to int for DB lookup)
@jwt.user_lookup_loader
def user_lookup_callback(_jwt_header, jwt_data):
from .core.models import User
identity = jwt_data["sub"]
return User.query.get(int(identity))
return app
def register_blueprints(app: Flask):
"""Register core API blueprints."""
from .core.api import (
auth_bp,
assets_bp,
machines_bp,
machinetypes_bp,
pctypes_bp,
statuses_bp,
vendors_bp,
models_bp,
businessunits_bp,
locations_bp,
operatingsystems_bp,
dashboard_bp,
applications_bp,
knowledgebase_bp,
search_bp,
reports_bp,
collector_bp,
employees_bp,
slides_bp,
settings_bp,
auditlogs_bp,
users_bp,
)
api_prefix = '/api'
app.register_blueprint(auth_bp, url_prefix=f'{api_prefix}/auth')
app.register_blueprint(assets_bp, url_prefix=f'{api_prefix}/assets')
app.register_blueprint(machines_bp, url_prefix=f'{api_prefix}/machines')
app.register_blueprint(machinetypes_bp, url_prefix=f'{api_prefix}/machinetypes')
app.register_blueprint(pctypes_bp, url_prefix=f'{api_prefix}/pctypes')
app.register_blueprint(statuses_bp, url_prefix=f'{api_prefix}/statuses')
app.register_blueprint(vendors_bp, url_prefix=f'{api_prefix}/vendors')
app.register_blueprint(models_bp, url_prefix=f'{api_prefix}/models')
app.register_blueprint(businessunits_bp, url_prefix=f'{api_prefix}/businessunits')
app.register_blueprint(locations_bp, url_prefix=f'{api_prefix}/locations')
app.register_blueprint(operatingsystems_bp, url_prefix=f'{api_prefix}/operatingsystems')
app.register_blueprint(dashboard_bp, url_prefix=f'{api_prefix}/dashboard')
app.register_blueprint(applications_bp, url_prefix=f'{api_prefix}/applications')
app.register_blueprint(knowledgebase_bp, url_prefix=f'{api_prefix}/knowledgebase')
app.register_blueprint(search_bp, url_prefix=f'{api_prefix}/search')
app.register_blueprint(reports_bp, url_prefix=f'{api_prefix}/reports')
app.register_blueprint(collector_bp, url_prefix=f'{api_prefix}/collector')
app.register_blueprint(employees_bp, url_prefix=f'{api_prefix}/employees')
app.register_blueprint(slides_bp, url_prefix=f'{api_prefix}/slides')
app.register_blueprint(settings_bp, url_prefix=f'{api_prefix}/settings')
app.register_blueprint(auditlogs_bp, url_prefix=f'{api_prefix}/auditlogs')
app.register_blueprint(users_bp, url_prefix=f'{api_prefix}/users')
def register_cli_commands(app: Flask):
"""Register Flask CLI commands."""
from .plugins.cli import plugin_cli
from .cli import db_cli, seed_cli
app.cli.add_command(plugin_cli)
app.cli.add_command(db_cli)
app.cli.add_command(seed_cli)
def register_error_handlers(app: Flask):
"""Register error handlers."""
from .utils.responses import error_response, ErrorCodes
from .exceptions import ShopDBException
@app.errorhandler(ShopDBException)
def handle_shopdb_exception(error):
http_codes = {
'NOT_FOUND': 404,
'UNAUTHORIZED': 401,
'FORBIDDEN': 403,
'CONFLICT': 409,
'VALIDATION_ERROR': 400,
}
http_code = http_codes.get(error.code, 400)
return error_response(
error.code,
error.message,
details=error.details,
http_code=http_code
)
@app.errorhandler(404)
def not_found_error(error):
return error_response(
ErrorCodes.NOT_FOUND,
'Resource not found',
http_code=404
)
@app.errorhandler(500)
def internal_error(error):
return error_response(
ErrorCodes.INTERNAL_ERROR,
'An internal error occurred',
http_code=500
)
@app.errorhandler(401)
def unauthorized_error(error):
return error_response(
ErrorCodes.UNAUTHORIZED,
'Authentication required',
http_code=401
)
def register_frontend_routes(app: Flask):
"""Serve Vue frontend static files."""
frontend_dist = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'dist')
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve_frontend(path):
# Don't serve API routes as frontend
if path.startswith('api/'):
from .utils.responses import error_response, ErrorCodes
return error_response(ErrorCodes.NOT_FOUND, 'API endpoint not found', http_code=404)
# Serve static assets
if path and os.path.exists(os.path.join(frontend_dist, path)):
return send_from_directory(frontend_dist, path)
# Serve index.html for SPA routing
return send_from_directory(frontend_dist, 'index.html')
def configure_logging(app: Flask):
"""Configure application logging."""
log_level = app.config.get('LOG_LEVEL', 'INFO')
logging.basicConfig(
level=getattr(logging, log_level),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)