saiganesh2004 commited on
Commit
6aa0bac
Β·
verified Β·
1 Parent(s): 848d302

Update utils/auth.py

Browse files
Files changed (1) hide show
  1. utils/auth.py +143 -54
utils/auth.py CHANGED
@@ -32,18 +32,33 @@ def _db():
32
  CREATE TABLE IF NOT EXISTS tracking (
33
  username TEXT NOT NULL, date TEXT NOT NULL, day_idx INTEGER,
34
  status TEXT DEFAULT 'pending', PRIMARY KEY (username, date));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  """)
36
- # Add columns if upgrading from old schema
37
- try:
38
- _conn.execute("ALTER TABLE otps ADD COLUMN username TEXT")
39
- _conn.commit()
40
- except Exception:
41
- pass
42
- try:
43
- _conn.execute("ALTER TABLE otps ADD COLUMN password_hash TEXT")
44
- _conn.commit()
45
- except Exception:
46
- pass
47
  return _conn
48
 
49
  def _hash(pw): return hashlib.sha256(pw.encode()).hexdigest()
@@ -156,54 +171,147 @@ def delete_tracking(username):
156
  _db().execute("DELETE FROM tracking WHERE username=?", (username,))
157
  _db().commit()
158
 
159
- # ── OTP ops β€” stores username+password_hash in DB so session_state not needed ─
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  def store_otp(email, otp, username="", password_plain=""):
161
  key = f"signup:{email.strip().lower()}"
162
  pw_hash = _hash(password_plain) if password_plain else ""
163
- _db().execute(
164
- """INSERT INTO otps(key,otp,expires_at,username,password_hash)
165
- VALUES(?,?,?,?,?)
166
- ON CONFLICT(key) DO UPDATE SET
167
- otp=excluded.otp, expires_at=excluded.expires_at,
168
- username=excluded.username, password_hash=excluded.password_hash""",
169
- (key, otp, time.time() + 600, username, pw_hash)
170
- )
171
  _db().commit()
172
 
173
  def check_otp(email, otp_input):
174
- """Returns (ok, msg, username, password_hash)"""
175
  key = f"signup:{email.strip().lower()}"
176
  c = _db().execute("SELECT * FROM otps WHERE key=?", (key,)).fetchone()
177
  if not c:
178
  return False, "Code not found. Please sign up again.", "", ""
179
  if time.time() > c["expires_at"]:
180
- _db().execute("DELETE FROM otps WHERE key=?", (key,))
181
- _db().commit()
182
  return False, "Code expired. Please sign up again.", "", ""
183
  if c["otp"] != otp_input.strip():
184
  return False, "Incorrect code. Try again.", "", ""
185
- username = c["username"] or ""
186
- pw_hash = c["password_hash"] or ""
187
- _db().execute("DELETE FROM otps WHERE key=?", (key,))
188
- _db().commit()
189
  return True, "OK", username, pw_hash
190
 
191
- # ── Brevo email ───────────────────────────────────────────────────────────────
192
  def send_otp_email(to_email, otp):
193
  if not BREVO_API_KEY:
194
  return True, "__NO_EMAIL__"
195
  html = f"""<div style="background:#0a0a0f;padding:40px;font-family:system-ui;
196
- max-width:520px;margin:auto;border-radius:16px;border:1px solid rgba(99,102,241,0.3)">
197
  <div style="text-align:center;margin-bottom:28px">
198
- <div style="font-size:28px;font-weight:900;letter-spacing:3px;color:#6366f1">FITPRO AI</div>
199
- <div style="font-size:11px;letter-spacing:4px;color:rgba(255,255,255,0.3);margin-top:4px">VERIFICATION</div>
200
  </div>
201
  <div style="background:rgba(99,102,241,0.08);border:1px solid rgba(99,102,241,0.25);
202
  border-radius:12px;padding:28px;text-align:center">
203
  <div style="font-size:48px;font-weight:900;letter-spacing:14px;color:#6366f1">{otp}</div>
204
  </div>
205
  <p style="color:rgba(255,255,255,0.4);font-size:12px;text-align:center;margin-top:20px">
206
- Expires in 10 minutes. Don't share this code.</p></div>"""
207
  payload = json.dumps({
208
  "sender": {"name": SENDER_NAME, "email": EMAIL_SENDER},
209
  "to": [{"email": to_email}],
@@ -212,54 +320,36 @@ def send_otp_email(to_email, otp):
212
  }).encode()
