Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions api/privacy_demo.py
Original file line number Diff line number Diff line change
@@ -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)
})
47 changes: 36 additions & 11 deletions api/profile.py
Original file line number Diff line number Diff line change
@@ -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 #

Expand All @@ -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'])
Expand Down
4 changes: 4 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down
Binary file modified instance/goals.db
Binary file not shown.
Binary file added my_database.db
Binary file not shown.
44 changes: 44 additions & 0 deletions utils/privacy.py
Original file line number Diff line number Diff line change
@@ -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
Loading