Jun 27 MT-4
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
control/panel/__init__.py
|
||||
--------------------------
|
||||
Superadmin control panel — MT-4.
|
||||
|
||||
Serves as a STANDALONE Flask application at admin.jqc.app (separate WSGI
|
||||
process). Shares only the `control` package (models, provision, tenant_migrate)
|
||||
with the main app. Has NO import dependency on `app/`.
|
||||
|
||||
Entry point: wsgi_panel.py
|
||||
gunicorn wsgi_panel:panel_app
|
||||
|
||||
Env vars required (same /etc/jqc/control.env sourced by the main app):
|
||||
CONTROL_DATABASE_URL
|
||||
CONTROL_FERNET_KEY
|
||||
PANEL_SECRET_KEY — separate from the main app SECRET_KEY
|
||||
PANEL_IMPERSONATE_KEY — HMAC key for impersonation tokens (32+ random bytes)
|
||||
TENANT_BASE_DOMAIN — e.g. jqc.app
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from .auth import bp as auth_bp
|
||||
from .tenants import bp as tenants_bp
|
||||
|
||||
|
||||
def create_panel_app():
|
||||
app = Flask(__name__, template_folder='templates')
|
||||
|
||||
secret = os.environ.get('PANEL_SECRET_KEY')
|
||||
if not secret:
|
||||
raise RuntimeError(
|
||||
'PANEL_SECRET_KEY is not set. '
|
||||
'Generate one: python -c "import secrets; print(secrets.token_hex(32))"'
|
||||
)
|
||||
app.secret_key = secret
|
||||
|
||||
# ── Logging ──────────────────────────────────────────────────────────
|
||||
log_level = logging.INFO
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
)
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(log_level)
|
||||
sh.setFormatter(formatter)
|
||||
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)))), 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
fh = RotatingFileHandler(
|
||||
os.path.join(log_dir, 'jqc_panel.log'),
|
||||
maxBytes=5 * 1024 * 1024, backupCount=5)
|
||||
fh.setLevel(log_level)
|
||||
fh.setFormatter(formatter)
|
||||
|
||||
app.logger.setLevel(log_level)
|
||||
for h in (sh, fh):
|
||||
app.logger.addHandler(h)
|
||||
|
||||
# ── Blueprints ────────────────────────────────────────────────────────
|
||||
app.register_blueprint(auth_bp) # /login, /logout
|
||||
app.register_blueprint(tenants_bp) # /tenants/…
|
||||
|
||||
# ── Root redirect ─────────────────────────────────────────────────────
|
||||
from flask import redirect, url_for
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return redirect(url_for('tenants.list_tenants'))
|
||||
|
||||
# ── Security headers ──────────────────────────────────────────────────
|
||||
@app.after_request
|
||||
def security_headers(response):
|
||||
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
|
||||
response.headers.setdefault('X-Frame-Options', 'DENY')
|
||||
response.headers.setdefault('Referrer-Policy', 'strict-origin-when-cross-origin')
|
||||
response.headers.setdefault(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||
"font-src 'self' https://cdn.jsdelivr.net; "
|
||||
"img-src 'self' data:; "
|
||||
"frame-ancestors 'none';"
|
||||
)
|
||||
return response
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user