213
  req = urllib.request.Request(
214
  "https://api.brevo.com/v3/smtp/email", data=payload,
215
- headers={"accept": "application/json", "api-key": BREVO_API_KEY,
216
- "content-type": "application/json"},
217
- method="POST"
218
  )
219
  try:
220
  with urllib.request.urlopen(req, timeout=15) as r:
221
- return (r.status in (200, 201, 202)), "Sent"
222
  except Exception as e:
223
  return False, str(e)
224
 
225
  # ── Auth flows ────────────────────────────────────────────────────────────────
226
  def initiate_signup(username, email, password):
227
- """
228
- Returns (ok, mode_flag, token_or_none)
229
- ok=True, mode_flag='__DIRECT__', token β†’ no Brevo, registered immediately
230
- ok=True, mode_flag='__OTP__', None β†’ OTP sent, call complete_signup next
231
- ok=False, mode_flag=error_string, None β†’ validation / duplicate error
232
- """
233
  username = username.strip()
234
  email = email.strip().lower()
235
-
236
  if get_user_by_username(username):
237
  return False, "Username already taken.", None
238
  if get_user_by_email(email):
239
  return False, "Email already registered.", None
240
  if len(password) < 6:
241
  return False, "Password must be at least 6 characters.", None
242
-
243
- # No Brevo β†’ register directly, log in immediately
244
  if not BREVO_API_KEY:
245
  token = create_user(username, email, password)
246
  return True, "__DIRECT__", token
247
-
248
- # Brevo configured β†’ send OTP and store everything in DB
249
  otp = _otp()
250
  ok, msg = send_otp_email(email, otp)
251
  if not ok:
252
  return False, msg, None
253
- # Store username + hashed password in the OTP row so we don't need session_state
254
  store_otp(email, otp, username=username, password_plain=password)
255
  return True, "__OTP__", None
256
 
257
-
258
  def complete_signup(email, otp_input):
259
- """
260
- No longer needs username/password β€” reads them from the OTP row in DB.
261
- Returns (ok, token_or_none, username_or_error_msg)
262
- """
263
  ok, msg, username, pw_hash = check_otp(email, otp_input)
264
  if not ok:
265
  return False, None, msg
@@ -268,7 +358,6 @@ def complete_signup(email, otp_input):
268
  token = create_user(username, email, pw_hash, already_hashed=True)
269
  return True, token, username
270
 
271
-
272
  def login(username_or_email, password):
273
  u = get_user(username_or_email)
274
  if not u:
 
32
  CREATE TABLE IF NOT EXISTS tracking (
33
  username TEXT NOT NULL, date TEXT NOT NULL, day_idx INTEGER,
34
  status TEXT DEFAULT 'pending', PRIMARY KEY (username, date));
