first commit

This commit is contained in:
2026-02-12 22:11:52 -06:00
commit 25d05e0e86
37 changed files with 4492 additions and 0 deletions

Binary file not shown.

139
api/main.py Normal file
View File

@@ -0,0 +1,139 @@
"""
main.py - Flask API with auth routes and module registry
Domain routes are registered via the routes registry.
"""
import os
import flask
import core.auth as auth
import core.users as users
import core.postgres as postgres
import api.routes.routines as routines_routes
import api.routes.medications as medications_routes
app = flask.Flask(__name__)
ROUTE_MODULES = [routines_routes, medications_routes]
def register_routes(module):
"""Register a routes module. Module should have a register(app) function."""
ROUTE_MODULES.append(module)
# ── Auth Routes ────────────────────────────────────────────────────
@app.route("/api/register", methods=["POST"])
def api_register():
data = flask.request.get_json()
username = data.get("username")
password = data.get("password")
if not username or not password:
return flask.jsonify({"error": "username and password required"}), 400
result = users.registerUser(username, password, data)
if result:
return flask.jsonify({"success": True}), 201
else:
return flask.jsonify({"error": "username taken"}), 409
@app.route("/api/login", methods=["POST"])
def api_login():
data = flask.request.get_json()
username = data.get("username")
password = data.get("password")
if not username or not password:
return flask.jsonify({"error": "username and password required"}), 400
token = auth.getLoginToken(username, password)
if token:
return flask.jsonify({"token": token}), 200
else:
return flask.jsonify({"error": "invalid credentials"}), 401
# ── User Routes ────────────────────────────────────────────────────
@app.route("/api/getUserUUID/<username>", methods=["GET"])
def api_getUserUUID(username):
header = flask.request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return flask.jsonify({"error": "missing token"}), 401
token = header[7:]
if auth.verifyLoginToken(token, username):
return flask.jsonify(users.getUserUUID(username)), 200
else:
return flask.jsonify({"error": "unauthorized"}), 401
@app.route("/api/user/<userUUID>", methods=["GET"])
def api_getUser(userUUID):
header = flask.request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return flask.jsonify({"error": "missing token"}), 401
token = header[7:]
if auth.verifyLoginToken(token, userUUID=userUUID):
user = postgres.select_one("users", {"id": userUUID})
if user:
user.pop("password_hashed", None)
return flask.jsonify(user), 200
else:
return flask.jsonify({"error": "user not found"}), 404
else:
return flask.jsonify({"error": "unauthorized"}), 401
@app.route("/api/user/<userUUID>", methods=["PUT"])
def api_updateUser(userUUID):
header = flask.request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return flask.jsonify({"error": "missing token"}), 401
token = header[7:]
if auth.verifyLoginToken(token, userUUID=userUUID):
data = flask.request.get_json()
result = users.updateUser(userUUID, data)
if result:
return flask.jsonify({"success": True}), 200
else:
return flask.jsonify({"error": "no valid fields to update"}), 400
else:
return flask.jsonify({"error": "unauthorized"}), 401
@app.route("/api/user/<userUUID>", methods=["DELETE"])
def api_deleteUser(userUUID):
header = flask.request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return flask.jsonify({"error": "missing token"}), 401
token = header[7:]
if auth.verifyLoginToken(token, userUUID=userUUID):
data = flask.request.get_json()
password = data.get("password")
if not password:
return flask.jsonify(
{"error": "password required for account deletion"}
), 400
result = auth.unregisterUser(userUUID, password)
if result:
return flask.jsonify({"success": True}), 200
else:
return flask.jsonify({"error": "invalid password"}), 401
else:
return flask.jsonify({"error": "unauthorized"}), 401
# ── Health Check ───────────────────────────────────────────────────
@app.route("/health", methods=["GET"])
def health_check():
return flask.jsonify({"status": "ok"}), 200
if __name__ == "__main__":
for module in ROUTE_MODULES:
if hasattr(module, "register"):
module.register(app)
app.run(host="0.0.0.0", port=5000)

