Files
Fossign/app/__init__.py

137 lines
5.7 KiB
Python

import os
from flask import Flask, jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge
from .extensions import db, login_manager
from .models import AppSettings, User
from .cli import init_db_command
def create_app():
app = Flask(__name__, instance_relative_config=True)
# Basic config
# Flask ships with SECRET_KEY=None by default, so setdefault() won't override it.
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY") or "dev-secret-change-me"
app.config.setdefault(
"SQLALCHEMY_DATABASE_URI",
os.environ.get("DATABASE_URL", "sqlite:///" + os.path.join(app.instance_path, "signage.sqlite")),
)
app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False)
app.config.setdefault("UPLOAD_FOLDER", os.path.join(app.root_path, "static", "uploads"))
# NOTE: Videos should be max 250MB.
# Flask's MAX_CONTENT_LENGTH applies to the full request payload (multipart includes overhead).
# We set this slightly above 250MB to allow for multipart/form fields overhead, while still
# blocking excessively large uploads early.
app.config.setdefault("MAX_CONTENT_LENGTH", 260 * 1024 * 1024) # ~260MB request cap
# Explicit per-video validation lives in the upload route; this app-wide cap is a safety net.
os.makedirs(app.instance_path, exist_ok=True)
os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
# Init extensions
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = "auth.login"
# Lightweight migration(s) for SQLite DBs created before new columns existed.
# This avoids requiring Alembic for this small project.
with app.app_context():
try:
uri = app.config.get("SQLALCHEMY_DATABASE_URI", "") or ""
if uri.startswith("sqlite:"):
cols = [r[1] for r in db.session.execute(db.text("PRAGMA table_info(user)")).fetchall()]
if "email" not in cols:
db.session.execute(db.text("ALTER TABLE user ADD COLUMN email VARCHAR(255)"))
# Best-effort unique index (SQLite doesn't support adding unique constraints after the fact).
db.session.execute(db.text("CREATE UNIQUE INDEX IF NOT EXISTS ix_user_email ON user (email)"))
db.session.commit()
# Displays: ensure optional description column exists.
display_cols = [
r[1] for r in db.session.execute(db.text("PRAGMA table_info(display)")).fetchall()
]
if "description" not in display_cols:
db.session.execute(db.text("ALTER TABLE display ADD COLUMN description VARCHAR(200)"))
db.session.commit()
# Companies: optional per-company storage quota
company_cols = [
r[1] for r in db.session.execute(db.text("PRAGMA table_info(company)")).fetchall()
]
if "storage_max_bytes" not in company_cols:
db.session.execute(db.text("ALTER TABLE company ADD COLUMN storage_max_bytes BIGINT"))
db.session.commit()
# AppSettings: create settings table if missing.
# (PRAGMA returns empty if the table doesn't exist.)
settings_cols = [
r[1] for r in db.session.execute(db.text("PRAGMA table_info(app_settings)")).fetchall()
]
if not settings_cols:
AppSettings.__table__.create(db.engine, checkfirst=True)
# AppSettings: add public_domain column if missing.
if settings_cols and "public_domain" not in settings_cols:
db.session.execute(db.text("ALTER TABLE app_settings ADD COLUMN public_domain VARCHAR(255)"))
db.session.commit()
except Exception:
db.session.rollback()
@login_manager.user_loader
def load_user(user_id: str):
return db.session.get(User, int(user_id))
# CLI
app.cli.add_command(init_db_command)
# Blueprints
from .routes.auth import bp as auth_bp
from .routes.admin import bp as admin_bp
from .routes.company import bp as company_bp
from .routes.display import bp as display_bp
from .routes.api import bp as api_bp
app.register_blueprint(auth_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(company_bp)
app.register_blueprint(display_bp)
app.register_blueprint(api_bp)
# Home
from flask import redirect, url_for
from flask_login import current_user
@app.get("/")
def index():
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
if current_user.is_admin:
return redirect(url_for("admin.dashboard"))
return redirect(url_for("company.dashboard"))
@app.errorhandler(RequestEntityTooLarge)
def handle_request_too_large(e):
"""Return a user-friendly message when uploads exceed MAX_CONTENT_LENGTH."""
# Keep behavior consistent with our AJAX endpoints.
wants_json = (
(request.headers.get("X-Requested-With") == "XMLHttpRequest")
or ("application/json" in (request.headers.get("Accept") or ""))
or request.is_json
or (request.form.get("response") == "json")
)
msg = "Upload too large. Videos must be 250MB or smaller."
if wants_json:
return jsonify({"ok": False, "error": msg}), 413
# For non-AJAX form posts, redirect back with a flash message.
from flask import flash, redirect
flash(msg, "danger")
return redirect(request.referrer or url_for("company.dashboard")), 413
return app