35
+ CREATE TABLE IF NOT EXISTS water_intake (
36
+ username TEXT NOT NULL, date TEXT NOT NULL,
37
+ amount_ml INTEGER DEFAULT 0,
38
+ goal_ml INTEGER DEFAULT 2500,
39
+ PRIMARY KEY (username, date));
40
+ CREATE TABLE IF NOT EXISTS diet_log (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ username TEXT NOT NULL, date TEXT NOT NULL,
43
+ meal_name TEXT NOT NULL,
44
+ calories REAL DEFAULT 0, protein REAL DEFAULT 0,
45
+ carbs REAL DEFAULT 0, fats REAL DEFAULT 0,
46
+ fiber REAL DEFAULT 0, logged_at REAL);
47
+ CREATE TABLE IF NOT EXISTS weight_log (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ username TEXT NOT NULL, date TEXT NOT NULL,
50
+ weight REAL NOT NULL, note TEXT DEFAULT '',
51
+ logged_at REAL);
52
  """)
53
+ # Upgrade older schemas safely
54
+ for col_sql in [
55
+ "ALTER TABLE otps ADD COLUMN username TEXT",
56
+ "ALTER TABLE otps ADD COLUMN password_hash TEXT",
57
+ ]:
58
+ try:
59
+ _conn.execute(col_sql); _conn.commit()
60
+ except Exception:
61
+ pass
 
 
62
  return _conn
63
 
64
  def _hash(pw): return hashlib.sha256(pw.encode()).hexdigest()
 
171
  _db().execute("DELETE FROM tracking WHERE username=?", (username,))
172
  _db().commit()
173
 
174
+ # ── Water intake ops ──────────────────────────────────────────────────────────
175
+ def get_water_today(username, date_str):
176
+ c = _db().execute(
177
+ "SELECT amount_ml, goal_ml FROM water_intake WHERE username=? AND date=?",
178
+ (username, date_str)
179
+ ).fetchone()
180
+ return dict(c) if c else {"amount_ml": 0, "goal_ml": 2500}
181
+
182
+ def add_water(username, date_str, amount_ml):
183
+ _db().execute("""
184
+ INSERT INTO water_intake (username, date, amount_ml, goal_ml)
185
+ VALUES (?, ?, ?, 2500)
186
+ ON CONFLICT(username, date) DO UPDATE SET
187
+ amount_ml = amount_ml + ?
188
+ """, (username, date_str, amount_ml, amount_ml))
189
+ _db().commit()
190
+
191
+ def set_water_goal(username, date_str, goal_ml):
192
+ _db().execute("""
193
+ INSERT INTO water_intake (username, date, amount_ml, goal_ml)
194
+ VALUES (?, ?, 0, ?)
195
+ ON CONFLICT(username, date) DO UPDATE SET goal_ml=?
196
+ """, (username, date_str, goal_ml, goal_ml))
197
+ _db().commit()
198
+
199
+ def reset_water(username, date_str):
200
+ _db().execute(
201
+ "UPDATE water_intake SET amount_ml=0 WHERE username=? AND date=?",
202
+ (username, date_str)
203
+ )
204
+ _db().commit()
205
+
206
+ def get_water_history(username, days=14):
207
+ rows = _db().execute("""
208
+ SELECT date, amount_ml, goal_ml FROM water_intake
209
+ WHERE username=? ORDER BY date DESC LIMIT ?
210
+ """, (username, days)).fetchall()
211
+ return [dict(r) for r in rows]
212
+
213
+ # ── Diet log ops ──────────────────────────────────────────────────────────────
214
+ def log_diet(username, date_str, meal_name, calories=0, protein=0, carbs=0, fats=0, fiber=0):
215
+ _db().execute("""
216
+ INSERT INTO diet_log (username, date, meal_name, calories, protein, carbs, fats, fiber, logged_at)
217
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
218
+ """, (username, date_str, meal_name, calories, protein, carbs, fats, fiber, time.time()))
219
+ _db().commit()
220
+
221
+ def get_diet_today(username, date_str):
222
+ rows = _db().execute("""
223
+ SELECT * FROM diet_log WHERE username=? AND date=? ORDER BY logged_at ASC
224
+ """, (username, date_str)).fetchall()
225
+ return [dict(r) for r in rows]
226
+
227
+ def delete_diet_entry(entry_id):
228
+ _db().execute("DELETE FROM diet_log WHERE id=?", (entry_id,))
229
+ _db().commit()
230
+
231
+ def get_diet_totals(username, date_str):
232
+ c = _db().execute("""
233
+ SELECT
234
+ COALESCE(SUM(calories),0) as calories,
235
+ COALESCE(SUM(protein),0) as protein,
236
+ COALESCE(SUM(carbs),0) as carbs,
237
+ COALESCE(SUM(fats),0) as fats,
238
+ COALESCE(SUM(fiber),0) as fiber
239
+ FROM diet_log WHERE username=? AND date=?
240
+ """, (username, date_str)).fetchone()
241
+ return dict(c) if c else {"calories":0,"protein":0,"carbs":0,"fats":0,"fiber":0}
242
+
243
+ # ── Weight log ops ────────────────────────────────────────────────────────────
244
+ def log_weight(username, date_str, weight_kg, note=""):
245
+ # upsert β€” one entry per day
246
+ _db().execute("""
247
+ INSERT INTO weight_log (username, date, weight, note, logged_at)
248
+ VALUES (?, ?, ?, ?, ?)
249
+ ON CONFLICT DO NOTHING
250
+ """, (username, date_str, weight_kg, note, time.time()))
251
+ # if already exists, update it
252
+ _db().execute("""
253
+ UPDATE weight_log SET weight=?, note=?, logged_at=?
254
+ WHERE username=? AND date=?
255
+ """, (weight_kg, note, time.time(), username, date_str))
256
+ _db().commit()
257
+
258
+ def get_weight_history(username, limit=24):
259
+ rows = _db().execute("""
260
+ SELECT date, weight, note FROM weight_log
261
+ WHERE username=? ORDER BY date ASC LIMIT ?
262
+ """, (username, limit)).fetchall()
263
+ return [dict(r) for r in rows]
264
+
265
+ def get_latest_weight(username):
266
+ c = _db().execute("""
267
+ SELECT weight, date FROM weight_log WHERE username=?
268
+ ORDER BY date DESC LIMIT 1
269
+ """, (username,)).fetchone()
270
+ return dict(c) if c else None
271
+
272
+ # ── OTP ops ───────────────────────────────────────────────────────────────────
273
  def store_otp(email, otp, username="", password_plain=""):
274
  key = f"signup:{email.strip().lower()}"
275
  pw_hash = _hash(password_plain) if password_plain else ""
276
+ _db().execute("""
277
+ INSERT INTO otps(key,otp,expires_at,username,password_hash)
278
+ VALUES(?,?,?,?,?)
279
+ ON CONFLICT(key) DO UPDATE SET
280
+ otp=excluded.otp, expires_at=excluded.expires_at,
281
+ username=excluded.username, password_hash=excluded.password_hash
282
+ """, (key, otp, time.time() + 600, username, pw_hash))
 
283
  _db().commit()
284
 
285
  def check_otp(email, otp_input):
 
286
  key = f"signup:{email.strip().lower()}"
287
  c = _db().execute("SELECT * FROM otps WHERE key=?", (key,)).fetchone()
288
  if not c:
289
  return False, "Code not found. Please sign up again.", "", ""
290
  if time.time() > c["expires_at"]:
291
+ _db().execute("DELETE FROM otps WHERE key=?", (key,)); _db().commit()
 
292
  return False, "Code expired. Please sign up again.", "", ""
293
  if c["otp"] != otp_input.strip():
294
  return False, "Incorrect code. Try again.", "", ""
295
+ username = c["username"] or ""
296
+ pw_hash = c["password_hash"] or ""
297
+ _db().execute("DELETE FROM otps WHERE key=?", (key,)); _db().commit()
 
298
  return True, "OK", username, pw_hash
299
 
300
+ # ── Email ─────────────────────────────────────────────────────────────────────
301
  def send_otp_email(to_email, otp):
302
  if not BREVO_API_KEY:
303
  return True, "__NO_EMAIL__"
304
  html = f"""<div style="background:#0a0a0f;padding:40px;font-family:system-ui;
