murtaza-2007 commited on
Commit
408707f
·
1 Parent(s): bd46f58

Add email/password sign-in and restrict sign-in to altafhvali.com

Browse files
RAG_Products/api.py CHANGED
@@ -18,7 +18,7 @@ from fastapi.responses import FileResponse
18
  from fastapi.staticfiles import StaticFiles
19
  from pydantic import BaseModel, Field
20
 
21
- from RAG_Products.config import SIMILAR_K, OPENROUTER_API_KEY, ALLOWED_ORIGINS
22
  from RAG_Products.graph import get_graph
23
  from RAG_Products.similar import find_similar, doc_to_dict
24
  from RAG_Products.models import llm_groq
@@ -118,10 +118,16 @@ def health():
118
 
119
  def _resolve_user(authorization, fallback):
120
  """Prefer the verified Firebase uid from the Bearer token; else the
121
- client-supplied id (used in local/no-auth mode)."""
 
 
 
 
122
  if authorization and authorization.lower().startswith("bearer "):
123
- uid = storage.verify_token(authorization[7:].strip())
124
  if uid:
 
 
125
  return uid
126
  return fallback or "default"
127
 
 
18
  from fastapi.staticfiles import StaticFiles
19
  from pydantic import BaseModel, Field
20
 
21
+ from RAG_Products.config import SIMILAR_K, OPENROUTER_API_KEY, ALLOWED_ORIGINS, ALLOWED_EMAIL_DOMAIN
22
  from RAG_Products.graph import get_graph
23
  from RAG_Products.similar import find_similar, doc_to_dict
24
  from RAG_Products.models import llm_groq
 
118
 
119
  def _resolve_user(authorization, fallback):
120
  """Prefer the verified Firebase uid from the Bearer token; else the
121
+ client-supplied id (used in local/no-auth mode).
122
+
123
+ Enforces ALLOWED_EMAIL_DOMAIN: a signed-in user whose email isn't on that
124
+ domain is rejected outright (403), never silently downgraded to anonymous.
125
+ """
126
  if authorization and authorization.lower().startswith("bearer "):
127
+ uid, email = storage.verify_token(authorization[7:].strip())
128
  if uid:
129
+ if ALLOWED_EMAIL_DOMAIN and not (email or "").lower().endswith("@" + ALLOWED_EMAIL_DOMAIN):
130
+ raise HTTPException(403, f"Sign-in is restricted to @{ALLOWED_EMAIL_DOMAIN} accounts.")
131
  return uid
132
  return fallback or "default"
133
 
RAG_Products/config.py CHANGED
@@ -57,6 +57,10 @@ FIREBASE_CREDENTIALS_JSON = os.getenv("FIREBASE_CREDENTIALS_JSON")
57
  FIREBASE_PROJECT_ID = os.getenv("FIREBASE_PROJECT_ID")
58
  CHATS_LOCAL_PATH = BASE_DIR / "Data" / "chats.json" # fallback store
59
 
 
 
 
 
60
  # CORS: comma-separated origins allowed to call the API (the Vercel frontend).
61
  # "*" is fine since there are no cookies; restrict to your domain for safety.
62
  ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*")
 
57
  FIREBASE_PROJECT_ID = os.getenv("FIREBASE_PROJECT_ID")
58
  CHATS_LOCAL_PATH = BASE_DIR / "Data" / "chats.json" # fallback store
59
 
60
+ # Only emails on this domain may sign in (any Firebase provider — Google or
61
+ # email/password). Empty string disables the restriction.
62
+ ALLOWED_EMAIL_DOMAIN = os.getenv("ALLOWED_EMAIL_DOMAIN", "altafhvali.com")
63
+
64
  # CORS: comma-separated origins allowed to call the API (the Vercel frontend).
65
  # "*" is fine since there are no cookies; restrict to your domain for safety.
66
  ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*")
RAG_Products/static/config.js CHANGED
@@ -8,8 +8,12 @@ window.AHV_API = "https://mvali77-ahv.hf.space";
8
  // "Your apps" → Web app → SDK setup and configuration → "Config".
9
  // Leave apiKey empty to disable sign-in (anonymous per-browser mode).
10
  window.FIREBASE_CONFIG = {
11
- apiKey: "",
12
  authDomain: "ahv-assistant.firebaseapp.com",
13
  projectId: "ahv-assistant",
14
  appId: "1:201860800907:web:26114586af1b4a42704f8e"
15
  };
 
 
 
 
 
8
  // "Your apps" → Web app → SDK setup and configuration → "Config".
9
  // Leave apiKey empty to disable sign-in (anonymous per-browser mode).