Binary file not shown.

56
api/routes/example.py Normal file
View File

@@ -0,0 +1,56 @@
"""
Example route module - Copy this pattern for your domain.
This module demonstrates:
1. Registering routes with Flask app
2. Using auth validation
3. Making database calls via postgres module
"""
import os
import flask
import jwt
import core.auth as auth
import core.postgres as postgres
def _get_user_uuid(token):
"""Decode JWT to extract user UUID. Returns None on failure."""
try:
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def register(app):
"""Register routes with the Flask app."""
@app.route("/api/example", methods=["GET"])
def api_listExamples():
header = flask.request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return flask.jsonify({"error": "missing token"}), 401
token = header[7:]
user_uuid = _get_user_uuid(token)
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
return flask.jsonify({"error": "unauthorized"}), 401
items = postgres.select("examples")
return flask.jsonify(items), 200
@app.route("/api/example", methods=["POST"])
def api_addExample():
header = flask.request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return flask.jsonify({"error": "missing token"}), 401
token = header[7:]
user_uuid = _get_user_uuid(token)
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
item = postgres.insert("examples", data)
return flask.jsonify(item), 201

160
api/routes/medications.py Normal file
View File

@@ -0,0 +1,160 @@
"""
Medications API - medication scheduling, logging, and adherence tracking
"""
import os
import flask
import jwt
import core.auth as auth
import core.postgres as postgres
def _get_user_uuid(token):
try:
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def _auth(request):
"""Extract and verify token. Returns user_uuid or None."""
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return None
token = header[7:]
user_uuid = _get_user_uuid(token)
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
return None
return user_uuid
def register(app):
# ── Medications CRUD ──────────────────────────────────────────
@app.route("/api/medications", methods=["GET"])
def api_listMedications():
"""List all medications for the logged-in user."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/medications", methods=["POST"])
def api_addMedication():
"""Add a medication. Body: {name, dosage, unit, frequency, times: ["08:00","20:00"], notes?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/medications/<med_id>", methods=["GET"])
def api_getMedication(med_id):
"""Get a single medication with its schedule."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/medications/<med_id>", methods=["PUT"])
def api_updateMedication(med_id):
"""Update medication details. Body: {name?, dosage?, unit?, frequency?, times?, notes?, active?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/medications/<med_id>", methods=["DELETE"])
def api_deleteMedication(med_id):
"""Delete a medication and its logs."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
# ── Medication Logging (take / skip / snooze) ─────────────────
@app.route("/api/medications/<med_id>/take", methods=["POST"])
def api_takeMedication(med_id):
"""Log that a dose was taken. Body: {scheduled_time?, notes?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json() or {}
pass
@app.route("/api/medications/<med_id>/skip", methods=["POST"])
def api_skipMedication(med_id):
"""Log a skipped dose. Body: {scheduled_time?, reason?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json() or {}
pass
@app.route("/api/medications/<med_id>/snooze", methods=["POST"])
def api_snoozeMedication(med_id):
"""Snooze a reminder. Body: {minutes: 15}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
# ── Medication Log / History ──────────────────────────────────
@app.route("/api/medications/<med_id>/log", methods=["GET"])
def api_getMedLog(med_id):
"""Get dose log for a medication. Query: ?days=30"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/medications/today", methods=["GET"])
def api_todaysMeds():
"""Get today's medication schedule with taken/pending status."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
# ── Adherence Stats ───────────────────────────────────────────
@app.route("/api/medications/adherence", methods=["GET"])
def api_adherenceStats():
"""Get adherence stats across all meds. Query: ?days=30"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/medications/<med_id>/adherence", methods=["GET"])
def api_medAdherence(med_id):
"""Get adherence stats for a single medication. Query: ?days=30"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
# ── Refills ───────────────────────────────────────────────────
@app.route("/api/medications/<med_id>/refill", methods=["PUT"])
def api_setRefill(med_id):
"""Set refill info. Body: {quantity_remaining, refill_date?, pharmacy_notes?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/medications/refills-due", methods=["GET"])
def api_refillsDue():
"""Get medications that need refills soon. Query: ?days_ahead=7"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass

204
api/routes/routines.py Normal file
View File

@@ -0,0 +1,204 @@
"""
Routines API - Brilli-style routine management
Routines have ordered steps. Users start sessions to walk through them.
"""
import os
import flask
import jwt
import core.auth as auth
import core.postgres as postgres
def _get_user_uuid(token):
try:
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def _auth(request):
"""Extract and verify token. Returns user_uuid or None."""
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return None
token = header[7:]
user_uuid = _get_user_uuid(token)
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
return None
return user_uuid
def register(app):
# ── Routines CRUD ─────────────────────────────────────────────
@app.route("/api/routines", methods=["GET"])
def api_listRoutines():
"""List all routines for the logged-in user."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/routines", methods=["POST"])
def api_createRoutine():
"""Create a new routine. Body: {name, description?, icon?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/routines/<routine_id>", methods=["GET"])
def api_getRoutine(routine_id):
"""Get a routine with its steps."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/routines/<routine_id>", methods=["PUT"])
def api_updateRoutine(routine_id):
"""Update routine details. Body: {name?, description?, icon?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/routines/<routine_id>", methods=["DELETE"])
def api_deleteRoutine(routine_id):
"""Delete a routine and all its steps/sessions."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
# ── Steps CRUD ────────────────────────────────────────────────
@app.route("/api/routines/<routine_id>/steps", methods=["GET"])
def api_listSteps(routine_id):
"""List steps for a routine, ordered by position."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/routines/<routine_id>/steps", methods=["POST"])
def api_addStep(routine_id):
"""Add a step to a routine. Body: {name, duration_minutes?, position?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/routines/<routine_id>/steps/<step_id>", methods=["PUT"])
def api_updateStep(routine_id, step_id):
"""Update a step. Body: {name?, duration_minutes?, position?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/routines/<routine_id>/steps/<step_id>", methods=["DELETE"])
def api_deleteStep(routine_id, step_id):
"""Delete a step from a routine."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/routines/<routine_id>/steps/reorder", methods=["PUT"])
def api_reorderSteps(routine_id):
"""Reorder steps. Body: {step_ids: [ordered list of step UUIDs]}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
# ── Routine Sessions (active run-through) ─────────────────────
@app.route("/api/routines/<routine_id>/start", methods=["POST"])
def api_startRoutine(routine_id):
"""Start a routine session. Returns the session with first step."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/sessions/active", methods=["GET"])
def api_getActiveSession():
"""Get the user's currently active routine session, if any."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/sessions/<session_id>/complete-step", methods=["POST"])
def api_completeStep(session_id):
"""Mark current step done, advance to next. Body: {step_id}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/sessions/<session_id>/skip-step", methods=["POST"])
def api_skipStep(session_id):
"""Skip current step, advance to next. Body: {step_id}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/sessions/<session_id>/cancel", methods=["POST"])
def api_cancelSession(session_id):
"""Cancel an active routine session."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
# ── Routine History / Stats ───────────────────────────────────
@app.route("/api/routines/<routine_id>/history", methods=["GET"])
def api_routineHistory(routine_id):
"""Get past sessions for a routine. Query: ?days=7"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
# ── Routine Scheduling ────────────────────────────────────────
@app.route("/api/routines/<routine_id>/schedule", methods=["PUT"])
def api_setRoutineSchedule(routine_id):
"""Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
pass
@app.route("/api/routines/<routine_id>/schedule", methods=["GET"])
def api_getRoutineSchedule(routine_id):
"""Get the schedule for a routine."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass
@app.route("/api/routines/<routine_id>/schedule", methods=["DELETE"])
def api_deleteRoutineSchedule(routine_id):
"""Remove the schedule from a routine."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
pass