305
+ max-width:520px;margin:auto;border-radius:16px">
306
  <div style="text-align:center;margin-bottom:28px">
307
+ <div style="font-size:28px;font-weight:900;color:#6366f1">FITPRO AI</div>
 
308
  </div>
309
  <div style="background:rgba(99,102,241,0.08);border:1px solid rgba(99,102,241,0.25);
310
  border-radius:12px;padding:28px;text-align:center">
311
  <div style="font-size:48px;font-weight:900;letter-spacing:14px;color:#6366f1">{otp}</div>
312
  </div>
313
  <p style="color:rgba(255,255,255,0.4);font-size:12px;text-align:center;margin-top:20px">
314
+ Expires in 10 minutes.</p></div>"""
315
  payload = json.dumps({
316
  "sender": {"name": SENDER_NAME, "email": EMAIL_SENDER},
317
  "to": [{"email": to_email}],
 
320
  }).encode()
321
  req = urllib.request.Request(
322
  "https://api.brevo.com/v3/smtp/email", data=payload,
323
+ headers={"accept":"application/json","api-key":BREVO_API_KEY,
324
+ "content-type":"application/json"}, method="POST"
 
325
  )
326
  try:
327
  with urllib.request.urlopen(req, timeout=15) as r:
328
+ return (r.status in (200,201,202)), "Sent"
329
  except Exception as e:
330
  return False, str(e)
331
 
332
  # ── Auth flows ────────────────────────────────────────────────────────────────
333
  def initiate_signup(username, email, password):
 
 
 
 
 
 
334
  username = username.strip()
335
  email = email.strip().lower()
 
336
  if get_user_by_username(username):
337
  return False, "Username already taken.", None
338
  if get_user_by_email(email):
339
  return False, "Email already registered.", None
340
  if len(password) < 6:
341
  return False, "Password must be at least 6 characters.", None
 
 
342
  if not BREVO_API_KEY:
343
  token = create_user(username, email, password)
344
  return True, "__DIRECT__", token
 
 
345
  otp = _otp()
346
  ok, msg = send_otp_email(email, otp)
347
  if not ok:
348
  return False, msg, None
 
349
  store_otp(email, otp, username=username, password_plain=password)
350
  return True, "__OTP__", None
351
 
 
352
  def complete_signup(email, otp_input):
 
 
 
 
353
  ok, msg, username, pw_hash = check_otp(email, otp_input)
354
  if not ok:
355
  return False, None, msg
 
358
  token = create_user(username, email, pw_hash, already_hashed=True)
359
  return True, token, username
360
 
 
361
  def login(username_or_email, password):
362
  u = get_user(username_or_email)
363
  if not u: