Files
Synculous-2/api/routes/routine_templates.py

169 lines
7.1 KiB
Python

"""
Routine Templates API - pre-built routines that users can clone
"""
import os
import uuid
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):
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):
@app.route("/api/templates", methods=["GET"])
def api_listTemplates():
"""List all available templates."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
templates = postgres.select("routine_templates", order_by="name")
for template in templates:
steps = postgres.select("routine_template_steps", {"template_id": template["id"]}, order_by="position")
template["step_count"] = len(steps)
return flask.jsonify(templates), 200
@app.route("/api/templates", methods=["POST"])
def api_createTemplate():
"""Create a new template (admin only in production). Body: {name, description?, icon?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
if not data.get("name"):
return flask.jsonify({"error": "missing required field: name"}), 400
data["id"] = str(uuid.uuid4())
data["created_by_admin"] = False
template = postgres.insert("routine_templates", data)
return flask.jsonify(template), 201
@app.route("/api/templates/<template_id>", methods=["GET"])
def api_getTemplate(template_id):
"""Get a template with its steps."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
steps = postgres.select("routine_template_steps", {"template_id": template_id}, order_by="position")
return flask.jsonify({"template": template, "steps": steps}), 200
@app.route("/api/templates/<template_id>/clone", methods=["POST"])
def api_cloneTemplate(template_id):
"""Clone a template to user's routines."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
template_steps = postgres.select("routine_template_steps", {"template_id": template_id}, order_by="position")
new_routine = {
"id": str(uuid.uuid4()),
"user_uuid": user_uuid,
"name": template["name"],
"description": template.get("description"),
"icon": template.get("icon"),
}
routine = postgres.insert("routines", new_routine)
for step in template_steps:
new_step = {
"id": str(uuid.uuid4()),
"routine_id": routine["id"],
"name": step["name"],
"instructions": step.get("instructions"),
"step_type": step.get("step_type", "generic"),
"duration_minutes": step.get("duration_minutes"),
"media_url": step.get("media_url"),
"position": step["position"],
}
postgres.insert("routine_steps", new_step)
return flask.jsonify(routine), 201
@app.route("/api/templates/<template_id>", methods=["PUT"])
def api_updateTemplate(template_id):
"""Update a template."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
allowed = ["name", "description", "icon"]
updates = {k: v for k, v in data.items() if k in allowed}
if not updates:
return flask.jsonify({"error": "no valid fields to update"}), 400
result = postgres.update("routine_templates", updates, {"id": template_id})
return flask.jsonify(result[0] if result else {}), 200
@app.route("/api/templates/<template_id>", methods=["DELETE"])
def api_deleteTemplate(template_id):
"""Delete a template."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
postgres.delete("routine_template_steps", {"template_id": template_id})
postgres.delete("routine_templates", {"id": template_id})
return flask.jsonify({"deleted": True}), 200
@app.route("/api/templates/<template_id>/steps", methods=["POST"])
def api_addTemplateStep(template_id):
"""Add a step to a template. Body: {name, instructions?, step_type?, duration_minutes?, position?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
data = flask.request.get_json()
if not data or not data.get("name"):
return flask.jsonify({"error": "missing required field: name"}), 400
max_pos = postgres.select(
"routine_template_steps",
{"template_id": template_id},
order_by="position DESC",
limit=1,
)
next_pos = (max_pos[0]["position"] + 1) if max_pos else 1
step = {
"id": str(uuid.uuid4()),
"template_id": template_id,
"name": data["name"],
"instructions": data.get("instructions"),
"step_type": data.get("step_type", "generic"),
"duration_minutes": data.get("duration_minutes"),
"media_url": data.get("media_url"),
"position": data.get("position", next_pos),
}
result = postgres.insert("routine_template_steps", step)
return flask.jsonify(result), 201