This commit is contained in:
2026-10-03 20:24:37 +02:00
parent d603d8d574
commit 7094d05030
8 changed files with 581 additions and 135 deletions
+37 -18
View File
@@ -1,4 +1,5 @@
import os
from datetime import datetime, timedelta, timezone
import click
from flask import Flask, jsonify, redirect, render_template, request, url_for
@@ -34,7 +35,7 @@ def create_app():
@login_manager.unauthorized_handler
def unauthorized():
# Dla API zwróć JSON zamiast przekierowania na stronę logowania.
# Return JSON for API calls instead of redirecting to the login page.
if request.path.startswith("/api/"):
return jsonify({"error": "authentication required"}), 401
next_page = request.path
@@ -46,7 +47,7 @@ def create_app():
def _is_safe_next(target: str | None) -> bool:
return bool(target) and target.startswith("/") and not target.startswith("//")
# --- Strony ---
# --- Pages ---
@app.route("/")
def index():
@@ -69,7 +70,7 @@ def create_app():
if not _is_safe_next(next_page):
next_page = url_for("home")
return redirect(next_page)
error = "Nieprawidłowa nazwa użytkownika lub hasło."
error = "Invalid username or password."
return render_template("login.html", error=error, next=request.args.get("next", ""))
@app.route("/logout")
@@ -86,21 +87,24 @@ def create_app():
@app.route("/sensors")
@login_required
def sensors_page():
return render_template("sensors.html")
return render_template("sensors.html", user=current_user)
@app.route("/api/docs")
@login_required
def api_docs():
return render_template("docs.html")
return render_template("docs.html", user=current_user)
# --- API: odczyt (wymaga zalogowania, sesja z przeglądarki wystarcza) ---
# --- API: device endpoints (intentionally public) ---
# The sensor firmware has no browser session, but it needs the sensor
# list to resolve a name to an id before pushing readouts.
@app.route("/api/sensors")
@login_required
def api_sensors():
sensors = Sensor.query.order_by(Sensor.name).all()
return jsonify([{"id": s.id, "name": s.name} for s in sensors])
# --- API: dashboard reads (login required, the browser session cookie is enough) ---
@app.route("/api/sensors/<int:sensor_id>/types")
@login_required
def api_sensor_types(sensor_id):
@@ -132,6 +136,19 @@ def create_app():
@login_required
def api_sensor_readouts_hourly(sensor_id):
type_ = request.args.get("type")
range_ = request.args.get("range", "all")
# Timestamps are stored as naive UTC, so the cutoff must be naive UTC too.
now = datetime.now(timezone.utc).replace(tzinfo=None)
if range_ == "today":
cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
elif range_ == "week":
cutoff = now - timedelta(days=7)
elif range_ == "month":
cutoff = now - timedelta(days=30)
elif range_ == "all":
cutoff = None
else:
return jsonify({"error": "range must be one of: today, week, month, all"}), 400
query = (
db.session.query(
func.strftime("%Y-%m-%dT%H:00:00", SensorReadout.timestamp).label("hour"),
@@ -139,6 +156,8 @@ def create_app():
)
.filter(SensorReadout.sensor_id == sensor_id)
)
if cutoff is not None:
query = query.filter(SensorReadout.timestamp >= cutoff)
if type_:
query = query.filter(SensorReadout.type == type_)
rows = (
@@ -150,7 +169,7 @@ def create_app():
[{"timestamp": r.hour, "value": round(r.avg_value, 2)} for r in rows]
)
# --- API: zapis (celowo bez logowania — wywołują je urządzenia/sensory) ---
# --- API: device writes (intentionally public — called by devices/sensors) ---
@app.route("/api/sensors", methods=["POST"])
def api_create_sensor():
@@ -206,17 +225,17 @@ def create_app():
}
), 201
# --- Komenda do utworzenia pierwszego admina ---
# Użycie:
# flask --app app create-admin --username admin --password 'sekret'
# ADMIN_USERNAME=admin ADMIN_PASSWORD=sekret flask --app app create-admin
# flask --app app create-admin (zapyta interaktywnie)
# --- Command for creating the first admin ---
# Usage:
# flask --app app create-admin --username admin --password 'secret'
# ADMIN_USERNAME=admin ADMIN_PASSWORD=secret flask --app app create-admin
# flask --app app create-admin (asks interactively)
@app.cli.command("create-admin")
@click.option("--username", default=None, help="Nazwa administratora")
@click.option("--password", default=None, help="Hasło administratora")
@click.option("--username", default=None, help="Administrator username")
@click.option("--password", default=None, help="Administrator password")
def create_admin(username, password):
"""Utwórz lub zaktualizuj konto administratora (is_admin=True)."""
"""Create or update an administrator account (is_admin=True)."""
username = username or os.environ.get("ADMIN_USERNAME")
if not username:
username = click.prompt("Admin username")
@@ -235,13 +254,13 @@ def create_app():
user.set_password(password)
user.is_admin = True
db.session.commit()
click.echo(f"Zaktualizowano istniejącego użytkownika '{username}' (nadano is_admin=True).")
click.echo(f"Updated existing user '{username}' (granted is_admin=True).")
else:
user = User(username=username, is_admin=True)
user.set_password(password)
db.session.add(user)
db.session.commit()
click.echo(f"Utworzono administratora '{username}'.")
click.echo(f"Created administrator '{username}'.")
return app