Phase 9: scaffold remote server (Flask + Postgres, docker-compose for unraid)

- server/sql/init.sql: devices/tracking_points/dashcam_files schema
- server/modules/db.py: psycopg3 pool + query helpers
- server/modules/auth.py: X-Device-Name/X-Api-Key header auth
- server/app.py: /health, /api/v1/tracking/batch, /api/v1/dashcam/upload
- server/Dockerfile + docker-compose.yml: api (gunicorn) + postgres services
- server/scripts/create_device.py: one-time device/API-key registration
- server/README.md: unraid deployment + Cloudflare Tunnel exposure docs
- PLAN.md: Phase 9 marked in progress with full build notes

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-08 19:50:52 -04:00
co-authored by Copilot
parent 261ef3f4db
commit 9bdf5bad2c
12 changed files with 582 additions and 1 deletions
+40
View File
@@ -0,0 +1,40 @@
"""API-key authentication for device uploads.
Devices authenticate with two headers:
X-Device-Name: <device name row in the devices table, e.g. "yaris-2013">
X-Api-Key: <raw API key, only ever compared as a sha256 hash>
The raw key is never stored - only its sha256 hex digest is kept in the
devices.api_key_hash column, generated once via scripts/create_device.py.
"""
import hashlib
from functools import wraps
from flask import request, jsonify, g
from modules import db
def _hash_key(raw_key):
return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
def require_device_auth(view):
@wraps(view)
def wrapped(*args, **kwargs):
device_name = request.headers.get("X-Device-Name", "")
api_key = request.headers.get("X-Api-Key", "")
if not device_name or not api_key:
return jsonify(error="missing X-Device-Name or X-Api-Key header"), 401
device = db.get_device_by_name(device_name)
if device is None or device["api_key_hash"] != _hash_key(api_key):
return jsonify(error="invalid device name or api key"), 401
g.device_id = device["id"]
g.device_name = device["name"]
db.touch_device_last_seen(device["id"])
return view(*args, **kwargs)
return wrapped