- 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]>
41 lines
1.3 KiB
Python
41 lines
1.3 KiB
Python
"""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
|