from flask import Flask, request, url_for import click import hashlib from pathlib import Path from .extensions import db, login_manager from .models import User, AppSetting from .config import Config THEMES = {name: {} for name in ("bootstrap", "flatly", "darkly", "pulse")} class RemoveVaryCookieMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): path = environ.get("PATH_INFO", "") def custom_start_response(status, headers, exc_info=None): if path == "/favicon.ico" or path.endswith((".css", ".js")): result, vary_values = [], [] for name, value in headers: if name.lower() == "vary": vary_values.extend(item.strip() for item in value.split(",") if item.strip()) else: result.append((name, value)) vary_values = [value for value in vary_values if value.lower() != "cookie"] if vary_values: result.append(("Vary", ", ".join(dict.fromkeys(vary_values)))) headers = result return start_response(status, headers, exc_info) return self.app(environ, custom_start_response) def _static_hashes(app): static_root = Path(app.static_folder) hashes = {} for path in static_root.rglob("*"): if path.is_file(): relative = path.relative_to(static_root).as_posix() hashes[relative] = hashlib.md5(path.read_bytes()).hexdigest() return hashes def _themes(app): return { "bootstrap": {"label": "Bootstrap", "url": app.config["THEME_BOOTSTRAP_URL"], "mode": "light"}, "flatly": {"label": "Flatly", "url": app.config["THEME_FLATLY_URL"], "mode": "light"}, "darkly": {"label": "Ciemny", "url": app.config["THEME_BOOTSTRAP_URL"], "mode": "dark"}, "pulse": {"label": "Pulse", "url": app.config["THEME_PULSE_URL"], "mode": "light"}, } def create_app(test_config=None): app = Flask(__name__) app.config.from_object(Config) if test_config: app.config.update(test_config) db.init_app(app); login_manager.init_app(app) from .database import prepare_database prepare_database(app) from .auth import auth from .main import main from .api import api from .openapi import docs app.register_blueprint(auth); app.register_blueprint(main) app.register_blueprint(api); app.register_blueprint(docs) static_hashes = _static_hashes(app) @app.context_processor def inject_static_url(): def static_url(filename): return url_for("static", filename=filename, v=static_hashes.get(filename, "")) return {"static_url": static_url} @app.after_request def set_cache_headers(response): if request.path == "/favicon.ico": response.headers.pop("Cache-Control", None) response.headers.pop("Content-Disposition", None) return response if request.endpoint == "static": filename = request.view_args.get("filename", "") if request.view_args else "" if filename.endswith((".css", ".js")): response.headers["Cache-Control"] = "public, max-age=2592000, immutable" response.headers.pop("Content-Disposition", None) elif response.mimetype == "text/html": response.headers["Cache-Control"] = "no-store, private, must-revalidate" return response @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id)) @login_manager.request_loader def load_user_from_request(req): header = req.headers.get("Authorization", "") scheme, _, token = header.partition(" ") if scheme.lower() != "bearer" or not token.strip(): return None from .api_tokens import load_access_token return load_access_token(token.strip()) @login_manager.unauthorized_handler def unauthorized(): if request.path.startswith("/api/"): return {"ok": False, "message": "Wymagane uwierzytelnienie Bearer lub aktywna sesja"}, 401 from flask import redirect return redirect(url_for("auth.login", next=request.full_path.rstrip("?"))) @app.context_processor def inject_theme(): themes = _themes(app) record = db.session.get(AppSetting, "theme") theme_name = record.value if record and record.value in themes else "bootstrap" return { "app_theme": themes[theme_name], "app_theme_name": theme_name, "themes": themes, "asset_urls": { "bootstrap_js": app.config["BOOTSTRAP_JS_URL"], "choices_css": app.config["CHOICES_CSS_URL"], "choices_js": app.config["CHOICES_JS_URL"], "chart_js": app.config["CHART_JS_URL"], }, } @app.get("/health") def health(): return {"status": "ok"} @app.get("/favicon.ico") def favicon(): return app.response_class(status=204) @app.cli.command("reset-admin-password") @click.option("--email", required=True, help="Adres e-mail konta administratora.") @click.option("--password", prompt=True, hide_input=True, confirmation_prompt=True, help="Nowe hasło. Bez tej opcji polecenie poprosi o hasło bez wyświetlania go.") def reset_admin_password(email, password): normalized_email = email.strip().lower() user = User.query.filter_by(email=normalized_email).first() if not user: raise click.ClickException("Nie znaleziono użytkownika o podanym adresie e-mail.") if user.role != "admin": raise click.ClickException("Wskazany użytkownik nie ma roli administratora.") if len(password) < 8: raise click.ClickException("Hasło musi mieć co najmniej 8 znaków.") user.set_password(password) user.active = True db.session.commit() click.echo(f"Zresetowano hasło administratora: {normalized_email}") app.wsgi_app = RemoveVaryCookieMiddleware(app.wsgi_app) return app