S SmartDocs
系列: Ricky python 154 行 · 更新於 2026-04-19

cat_register.py

Ricky/cat_project/backend/app/routes/cat_register.py

import base64
import os
import re
import uuid
from typing import Optional
from flask import Blueprint, request, jsonify, current_app
from werkzeug.security import generate_password_hash

from ..services.storage import Storage
from ..services.s3_storage import s3_enabled, upload_bytes, ext_to_content_type
from ..utils.auth import issue_jwt
from ..utils.media_url import resolve_stored_media_url


bp = Blueprint("cat_register", __name__, url_prefix="/api")

_EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")


def _decode_data_url(data_url: str) -> Optional[tuple[bytes, str]]:
    if not data_url or not data_url.startswith("data:"):
        return None
    try:
        header, b64 = data_url.split(",", 1)
        ext = "bin"
        if ";" in header:
            mime = header[5:].split(";", 1)[0]
            if "/" in mime:
                ext = mime.split("/", 1)[1].strip() or "bin"
        raw = base64.b64decode(b64)
        return raw, ext
    except Exception:
        return None


def _persist_cat_photo(data_url: str, upload_dir: str) -> Optional[str]:
    """
    Store questionnaire photo: S3 key upload/cat_{uuid}.{ext} when AWS_S3_BUCKET is set,
    else local /uploads/cat_{uuid}.{ext}.
    Returns DB value: s3://bucket/... or /uploads/... path.
    """
    decoded = _decode_data_url(data_url)
    if not decoded:
        return None
    raw, ext = decoded
    if s3_enabled():
        info = upload_bytes(
            prefix="upload",
            ext=ext,
            body=raw,
            content_type=ext_to_content_type(ext),
        )
        return info["s3_uri"]
    os.makedirs(upload_dir, exist_ok=True)
    filename = f"cat_{uuid.uuid4().hex}.{ext}"
    path = os.path.join(upload_dir, filename)
    with open(path, "wb") as f:
        f.write(raw)
    return f"/uploads/{filename}"




@bp.route("/cat-questionnaire", methods=["OPTIONS"])
def cat_questionnaire_options():
    return jsonify({"ok": True}), 200
@bp.post("/cat-questionnaire")
def register_with_cat():
    """Register user (email+password) and upsert the single cat profile."""
    data = request.get_json(silent=True) or {}

    email = (data.get("email") or "").strip().lower()
    password = data.get("password") or ""

    if not email or not password:
        return jsonify({"status": "error", "message": "email and password are required"}), 400
    if not _EMAIL_RE.match(email):
        return jsonify({"status": "error", "message": "invalid email"}), 400
    if len(password) < 8:
        return jsonify({"status": "error", "message": "password must be at least 8 characters"}), 400

    # Cat fields
    name = (data.get("cat_name") or "").strip()
    breed = (data.get("breed") or data.get("cat_breed") or "").strip()
    age = data.get("age")
    gender = (data.get("gender") or "").strip()
    health = (data.get("health") or "").strip()
    vaccinations = data.get("vaccines") or data.get("vaccinations") or []
    photo_data_url = data.get("photo_data_url") or ""

    if not name or not breed:
        return jsonify({"status": "error", "message": "cat_name and breed are required"}), 400
    try:
        age_int = int(age)
        if age_int < 0:
            raise ValueError
    except Exception:
        return jsonify({"status": "error", "message": "invalid age"}), 400
    if not gender:
        return jsonify({"status": "error", "message": "gender is required"}), 400
    if not health:
        return jsonify({"status": "error", "message": "health is required"}), 400

    if not isinstance(vaccinations, list):
        vaccinations = []

    required_v = {"FVRCP", "Rabies", "FeLV", "FIP"}
    if not required_v.issubset(set(vaccinations)):
        return jsonify({"status": "error", "message": "all four core vaccines are required"}), 400

    store = Storage(current_app.config["SQLITE_PATH"])
    if store.get_user_by_email(email):
        return jsonify({"status": "error", "message": "user already exists"}), 409

    password_hash = generate_password_hash(password, method='pbkdf2:sha256')
    store.create_user(email, password_hash)
    user = store.get_user_by_email(email)
    if not user:
        return jsonify({"status": "error", "message": "failed to create user"}), 500

    upload_dir = current_app.config.get("UPLOAD_DIR", "./uploads")
    photo_path = _persist_cat_photo(photo_data_url, upload_dir)

    profile = dict(data)
    cat_row = store.upsert_cat_for_user(
        user_id=user["id"],
        name=name,
        breed=breed,
        age=age_int,
        gender=gender,
        health=health,
        vaccinations=vaccinations,
        profile=profile,
        photo_path=photo_path,
    )
    cat_id = (cat_row or {}).get("id")
    photo_for_client = resolve_stored_media_url(current_app.config, photo_path)

    token = issue_jwt(user_id=user["id"], email=user["email"])
    return jsonify({
        "status": "ok",
        "token": token,
        "user": {"id": user["id"], "email": user["email"]},
        "cat": {
            "id": cat_id,
            "name": name,
            "breed": breed,
            "age": age_int,
            "gender": gender,
            "health": health,
            "vaccinations": vaccinations,
            "photo_path": photo_for_client,
        },
    }), 201

相關文章