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 +9 -3
- RAG_Products/config.py +4 -0
- RAG_Products/static/config.js +5 -1
- RAG_Products/static/index.html +83 -7
- RAG_Products/storage.py +7 -6
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:
|
| 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 |
-
|
|
|
|
|
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = ''; //
|
| 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
|
| 78 |
|
| 79 |
-
Returns None when Firestore/Auth isn't configured (local mode) so
|
| 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 |
-
|
|
|
|
| 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 |
# ---------------------------------------------------------------------------
|