Phase 24: remote trip + dashcam portal
- Add Flask-Login browser session auth (separate from device X-Api-Key auth)
- modules/user_auth.py: User model, require_login decorator, bcrypt helpers
- /login with CSRF token, /logout
- scripts/create_user.py to provision admin user
- Add server-side trip computation from tracking_points
- db.compute_trips_for_device(): gap > 5 min = new trip, haversine distance,
upserts into trips table, auto-triggered in bg thread after each upload batch
- db.list_trips(), db.get_trip_route(): same JSON shape as Pi API
- db.get_overview_stats(): aggregated totals for overview page
- db.list_dashcam_files(), db.get_dashcam_file(): browse/stream helpers
- Add web portal to server/app.py
- Overview page (/): stat cards + recent trips + recent clips
- Trip history (/history): reuses Pi history.js + data-list.js unchanged
via /api/trips + /api/trip/<id> compatibility aliases
- Dashcam archive (/dashcam): file list + inline video player
- /api/v1/portal/dashcam/stream/<id>: Range-request streaming, path traversal guard
- /api/geocode: same Nominatim→reverse_geocoder offline fallback as Pi
- Add server templates (server/templates/)
- base.html: shared shell with 'Remote server' badge in sidebar + mobile topbar
- login.html, index.html, history.html, dashcam.html, components.html
- DB schema: users + trips tables added to init.sql (fresh deploys) +
migrate_phase24.sql (existing deployments)
- Infrastructure: Dockerfile uses repo-root build context to copy shared
static/ assets; docker-compose updated; SECRET_KEY + DEVICE_NAME in .env;
requirements: Flask-Login, bcrypt, reverse_geocoder
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"""Browser session authentication using Flask-Login + bcrypt.
|
||||
|
||||
A single admin user is created via scripts/create_user.py.
|
||||
Device upload auth (X-Api-Key) is separate and unchanged.
|
||||
"""
|
||||
import bcrypt
|
||||
from functools import wraps
|
||||
|
||||
from flask import redirect, url_for
|
||||
from flask_login import LoginManager, UserMixin, current_user
|
||||
|
||||
from modules import db
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = "login_page"
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, id, username):
|
||||
self.id = id
|
||||
self.username = username
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
row = db.get_user_by_id(int(user_id))
|
||||
if row is None:
|
||||
return None
|
||||
return User(row["id"], row["username"])
|
||||
|
||||
|
||||
def check_password(plain, hashed):
|
||||
return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
|
||||
|
||||
|
||||
def hash_password(plain):
|
||||
return bcrypt.hashpw(plain.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
|
||||
def require_login(view):
|
||||
"""Decorator: redirect to login page if not authenticated."""
|
||||
@wraps(view)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for("login_page"))
|
||||
return view(*args, **kwargs)
|
||||
return wrapped
|
||||
Reference in New Issue
Block a user