saivivek6's picture
Deploy vivek app to Space: Vue UI (bundled dist), streaming widgets, JSON schema renderer
c97e8a9
Raw
History Blame
5.27 kB
from __future__ import annotations
import uuid
from dataclasses import dataclass
from threading import Lock
from typing import Any, Dict, Optional
import bcrypt
import jwt
from . import config
@dataclass(frozen=True)
class UserRecord:
user_id: str
username: str
email: str
password_hash: bytes
_lock = Lock()
_users_by_id: Dict[str, UserRecord] = {}
_user_id_by_username: Dict[str, str] = {}
_user_id_by_email: Dict[str, str] = {}
def _normalize_username(s: str) -> str:
return (s or "").strip().lower()
def _normalize_email(s: str) -> str:
return (s or "").strip().lower()
def register_user(*, username: str, email: str, password: str) -> UserRecord:
username_n = _normalize_username(username)
email_n = _normalize_email(email)
if not username_n:
raise ValueError("username required")
if not email_n or "@" not in email_n:
raise ValueError("valid email required")
if not password or len(password) < 6:
raise ValueError("password must be at least 6 chars")
with _lock:
if username_n in _user_id_by_username:
raise ValueError("username already exists")
if email_n in _user_id_by_email:
raise ValueError("email already exists")
password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
user_id = uuid.uuid4().hex
rec = UserRecord(user_id=user_id, username=username_n, email=email_n, password_hash=password_hash)
_users_by_id[user_id] = rec
_user_id_by_username[username_n] = user_id
_user_id_by_email[email_n] = user_id
# Persist users so bandit state can be recovered across restarts.
try:
from . import db as persistence
persistence.persist_user(rec)
except Exception:
# For local dev, allow auth to work even if DB isn't ready yet.
pass
return rec
def authenticate_login(*, username_or_email: str, password: str) -> Optional[UserRecord]:
ident = (username_or_email or "").strip()
if not ident or not password:
return None
username_n = _normalize_username(ident)
email_n = _normalize_email(ident)
with _lock:
user_id = _user_id_by_email.get(email_n) or _user_id_by_username.get(username_n)
if not user_id:
return None
rec = _users_by_id.get(user_id)
if not rec:
return None
ok = bcrypt.checkpw(password.encode("utf-8"), rec.password_hash)
return rec if ok else None
def get_user_by_id(user_id: str) -> Optional[UserRecord]:
if not user_id:
return None
with _lock:
return _users_by_id.get(user_id)
def get_user_by_email(email: str) -> Optional[UserRecord]:
email_n = _normalize_email(email)
if not email_n:
return None
with _lock:
user_id = _user_id_by_email.get(email_n)
return _users_by_id.get(user_id) if user_id else None
def update_password(*, user_id: str, new_password: str) -> bool:
"""
Update password hash in-memory and persist to SQLite.
Returns True if updated, False if user not found.
"""
if not user_id or not new_password or len(new_password) < 6:
raise ValueError("password must be at least 6 chars")
new_hash = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
with _lock:
rec = _users_by_id.get(user_id)
if not rec:
return False
updated = UserRecord(user_id=rec.user_id, username=rec.username, email=rec.email, password_hash=new_hash)
_users_by_id[user_id] = updated
# Persist outside the lock.
try:
from . import db as persistence
persistence.persist_user(updated)
except Exception:
# Allow dev/test if DB isn't ready.
pass
return True
def seed_users_from_db(rows: list[dict[str, Any]]) -> None:
"""
Replace the in-memory user store with persisted users from SQLite.
Intended to be called once during FastAPI startup.
"""
with _lock:
_users_by_id.clear()
_user_id_by_username.clear()
_user_id_by_email.clear()
for r in rows:
rec = UserRecord(
user_id=str(r["user_id"]),
username=str(r["username"]),
email=str(r["email"]),
password_hash=r["password_hash"],
)
_users_by_id[rec.user_id] = rec
_user_id_by_username[_normalize_username(rec.username)] = rec.user_id
_user_id_by_email[_normalize_email(rec.email)] = rec.user_id
def create_access_token(*, user_id: str) -> str:
import time as _time
now = int(_time.time())
exp = now + int(getattr(config, "JWT_EXPIRE_SECONDS", 60 * 60 * 24 * 7))
payload = {"sub": user_id, "iat": now, "exp": exp}
return jwt.encode(payload, config.JWT_SECRET, algorithm=getattr(config, "JWT_ALGORITHM", "HS256"))
def decode_access_token(token: str) -> str:
if not token:
raise ValueError("missing token")
payload = jwt.decode(token, config.JWT_SECRET, algorithms=[getattr(config, "JWT_ALGORITHM", "HS256")])
sub = payload.get("sub")
if not sub:
raise ValueError("invalid token payload")
return str(sub)