import sqlite3, hashlib, secrets, time, random, json, os import urllib.request, urllib.error, urllib.parse BREVO_API_KEY = os.environ.get("BREVO_API_KEY", "") EMAIL_SENDER = os.environ.get("EMAIL_SENDER", "noreply@fitpro.app") SENDER_NAME = "FitPro AI" DB_PATH = "/tmp/fitpro.db" _conn = None def _db(): global _conn if _conn is None: _conn = sqlite3.connect(DB_PATH, check_same_thread=False) _conn.row_factory = sqlite3.Row _conn.executescript(""" CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, token TEXT, created_at REAL, last_login REAL); CREATE TABLE IF NOT EXISTS otps ( key TEXT PRIMARY KEY, otp TEXT NOT NULL, expires_at REAL NOT NULL, username TEXT, password_hash TEXT); CREATE TABLE IF NOT EXISTS profiles ( username TEXT PRIMARY KEY, name TEXT, age INTEGER, gender TEXT, height REAL, weight REAL, goal TEXT, level TEXT, equipment TEXT, updated_at REAL); CREATE TABLE IF NOT EXISTS workouts ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, plan_text TEXT, days_total INTEGER, created_at REAL); CREATE TABLE IF NOT EXISTS tracking ( username TEXT NOT NULL, date TEXT NOT NULL, day_idx INTEGER, status TEXT DEFAULT 'pending', PRIMARY KEY (username, date)); CREATE TABLE IF NOT EXISTS water_intake ( username TEXT NOT NULL, date TEXT NOT NULL, amount_ml INTEGER DEFAULT 0, goal_ml INTEGER DEFAULT 2500, PRIMARY KEY (username, date)); CREATE TABLE IF NOT EXISTS diet_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, date TEXT NOT NULL, meal_name TEXT NOT NULL, calories REAL DEFAULT 0, protein REAL DEFAULT 0, carbs REAL DEFAULT 0, fats REAL DEFAULT 0, fiber REAL DEFAULT 0, logged_at REAL); CREATE TABLE IF NOT EXISTS weight_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, date TEXT NOT NULL, weight REAL NOT NULL, note TEXT DEFAULT '', logged_at REAL); """) # Upgrade older schemas safely for col_sql in [ "ALTER TABLE otps ADD COLUMN username TEXT", "ALTER TABLE otps ADD COLUMN password_hash TEXT", ]: try: _conn.execute(col_sql); _conn.commit() except Exception: pass return _conn def _hash(pw): return hashlib.sha256(pw.encode()).hexdigest() def _token(): return secrets.token_hex(32) def _otp(): return str(random.randint(100000, 999999)) # ── User ops ────────────────────────────────────────────────────────────────── def get_user(username_or_email): v = username_or_email.strip().lower() c = _db().execute( "SELECT * FROM users WHERE lower(username)=? OR lower(email)=?", (v, v) ).fetchone() return dict(c) if c else None def get_user_by_username(username): c = _db().execute( "SELECT * FROM users WHERE lower(username)=?", (username.strip().lower(),) ).fetchone() return dict(c) if c else None def get_user_by_email(email): c = _db().execute( "SELECT * FROM users WHERE lower(email)=?", (email.strip().lower(),) ).fetchone() return dict(c) if c else None def create_user(username, email, password_hash_or_plain, already_hashed=False): token = _token() pw = password_hash_or_plain if already_hashed else _hash(password_hash_or_plain) _db().execute( "INSERT INTO users (username,email,password,token,created_at) VALUES (?,?,?,?,?)", (username.strip(), email.strip().lower(), pw, token, time.time()) ) _db().commit() return token def update_user_token(username, token): _db().execute( "UPDATE users SET token=?,last_login=? WHERE lower(username)=?", (token, time.time(), username.lower()) ) _db().commit() def logout_user(username): _db().execute( "UPDATE users SET token=NULL WHERE lower(username)=?", (username.lower(),) ) _db().commit() # ── Profile ops ─────────────────────────────────────────────────────────────── def save_profile(username, data: dict): _db().execute(""" INSERT INTO profiles (username,name,age,gender,height,weight,goal,level,equipment,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) ON CONFLICT(username) DO UPDATE SET name=excluded.name, age=excluded.age, gender=excluded.gender, height=excluded.height, weight=excluded.weight, goal=excluded.goal, level=excluded.level, equipment=excluded.equipment, updated_at=excluded.updated_at """, ( username, data.get("name",""), data.get("age",25), data.get("gender","Male"), data.get("height",170), data.get("weight",70), data.get("goal","Build Muscle"), data.get("level","Beginner"), json.dumps(data.get("equipment",[])), time.time() )) _db().commit() def load_profile(username): c = _db().execute("SELECT * FROM profiles WHERE username=?", (username,)).fetchone() if not c: return None d = dict(c) try: d["equipment"] = json.loads(d.get("equipment") or "[]") except Exception: d["equipment"] = [] return d # ── Workout ops ─────────────────────────────────────────────────────────────── def save_workout(username, plan_text, days_total): _db().execute( "INSERT INTO workouts (username,plan_text,days_total,created_at) VALUES (?,?,?,?)", (username, plan_text, days_total, time.time()) ) _db().commit() def load_latest_workout(username): c = _db().execute( "SELECT * FROM workouts WHERE username=? ORDER BY created_at DESC LIMIT 1", (username,) ).fetchone() return dict(c) if c else None # ── Tracking ops ────────────────────────────────────────────────────────────── def save_tracking(username, date_str, day_idx, status): _db().execute(""" INSERT INTO tracking (username,date,day_idx,status) VALUES (?,?,?,?) ON CONFLICT(username,date) DO UPDATE SET status=excluded.status, day_idx=excluded.day_idx """, (username, date_str, day_idx, status)) _db().commit() def load_tracking(username): rows = _db().execute( "SELECT date,day_idx,status FROM tracking WHERE username=?", (username,) ).fetchall() return {r["date"]: {"day_idx": r["day_idx"], "status": r["status"]} for r in rows} def delete_tracking(username): _db().execute("DELETE FROM tracking WHERE username=?", (username,)) _db().commit() # ── Water intake ops ────────────────────────────────────────────────────────── def get_water_today(username, date_str): c = _db().execute( "SELECT amount_ml, goal_ml FROM water_intake WHERE username=? AND date=?", (username, date_str) ).fetchone() return dict(c) if c else {"amount_ml": 0, "goal_ml": 2500} def add_water(username, date_str, amount_ml): _db().execute(""" INSERT INTO water_intake (username, date, amount_ml, goal_ml) VALUES (?, ?, ?, 2500) ON CONFLICT(username, date) DO UPDATE SET amount_ml = amount_ml + ? """, (username, date_str, amount_ml, amount_ml)) _db().commit() def set_water_goal(username, date_str, goal_ml): _db().execute(""" INSERT INTO water_intake (username, date, amount_ml, goal_ml) VALUES (?, ?, 0, ?) ON CONFLICT(username, date) DO UPDATE SET goal_ml=? """, (username, date_str, goal_ml, goal_ml)) _db().commit() def reset_water(username, date_str): _db().execute( "UPDATE water_intake SET amount_ml=0 WHERE username=? AND date=?", (username, date_str) ) _db().commit() def get_water_history(username, days=14): rows = _db().execute(""" SELECT date, amount_ml, goal_ml FROM water_intake WHERE username=? ORDER BY date DESC LIMIT ? """, (username, days)).fetchall() return [dict(r) for r in rows] # ── Diet log ops ────────────────────────────────────────────────────────────── def log_diet(username, date_str, meal_name, calories=0, protein=0, carbs=0, fats=0, fiber=0): _db().execute(""" INSERT INTO diet_log (username, date, meal_name, calories, protein, carbs, fats, fiber, logged_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (username, date_str, meal_name, calories, protein, carbs, fats, fiber, time.time())) _db().commit() def get_diet_today(username, date_str): rows = _db().execute(""" SELECT * FROM diet_log WHERE username=? AND date=? ORDER BY logged_at ASC """, (username, date_str)).fetchall() return [dict(r) for r in rows] def delete_diet_entry(entry_id): _db().execute("DELETE FROM diet_log WHERE id=?", (entry_id,)) _db().commit() def get_diet_totals(username, date_str): c = _db().execute(""" SELECT COALESCE(SUM(calories),0) as calories, COALESCE(SUM(protein),0) as protein, COALESCE(SUM(carbs),0) as carbs, COALESCE(SUM(fats),0) as fats, COALESCE(SUM(fiber),0) as fiber FROM diet_log WHERE username=? AND date=? """, (username, date_str)).fetchone() return dict(c) if c else {"calories":0,"protein":0,"carbs":0,"fats":0,"fiber":0} # ── Weight log ops ──────────────────────────────────────────────────────────── def log_weight(username, date_str, weight_kg, note=""): # upsert — one entry per day _db().execute(""" INSERT INTO weight_log (username, date, weight, note, logged_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING """, (username, date_str, weight_kg, note, time.time())) # if already exists, update it _db().execute(""" UPDATE weight_log SET weight=?, note=?, logged_at=? WHERE username=? AND date=? """, (weight_kg, note, time.time(), username, date_str)) _db().commit() def get_weight_history(username, limit=24): rows = _db().execute(""" SELECT date, weight, note FROM weight_log WHERE username=? ORDER BY date ASC LIMIT ? """, (username, limit)).fetchall() return [dict(r) for r in rows] def get_latest_weight(username): c = _db().execute(""" SELECT weight, date FROM weight_log WHERE username=? ORDER BY date DESC LIMIT 1 """, (username,)).fetchone() return dict(c) if c else None # ── OTP ops ─────────────────────────────────────────────────────────────────── def store_otp(email, otp, username="", password_plain=""): key = f"signup:{email.strip().lower()}" pw_hash = _hash(password_plain) if password_plain else "" _db().execute(""" INSERT INTO otps(key,otp,expires_at,username,password_hash) VALUES(?,?,?,?,?) ON CONFLICT(key) DO UPDATE SET otp=excluded.otp, expires_at=excluded.expires_at, username=excluded.username, password_hash=excluded.password_hash """, (key, otp, time.time() + 600, username, pw_hash)) _db().commit() def check_otp(email, otp_input): key = f"signup:{email.strip().lower()}" c = _db().execute("SELECT * FROM otps WHERE key=?", (key,)).fetchone() if not c: return False, "Code not found. Please sign up again.", "", "" if time.time() > c["expires_at"]: _db().execute("DELETE FROM otps WHERE key=?", (key,)); _db().commit() return False, "Code expired. Please sign up again.", "", "" if c["otp"] != otp_input.strip(): return False, "Incorrect code. Try again.", "", "" username = c["username"] or "" pw_hash = c["password_hash"] or "" _db().execute("DELETE FROM otps WHERE key=?", (key,)); _db().commit() return True, "OK", username, pw_hash # ── Email ───────────────────────────────────────────────────────────────────── def send_otp_email(to_email, otp): if not BREVO_API_KEY: return True, "__NO_EMAIL__" html = f"""
Expires in 10 minutes.