diff --git a/api/privacy_demo.py b/api/privacy_demo.py new file mode 100644 index 0000000..6b54606 --- /dev/null +++ b/api/privacy_demo.py @@ -0,0 +1,26 @@ +from flask import Blueprint, request, jsonify +from utils.privacy import ( + stable_hash, initials, pseudonym, + birth_year_from_iso, age_bucket_from_year +) + +# Create a Blueprint for privacy demo that can be plugged into the main Flask app +bp = Blueprint('privacy_demo', __name__, url_prefix = "/debug") + +@bp.post("/anonymize") + +def anonymize_demo(): + data = request.get_json(silent = True) or {} + email = data.get("email") + name = data.get("name") + dob = data.get("dob") + + by = birth_year_from_iso(dob) + + return jsonify({ + "email_hash": stable_hash(email) if email else None, + "name_initials": initials(name), + "pseudonymId": pseudonym("http://redback.fit/user", email or name or "temp"), + "birth_year": by, + "age_bucket": age_bucket_from_year(by) + }) diff --git a/api/profile.py b/api/profile.py index 0496bc3..1d38516 100644 --- a/api/profile.py +++ b/api/profile.py @@ -1,10 +1,39 @@ # /api/profile.py from flask import Blueprint, jsonify, request from models.user import db, UserProfile -from flask_cors import CORS + +from utils.privacy import( + stable_hash, initials, pseudonym, + birth_year_from_iso, age_bucket_from_year +) api = Blueprint('profile_api', __name__) +def anonymize_user_record(u): + """Build an anonymized profile payload from a UserProfile row. + Hides raw PII (name/email/DOB) and exposes safe equivalents.""" + + if not u: + return None + + email_value = getattr(u, "email", None) or getattr(u, "account", None) + raw_key = email_value or getattr(u, "name", None) or str(getattr(u, "id", "")) + + # Your model uses 'birthDate' (ISO string) rather than 'dob' + by = birth_year_from_iso(getattr(u, "birthDate", None)) + + return { + "id": getattr(u, "id", None), # safe to keep if your API expects it + "pseudoId": pseudonym("https://redback.fit/user", raw_key), + "nameInitials": initials(getattr(u, "name", None)), + "emailHash": stable_hash(email_value), + "ageBucket": age_bucket_from_year(by), + + # keep non-PII fields you were already returning + "account": getattr(u, "account", None), + "gender": getattr(u, "gender", None), + "avatar": getattr(u, "avatar", None), + } # Profile Endpoints # @@ -13,19 +42,15 @@ @api.route('', methods=['GET']) def get_profile(): - # In future develpment get the user_id from a session or token + # In future development get the user_id from a session or token user_id = 1 # Replace with authenticated user ID user = UserProfile.query.filter_by(id=user_id).first() - if user: - return jsonify({ - 'name': user.name, - 'account': user.account, - 'birthDate': user.birthDate, - 'gender': user.gender, - 'avatar': user.avatar - }) - return jsonify({'message': 'User not found'}), 404 + if not user: + return jsonify({'message': 'User not found'}), 404 + + + return jsonify(anonymize_user_record(user)), 200 @api.route('', methods=['POST']) diff --git a/app.py b/app.py index f9f3767..e667de9 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,8 @@ from api.dashboard import dashboard_bp from models import db from dotenv import load_dotenv +from api.privacy_demo import bp as privacy_demo_bp + import os import pyrebase @@ -52,6 +54,8 @@ app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard') app.register_blueprint(profile_api, url_prefix='/api/profile') +app.register_blueprint(privacy_demo_bp) + # Main index route (login + welcome) @app.route('/', methods=['GET', 'POST']) def index(): diff --git a/instance/goals.db b/instance/goals.db index bdb45dd..82304af 100644 Binary files a/instance/goals.db and b/instance/goals.db differ diff --git a/my_database.db b/my_database.db new file mode 100644 index 0000000..dd68529 Binary files /dev/null and b/my_database.db differ diff --git a/utils/privacy.py b/utils/privacy.py new file mode 100644 index 0000000..7c52a01 --- /dev/null +++ b/utils/privacy.py @@ -0,0 +1,44 @@ +import hashlib, os, re, uuid +from datetime import date + +# Optional salts for extra safety; we can add to a .env later +PII_SALT = os.getenv("PII_SALT", "") +PII_PEPPER = os.getenv("PII_PEPPER", "") + +def stable_hash(value: str | None) -> str | None: + """Deterministic, non-reversible hash for identifiers like email.""" + if not value: + return None + data = (PII_SALT + value + PII_PEPPER).encode("utf-8") + return hashlib.sha256(data).hexdigest() + +def initials(full_name: str | None) -> str | None: + """J D for 'Jane Doe'. Up to 3 initials, letters only.""" + if not full_name: + return None + parts = re.findall(r"[A-Za-z]+", full_name) + return "".join(p[0].upper() for p in parts[:3]) or None + +def pseudonym(namespace: str, raw_key: str) -> str: + """Stable pseudonym like user_7f3ab2 derived from a namespace + raw key.""" + ns = uuid.uuid5(uuid.NAMESPACE_URL, namespace) + return "user_" + uuid.uuid5(ns, raw_key).hex[:8] + +def birth_year_from_iso(dob_iso: str | None) -> int | None: + """YYYY[-MM[-DD]] -> YYYY""" + try: + return int(dob_iso[:4]) if dob_iso else None + except Exception: + return None + +def age_bucket_from_year(year: int | None) -> str | None: + """Return coarse age bucket for privacy (e.g., 18-24, 25-29, 30-34, ..., 50+).""" + if not year: + return None + today_year = date.today().year + age = max(0, today_year - year) + bins = [(0,17),(18,24),(25,29),(30,34),(35,39),(40,44),(45,49),(50,120)] + for lo, hi in bins: + if lo <= age <= hi: + return f"{lo:02d}-{hi:02d}" if hi < 120 else f"{lo:02d}+" + return None