10
  window.FIREBASE_CONFIG = {
11
+ apiKey: "AIzaSyDlHQi35b2bucAlXTHsso93cIOPdZFFZ0Y",
12
  authDomain: "ahv-assistant.firebaseapp.com",
13
  projectId: "ahv-assistant",
14
  appId: "1:201860800907:web:26114586af1b4a42704f8e"
15
  };
16
+
17
+ // Only this email domain may sign in (must match ALLOWED_EMAIL_DOMAIN on the
18
+ // backend — this is just a fast client-side check, the backend enforces it).
19
+ window.ALLOWED_EMAIL_DOMAIN = "altafhvali.com";
RAG_Products/static/index.html CHANGED
@@ -48,8 +48,21 @@
48
  .user-row .uname { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
49
  .user-row .signout { font-size:12px; color:var(--muted); cursor:pointer; background:none; border:none; }
50
  .user-row .signout:hover { color:#dc2626; }
51
- .gate { text-align:center; color:var(--muted); margin-top:10vh; }
52
  .gate h2 { color:var(--ink); font-weight:600; font-size:20px; margin:0 0 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  .status { font-size:11.5px; color:var(--muted); display:flex; align-items:center; gap:6px; margin-top:2px; }
54
  .dot { width:7px; height:7px; border-radius:50%; background:#d4d4d8; }
55
  .dot.up { background:var(--good); } .dot.down { background:#dc2626; }
@@ -388,6 +401,9 @@
388
  body: JSON.stringify({ message: text, chat_id: activeChat, user_id: USER_ID })
389
  });
390
  const data = await res.json(); loading.remove();
 
 
 
391
  if (!res.ok) {
392
  const e = document.createElement('div'); e.className = 'msg-bot';
393
  e.innerHTML = `<div class="note">${data.detail || 'Something went wrong.'}</div>`; thread.appendChild(e);
@@ -484,11 +500,68 @@
484
 
485
  /* ---------- auth ---------- */
486
  const GOOGLE_SVG = '<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.6 30.2 0 24 0 14.6 0 6.5 5.4 2.6 13.2l7.8 6.1C12.2 13.2 17.6 9.5 24 9.5z"/><path fill="#4285F4" d="M46.1 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.4c-.5 2.9-2.1 5.3-4.5 7l7 5.4c4.1-3.8 6.5-9.4 6.5-16.9z"/><path fill="#FBBC05" d="M10.4 28.3c-.5-1.4-.8-2.9-.8-4.3s.3-3 .8-4.3l-7.8-6.1C.9 16.5 0 20.1 0 24s.9 7.5 2.6 10.4l7.8-6.1z"/><path fill="#34A853" d="M24 48c6.2 0 11.5-2 15.3-5.5l-7-5.4c-2 1.4-4.6 2.2-8.3 2.2-6.4 0-11.8-3.7-13.6-9l-7.8 6.1C6.5 42.6 14.6 48 24 48z"/></svg>';
487
- function gateSign() {
 
 
488
  thread.innerHTML = `<div class="gate"><h2>Welcome to AHV Assistant</h2>
489
- <div>Sign in to start chatting and keep your history on any device.</div></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
490
  box.disabled = true; send.disabled = true; box.placeholder = 'Sign in to chat…';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  }
 
492
  function renderAuth(user) {
493
  const el = $('#authBox');
494
  if (user) {
@@ -498,11 +571,8 @@
498
  <button class="signout" id="signOut">Sign out</button></div>`;
499
  $('#signOut').onclick = () => fbAuth.signOut();
500
  box.disabled = false; send.disabled = false; box.placeholder = 'Message AHV Assistant…';
501
- } else if (AUTH_ENABLED) {
502
- el.innerHTML = `<button class="signin-btn" id="signIn">${GOOGLE_SVG} Sign in with Google</button>`;
503
- $('#signIn').onclick = () => fbAuth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
504
  } else {
505
- el.innerHTML = ''; // anonymous mode: no auth UI
506
  }
507
  }
508
 
@@ -513,6 +583,12 @@
513
  fbAuth = firebase.auth();
514
  gateSign(); renderAuth(null);
515
  fbAuth.onAuthStateChanged(async (user) => {
 
 
 
 
 
 
516
  authUser = user;
517
  if (user) {
518
  USER_ID = user.uid;
 
48
  .user-row .uname { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
49
  .user-row .signout { font-size:12px; color:var(--muted); cursor:pointer; background:none; border:none; }
50
  .user-row .signout:hover { color:#dc2626; }
51
+ .gate { text-align:center; color:var(--muted); margin-top:6vh; }
52
  .gate h2 { color:var(--ink); font-weight:600; font-size:20px; margin:0 0 8px; }
53
+ .gate-box { max-width:340px; margin:24px auto 0; text-align:left; }
54
+ .gate-box .field { margin-bottom:10px; }
55
+ .gate-box input { width:100%; padding:11px 13px; border:1px solid var(--line); border-radius:9px;
56
+ font-size:14.5px; font-family:inherit; outline:none; }
57
+ .gate-box input:focus { border-color:var(--ink); }
58
+ .gate-box .btn-primary { width:100%; padding:11px 13px; border:none; border-radius:9px; background:var(--ink);
59
+ color:#fff; font-size:14.5px; font-weight:550; cursor:pointer; font-family:inherit; }
60
+ .gate-box .btn-primary:disabled { background:#a1a1aa; }
61
+ .gate-divider { display:flex; align-items:center; gap:10px; margin:16px 0; color:var(--muted); font-size:12px; }
62
+ .gate-divider::before, .gate-divider::after { content:''; flex:1; height:1px; background:var(--line); }
63
+ .gate-toggle { margin-top:14px; font-size:13px; }
64
+ .gate-toggle a { color:var(--accent); cursor:pointer; text-decoration:none; }
65
+ .gate-err { color:#dc2626; font-size:13px; margin-top:8px; text-align:left; }
66
  .status { font-size:11.5px; color:var(--muted); display:flex; align-items:center; gap:6px; margin-top:2px; }
67
  .dot { width:7px; height:7px; border-radius:50%; background:#d4d4d8; }
68
  .dot.up { background:var(--good); } .dot.down { background:#dc2626; }
 
401
  body: JSON.stringify({ message: text, chat_id: activeChat, user_id: USER_ID })
402
  });
403
  const data = await res.json(); loading.remove();
404
+ if (res.status === 403 && fbAuth) {
405
+ await fbAuth.signOut(); gateSign(data.detail || 'Access denied.'); return;
406
+ }
407
  if (!res.ok) {
408
  const e = document.createElement('div'); e.className = 'msg-bot';
409
  e.innerHTML = `<div class="note">${data.detail || 'Something went wrong.'}</div>`; thread.appendChild(e);
 
500
 
501
  /* ---------- auth ---------- */
502
  const GOOGLE_SVG = '<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.6 30.2 0 24 0 14.6 0 6.5 5.4 2.6 13.2l7.8 6.1C12.2 13.2 17.6 9.5 24 9.5z"/><path fill="#4285F4" d="M46.1 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.4c-.5 2.9-2.1 5.3-4.5 7l7 5.4c4.1-3.8 6.5-9.4 6.5-16.9z"/><path fill="#FBBC05" d="M10.4 28.3c-.5-1.4-.8-2.9-.8-4.3s.3-3 .8-4.3l-7.8-6.1C.9 16.5 0 20.1 0 24s.9 7.5 2.6 10.4l7.8-6.1z"/><path fill="#34A853" d="M24 48c6.2 0 11.5-2 15.3-5.5l-7-5.4c-2 1.4-4.6 2.2-8.3 2.2-6.4 0-11.8-3.7-13.6-9l-7.8 6.1C6.5 42.6 14.6 48 24 48z"/></svg>';
503
+ let gateMode = 'signin'; // 'signin' | 'signup'
504
+
505
+ function gateSign(errMsg) {
506
  thread.innerHTML = `<div class="gate"><h2>Welcome to AHV Assistant</h2>
507
+ <div>Sign in to start chatting and keep your history on any device.</div>
508
+ ${errMsg ? `<div class="gate-err" style="text-align:center;margin-top:10px">${errMsg}</div>` : ''}
509
+ <div class="gate-box">
510
+ <button class="signin-btn" id="gSignIn">${GOOGLE_SVG} Continue with Google</button>
511
+ <div class="gate-divider">or</div>
512
+ <div class="field"><input id="gEmail" type="email" placeholder="Email" autocomplete="email"></div>
513
+ <div class="field"><input id="gPass" type="password" placeholder="Password" autocomplete="current-password"></div>
514
+ <button class="btn-primary" id="gSubmit">Sign in</button>
515
+ <div class="gate-toggle" id="gToggleWrap">
516
+ <span id="gToggleText">Don't have an account? <a id="gToggle">Sign up</a></span>
517
+ </div>
518
+ <div class="gate-err" id="gErr"></div>
519
+ </div></div>`;
520
  box.disabled = true; send.disabled = true; box.placeholder = 'Sign in to chat…';
521
+
522
+ $('#gSignIn').onclick = () => fbAuth.signInWithPopup(new firebase.auth.GoogleAuthProvider())
523
+ .catch(e => showGateErr(e));
524
+ function toggleMode() {
525
+ gateMode = gateMode === 'signin' ? 'signup' : 'signin';
526
+ $('#gSubmit').textContent = gateMode === 'signin' ? 'Sign in' : 'Create account';
527
+ $('#gToggleText').innerHTML = gateMode === 'signin'
528
+ ? `Don't have an account? <a id="gToggle">Sign up</a>`
529
+ : `Already have an account? <a id="gToggle">Sign in</a>`;
530
+ $('#gToggle').onclick = toggleMode;
531
+ $('#gErr').textContent = '';
532
+ }
533
+ $('#gToggle').onclick = toggleMode;
534
+ $('#gSubmit').onclick = submitEmailAuth;
535
+ [$('#gEmail'), $('#gPass')].forEach(inp => inp.addEventListener('keydown', e => {
536
+ if (e.key === 'Enter') submitEmailAuth();
537
+ }));
538
+ }
539
+
540
+ function showGateErr(e) {
541
+ const map = {
542
+ 'auth/invalid-email': 'That email looks invalid.',
543
+ 'auth/missing-password': 'Enter a password.',
544
+ 'auth/weak-password': 'Password should be at least 6 characters.',
545
+ 'auth/email-already-in-use': 'An account already exists for that email — try signing in instead.',
546
+ 'auth/invalid-credential': 'Wrong email or password.',
547
+ 'auth/wrong-password': 'Wrong email or password.',
548
+ 'auth/user-not-found': 'No account found for that email — try signing up.',
549
+ 'auth/too-many-requests': 'Too many attempts. Try again in a bit.',
550
+ };
551
+ const el = $('#gErr'); if (el) el.textContent = map[e.code] || (e.message || 'Something went wrong.');
552
+ }
553
+
554
+ async function submitEmailAuth() {
555
+ const email = $('#gEmail').value.trim(), pass = $('#gPass').value;
556
+ if (!email || !pass) { $('#gErr').textContent = 'Enter an email and password.'; return; }
557
+ $('#gSubmit').disabled = true; $('#gErr').textContent = '';
558
+ try {
559
+ if (gateMode === 'signin') await fbAuth.signInWithEmailAndPassword(email, pass);
560
+ else await fbAuth.createUserWithEmailAndPassword(email, pass);
561
+ } catch (e) { showGateErr(e); }
562
+ $('#gSubmit').disabled = false;
563
  }
564
+
565
  function renderAuth(user) {
566
  const el = $('#authBox');
567
  if (user) {
 
571
  <button class="signout" id="signOut">Sign out</button></div>`;
572
  $('#signOut').onclick = () => fbAuth.signOut();
573
  box.disabled = false; send.disabled = false; box.placeholder = 'Message AHV Assistant…';
 
 
 
574
  } else {
575
+ el.innerHTML = ''; // signed out (gate covers the sign-in UI) or anonymous mode
576
  }
577
  }
578
 
 
583
  fbAuth = firebase.auth();
584
  gateSign(); renderAuth(null);
585
  fbAuth.onAuthStateChanged(async (user) => {
586
+ const domain = window.ALLOWED_EMAIL_DOMAIN;
587
+ if (user && domain && !(user.email || '').toLowerCase().endsWith('@' + domain.toLowerCase())) {
588
+ await fbAuth.signOut();
589
+ gateSign(`Sign-in is restricted to @${domain} accounts.`);
590
+ return;
591
+ }
592
  authUser = user;
593
  if (user) {
594
  USER_ID = user.uid;
RAG_Products/storage.py CHANGED
@@ -74,20 +74,21 @@ def backend() -> str:
74
 
75
 
76
  def verify_token(id_token):
77
- """Verify a Firebase ID token and return its uid, or None if invalid.
78
 
79
- Returns None when Firestore/Auth isn't configured (local mode) so callers
80
- can fall back to a client-supplied user_id.
81
  """
82
  _init()
83
  if _backend != "firestore" or not id_token:
84
- return None
85
  try:
86
  from firebase_admin import auth
87
 
88
- return auth.verify_id_token(id_token).get("uid")
 
89
  except Exception:
90
- return None
91
 
92
 
93
  # ---------------------------------------------------------------------------
 
74
 
75
 
76
  def verify_token(id_token):
77
+ """Verify a Firebase ID token and return (uid, email), or (None, None).
78
 
79
+ Returns (None, None) when Firestore/Auth isn't configured (local mode) so
80
+ callers can fall back to a client-supplied user_id.
81
  """
82
  _init()
83
  if _backend != "firestore" or not id_token:
84
+ return None, None
85
  try:
86
  from firebase_admin import auth
87
 
88
+ decoded = auth.verify_id_token(id_token)
89
+ return decoded.get("uid"), decoded.get("email")
90
  except Exception:
91
+ return None, None
92
 
93
 
94
  # ---------------------------------------------------------------------------