"""Garmin routes: trigger a sync and read sync status.""" from flask import Blueprint, request, g, jsonify from auth import require_auth from db import query_one from services import garmin as garmin_svc from services import garmin_auth bp = Blueprint("garmin", __name__) @bp.route("/sync", methods=["POST"]) @require_auth def sync(): data = request.get_json(silent=True) or {} creds = { "garminEmail": (data.get("garminEmail") or "").strip(), "garminPassword": data.get("garminPassword") or "", } # Fall back to the stored Garmin email when only a password is supplied. if not creds["garminEmail"]: user = query_one("SELECT garmin_email FROM users WHERE id = ?", [g.user_id]) if user and user.get("garmin_email"): creds["garminEmail"] = user["garmin_email"] # With stored OAuth tokens no password is needed at all. Without them the # plaintext password must come in the body, because only a hash is kept. if not creds["garminPassword"] and not garmin_svc.has_token(g.user_id): return ( jsonify({ "status": "error", "recordsSynced": 0, "message": "需要 Garmin 密码以执行同步,请在请求体中提供 garminPassword" "(密码仅作哈希存储,无法还原)。", }), 400, ) result = garmin_svc.sync_data(g.user_id, creds) return jsonify(result) @bp.route("/auth-status", methods=["GET"]) @require_auth def auth_status(): """Whether a stored token exists, so the UI knows to ask for a password.""" return jsonify({"hasToken": garmin_svc.has_token(g.user_id)}) @bp.route("/login", methods=["POST"]) @require_auth def login(): """Begin an interactive Garmin login. Returns immediately with a session id; the login continues in the background and parks if Garmin asks for a two-factor code. Poll /login-status and post the code to /mfa. """ data = request.get_json(silent=True) or {} password = data.get("garminPassword") or "" if not password: return jsonify({"error": "请提供 Garmin 密码"}), 400 garmin_email = (data.get("garminEmail") or "").strip() if not garmin_email: user = query_one("SELECT garmin_email FROM users WHERE id = ?", [g.user_id]) garmin_email = (user or {}).get("garmin_email") or "" if not garmin_email: return jsonify({"error": "缺少 Garmin 邮箱"}), 400 session_id = garmin_auth.start_login(g.user_id, garmin_email, password) return jsonify({"session": session_id, "status": "starting"}), 202 @bp.route("/login-status", methods=["GET"]) @require_auth def login_status(): session_id = request.args.get("session") or "" row = garmin_auth.get_session(session_id, g.user_id) if not row: return jsonify({"error": "登录会话不存在或已过期"}), 404 return jsonify({ "session": row["id"], "status": row["status"], "error": row["error"], }) @bp.route("/mfa", methods=["POST"]) @require_auth def submit_mfa(): data = request.get_json(silent=True) or {} session_id = (data.get("session") or "").strip() code = (data.get("code") or "").strip() if not session_id or not code: return jsonify({"error": "session 与 code 均为必填"}), 400 ok, message = garmin_auth.submit_code(session_id, g.user_id, code) return jsonify({"ok": ok, "message": message}), (200 if ok else 400) @bp.route("/login", methods=["DELETE"]) @require_auth def cancel_login(): session_id = request.args.get("session") or "" garmin_auth.cancel(session_id, g.user_id) return jsonify({"ok": True}) @bp.route("/status", methods=["GET"]) @require_auth def status(): return jsonify(garmin_svc.get_sync_status(g.user_id))