From 6b29227131fc0a56695785dfd68ce1610716af68 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 6 Jul 2026 15:55:14 +0200 Subject: [PATCH] fix(rmi-backend): define missing User helpers + import CryptContext (fixes 23 F821 in app/auth.py) --- app/auth.py | 92 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/app/auth.py b/app/auth.py index 9034c56..62b8e39 100644 --- a/app/auth.py +++ b/app/auth.py @@ -14,8 +14,11 @@ from typing import Any from fastapi import APIRouter, HTTPException, Request from jose import JWTError, jwt +from passlib.context import CryptContext from pydantic import BaseModel, Field +from app.core.redis import get_redis + logger = logging.getLogger(__name__) router = APIRouter(tags=["auth"]) @@ -83,12 +86,12 @@ def _generate_backup_codes(count: int = 8) -> list: def _hash_backup_code(code: str) -> str: """Hash a backup code for storage (same as password hashing).""" - return pwd_context.hash(code) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + return pwd_context.hash(code) def _verify_backup_code(code: str, hashed: str) -> bool: """Verify a backup code against its hash.""" - return pwd_context.verify(code, hashed) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + return pwd_context.verify(code, hashed) # ── Config ── @@ -98,6 +101,8 @@ JWT_EXPIRY_DAYS = 7 import bcrypt # noqa: E402 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + def hash_password(password: str) -> str: """Hash password using bcrypt directly.""" @@ -112,6 +117,45 @@ def verify_password(password: str, hashed: str) -> bool: return False +# ── Redis User Store ── +def _get_user(user_id: str) -> dict[str, Any] | None: + """Fetch user record by id from Redis hash rmi:users.""" + r = get_redis() + if not r: + return None + raw = r.hget("rmi:users", user_id) + if not raw: + return None + try: + return json.loads(raw) + except (json.JSONDecodeError, TypeError): + return None + + +def _get_user_by_email(email: str) -> dict[str, Any] | None: + """Fetch user record by email via rmi:users:email index → rmi:users hash.""" + r = get_redis() + if not r: + return None + user_id = r.hget("rmi:users:email", email.lower()) + if not user_id: + return None + if isinstance(user_id, bytes): + user_id = user_id.decode("utf-8") + return _get_user(user_id) + + +def _save_user(user: dict[str, Any]) -> None: + """Persist user record to Redis hash rmi:users, refresh email index if present.""" + r = get_redis() + if not r: + return + user_id = user["id"] + r.hset("rmi:users", user_id, json.dumps(user)) + if user.get("email"): + r.hset("rmi:users:email", user["email"].lower(), user_id) + + # ── Helpers ── def _is_valid_email(email: str) -> bool: """Basic email validation.""" @@ -265,7 +309,7 @@ def register_email(req: EmailRegisterRequest): raise HTTPException(status_code=400, detail="Password must be at least 8 characters") # Check if user exists - existing = _get_user_by_email(req.email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + existing = _get_user_by_email(req.email) if existing: raise HTTPException(status_code=400, detail="Email already registered") @@ -290,7 +334,7 @@ def register_email(req: EmailRegisterRequest): } # Save user + email index - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() r.hset("rmi:users", user_id, json.dumps(user)) r.hset("rmi:users:email", req.email.lower(), user_id) @@ -315,7 +359,7 @@ def register_email(req: EmailRegisterRequest): @router.post("/login", response_model=WalletAuthResponse) def login_email(req: EmailLoginRequest): """Login with email/password. If 2FA enabled, returns partial token requiring /2fa/login.""" - user = _get_user_by_email(req.email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user_by_email(req.email) if not user or not user.get("password_hash"): raise HTTPException(status_code=401, detail="Invalid credentials") @@ -434,7 +478,7 @@ async def get_current_user(request: Request): if not payload: raise HTTPException(status_code=401, detail="Invalid token") - user = _get_user(payload["id"]) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user(payload["id"]) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -530,7 +574,7 @@ async def google_callback(request: Request): google_user = resp.json() email = google_user.get("email") - user = _get_user_by_email(email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user_by_email(email) if not user: user_id = _derive_user_id(email) user = { @@ -546,7 +590,7 @@ async def google_callback(request: Request): "scans_remaining": 5, "scans_used": 0, } - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() r.hset("rmi:users", user_id, json.dumps(user)) r.hset("rmi:users:email", email.lower(), user_id) @@ -601,7 +645,7 @@ async def google_callback_post(request: Request): google_user = resp.json() email = google_user.get("email") - user = _get_user_by_email(email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user_by_email(email) if not user: user_id = _derive_user_id(email) user = { @@ -617,7 +661,7 @@ async def google_callback_post(request: Request): "scans_remaining": 5, "scans_used": 0, } - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() r.hset("rmi:users", user_id, json.dumps(user)) r.hset("rmi:users:email", email.lower(), user_id) @@ -671,7 +715,7 @@ async def telegram_auth(req: TelegramAuthRequest): raise HTTPException(status_code=401, detail="Invalid Telegram auth data") telegram_user_id = f"tg:{req.id}" - user = _get_user(telegram_user_id) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user(telegram_user_id) if not user: display_name = f"{req.first_name or ''} {req.last_name or ''}".strip() or req.username or f"User{req.id}" @@ -692,7 +736,7 @@ async def telegram_auth(req: TelegramAuthRequest): "scans_remaining": 5, "scans_used": 0, } - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() r.hset("rmi:users", telegram_user_id, json.dumps(user)) r.hset("rmi:users:telegram", str(req.id), telegram_user_id) @@ -801,7 +845,7 @@ async def github_callback(request: Request): if not email: email = f"{github_user.get('login', 'github_user')}@github.rmi" - user = _get_user_by_email(email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user_by_email(email) if not user: user_id = _derive_user_id(email) user = { @@ -817,7 +861,7 @@ async def github_callback(request: Request): "scans_remaining": 5, "scans_used": 0, } - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() r.hset("rmi:users", user_id, json.dumps(user)) r.hset("rmi:users:email", email.lower(), user_id) @@ -863,8 +907,6 @@ async def x_callback(request: Request): import httpx - from app.core.redis import get_redis - client_id = os.getenv("X_CLIENT_ID") client_secret = os.getenv("X_CLIENT_SECRET") redirect_uri = os.getenv("X_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/x/callback") @@ -904,7 +946,7 @@ async def x_callback(request: Request): username = x_user.get("username", f"x_user_{x_user.get('id', 'unknown')}") email = f"{username}@x.rmi" # X API v2 doesn't always return email - user = _get_user_by_email(email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user_by_email(email) if not user: user_id = _derive_user_id(email) user = { @@ -938,7 +980,7 @@ async def get_current_user(request: Request) -> dict[str, Any] | None: # noqa: payload = _verify_jwt(token) if not payload: return None - user = _get_user(payload["id"]) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user(payload["id"]) if not user: return None return user @@ -975,7 +1017,7 @@ async def twofa_setup(request: Request): backup_codes = _generate_backup_codes(8) # Store pending secret (not enabled until verified) - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() pending = { "secret": secret, "backup_codes": [_hash_backup_code(c) for c in backup_codes], @@ -1001,7 +1043,7 @@ async def twofa_enable(req: TwoFAEnableRequest, request: Request): if not user: raise HTTPException(status_code=401, detail="Authentication required") - r = get_redis() # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + r = get_redis() pending_raw = r.get(f"rmi:2fa_pending:{user['id']}") if not pending_raw: raise HTTPException(status_code=400, detail="No pending 2FA setup. Call /2fa/setup first.") @@ -1019,7 +1061,7 @@ async def twofa_enable(req: TwoFAEnableRequest, request: Request): user["totp_created_at"] = pending["created_at"] user["totp_backup_codes"] = pending["backup_codes"] # already hashed user["totp_last_used"] = None - _save_user(user) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + _save_user(user) # Clean up pending r.delete(f"rmi:2fa_pending:{user['id']}") @@ -1050,7 +1092,7 @@ async def twofa_disable(request: Request): ]: user.pop(key, None) - _save_user(user) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + _save_user(user) logger.info(f"[2FA DISABLED] user={user['id']} email={user['email']}") return {"status": "ok", "message": "2FA disabled successfully"} @@ -1114,7 +1156,7 @@ async def twofa_verify(req: TwoFAVerifyRequest, request: Request): # Update last used user["totp_last_used"] = datetime.utcnow().isoformat() - _save_user(user) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + _save_user(user) return {"status": "ok", "verified": True} @@ -1125,7 +1167,7 @@ async def twofa_login(req: TwoFALoginRequest): if not PYOTP_AVAILABLE: raise HTTPException(status_code=503, detail="2FA service unavailable") - user = _get_user_by_email(req.email) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + user = _get_user_by_email(req.email) if not user or not user.get("password_hash"): raise HTTPException(status_code=401, detail="Invalid credentials") @@ -1157,7 +1199,7 @@ async def twofa_login(req: TwoFALoginRequest): # Update last used user["totp_last_used"] = datetime.utcnow().isoformat() - _save_user(user) # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue + _save_user(user) token = _create_jwt( user["id"],