This commit is contained in:
Mateusz Gruszczyński
2026-07-14 15:28:23 +02:00
parent f6d588c56a
commit e26d9e4ff3
+65 -18
View File
@@ -9,15 +9,47 @@ from .config import Config
THEMES = {name: {} for name in ("bootstrap", "flatly", "darkly", "pulse")} 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): def _static_hashes(app):
static_root = Path(app.static_folder) static_root = Path(app.static_folder)
hashes = {} hashes = {}
for path in static_root.rglob("*"): for path in static_root.rglob("*"):
if path.is_file(): if path.is_file():
relative = path.relative_to(static_root).as_posix() relative = path.relative_to(static_root).as_posix()
hashes[relative] = hashlib.md5(path.read_bytes()).hexdigest() hashes[relative] = hashlib.md5(path.read_bytes()).hexdigest()
return hashes return hashes
def _themes(app): def _themes(app):
return { return {
"bootstrap": {"label": "Bootstrap", "url": app.config["THEME_BOOTSTRAP_URL"], "mode": "light"}, "bootstrap": {"label": "Bootstrap", "url": app.config["THEME_BOOTSTRAP_URL"], "mode": "light"},
@@ -31,14 +63,19 @@ def create_app(test_config=None):
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(Config) app.config.from_object(Config)
if test_config: app.config.update(test_config) if test_config: app.config.update(test_config)
db.init_app(app); login_manager.init_app(app) db.init_app(app); login_manager.init_app(app)
from .database import prepare_database from .database import prepare_database
prepare_database(app) prepare_database(app)
from .auth import auth; from .main import main
from .auth import auth
from .main import main
from .api import api from .api import api
from .openapi import docs from .openapi import docs
app.register_blueprint(auth); app.register_blueprint(main); app.register_blueprint(api); app.register_blueprint(docs)
app.register_blueprint(auth); app.register_blueprint(main)
app.register_blueprint(api); app.register_blueprint(docs)
static_hashes = _static_hashes(app) static_hashes = _static_hashes(app)
@@ -50,19 +87,17 @@ def create_app(test_config=None):
@app.after_request @app.after_request
def set_cache_headers(response): 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": if request.endpoint == "static":
filename = ( filename = request.view_args.get("filename", "") if request.view_args else ""
request.view_args.get("filename", "")
if request.view_args
else ""
)
if filename.endswith((".css", ".js")): if filename.endswith((".css", ".js")):
response.headers["Cache-Control"] = ( response.headers["Cache-Control"] = "public, max-age=3600, immutable"
"public, max-age=3600, immutable"
)
response.headers.pop("Content-Disposition", None) response.headers.pop("Content-Disposition", None)
response.headers.pop("Vary", None)
elif response.mimetype == "text/html": elif response.mimetype == "text/html":
response.headers["Cache-Control"] = "private, no-store" response.headers["Cache-Control"] = "private, no-store"
@@ -76,8 +111,8 @@ def create_app(test_config=None):
def load_user_from_request(req): def load_user_from_request(req):
header = req.headers.get("Authorization", "") header = req.headers.get("Authorization", "")
scheme, _, token = header.partition(" ") scheme, _, token = header.partition(" ")
if scheme.lower() != "bearer" or not token.strip(): if scheme.lower() != "bearer" or not token.strip(): return None
return None
from .api_tokens import load_access_token from .api_tokens import load_access_token
return load_access_token(token.strip()) return load_access_token(token.strip())
@@ -85,6 +120,7 @@ def create_app(test_config=None):
def unauthorized(): def unauthorized():
if request.path.startswith("/api/"): if request.path.startswith("/api/"):
return {"ok": False, "message": "Wymagane uwierzytelnienie Bearer lub aktywna sesja"}, 401 return {"ok": False, "message": "Wymagane uwierzytelnienie Bearer lub aktywna sesja"}, 401
from flask import redirect from flask import redirect
return redirect(url_for("auth.login", next=request.full_path.rstrip("?"))) return redirect(url_for("auth.login", next=request.full_path.rstrip("?")))
@@ -93,33 +129,44 @@ def create_app(test_config=None):
themes = _themes(app) themes = _themes(app)
record = db.session.get(AppSetting, "theme") record = db.session.get(AppSetting, "theme")
theme_name = record.value if record and record.value in themes else "bootstrap" 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"], return {
"choices_js": app.config["CHOICES_JS_URL"], "chart_js": app.config["CHART_JS_URL"]}} "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") @app.get("/health")
def health(): return {"status": "ok"} def health(): return {"status": "ok"}
@app.get("/favicon.ico") @app.get("/favicon.ico")
def favicon(): return "", 204 def favicon(): return app.response_class(status=204)
@app.cli.command("reset-admin-password") @app.cli.command("reset-admin-password")
@click.option("--email", required=True, help="Adres e-mail konta administratora.") @click.option("--email", required=True, help="Adres e-mail konta administratora.")
@click.option("--password", prompt=True, hide_input=True, confirmation_prompt=True, @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.") help="Nowe hasło. Bez tej opcji polecenie poprosi o hasło bez wyświetlania go.")
def reset_admin_password(email, password): def reset_admin_password(email, password):
"""Resetuje hasło istniejącego administratora."""
normalized_email = email.strip().lower() normalized_email = email.strip().lower()
user = User.query.filter_by(email=normalized_email).first() user = User.query.filter_by(email=normalized_email).first()
if not user: if not user:
raise click.ClickException("Nie znaleziono użytkownika o podanym adresie e-mail.") raise click.ClickException("Nie znaleziono użytkownika o podanym adresie e-mail.")
if user.role != "admin": if user.role != "admin":
raise click.ClickException("Wskazany użytkownik nie ma roli administratora.") raise click.ClickException("Wskazany użytkownik nie ma roli administratora.")
if len(password) < 8: if len(password) < 8:
raise click.ClickException("Hasło musi mieć co najmniej 8 znaków.") raise click.ClickException("Hasło musi mieć co najmniej 8 znaków.")
user.set_password(password) user.set_password(password)
user.active = True user.active = True
db.session.commit() db.session.commit()
click.echo(f"Zresetowano hasło administratora: {normalized_email}") click.echo(f"Zresetowano hasło administratora: {normalized_email}")
app.wsgi_app = RemoveVaryCookieMiddleware(app.wsgi_app)
return app return app