Spaces:
Runtime error
Runtime error
| """Auth credential models and data-access layer.""" | |
| from __future__ import annotations | |
| import logging | |
| import uuid | |
| from typing import Optional | |
| from rexpro_ai.internal.db import Base, JSONField, get_async_db_context | |
| from rexpro_ai.models.users import User, UserModel, UserProfileImageResponse, Users | |
| from rexpro_ai.utils.validate import validate_profile_image_url | |
| from pydantic import BaseModel, field_validator | |
| from sqlalchemy import Boolean, Column, String, Text, delete, select, update | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| log = logging.getLogger(__name__) | |
| class Auth(Base): # credential ↔ user linkage | |
| """Maps a user ID to an email/password pair with an active flag.""" | |
| __tablename__ = 'auth' | |
| id = Column(String, primary_key=True, unique=True) # mirrors User.id | |
| email = Column(String) # login address, kept in sync with User.email | |
| password = Column(Text) # argon2 / bcrypt hash | |
| active = Column(Boolean) # account soft-disable toggle | |
| class AuthModel(BaseModel): | |
| """Pydantic mirror of the ``auth`` table row.""" | |
| id: str | |
| email: str | |
| password: str | |
| active: bool = True | |
| class Token(BaseModel): | |
| """JWT bearer-token response wrapper.""" | |
| token: str | |
| token_type: str | |
| class ApiKey(BaseModel): | |
| api_key: str | None = None | |
| class SigninResponse(Token, UserProfileImageResponse): | |
| pass | |
| class SigninForm(BaseModel): | |
| email: str | |
| password: str | |
| class LdapForm(BaseModel): | |
| user: str | |
| password: str | |
| class ProfileImageUrlForm(BaseModel): | |
| profile_image_url: str | |
| class UpdatePasswordForm(BaseModel): | |
| password: str | |
| new_password: str | |
| class SignupForm(BaseModel): | |
| name: str | |
| email: str | |
| password: str | |
| profile_image_url: str | None = '/user.png' | |
| def check_profile_image_url(cls, v: str | None) -> str | None: | |
| if v is not None: | |
| return validate_profile_image_url(v) | |
| return v | |
| class AddUserForm(SignupForm): | |
| role: str | None = 'pending' | |
| # --- data-access layer --- | |
| class AuthsTable: | |
| """Provides CRUD operations for the Auth ↔ User lifecycle.""" | |
| async def insert_new_auth( | |
| self, | |
| email: str, | |
| password: str, | |
| name: str, | |
| profile_image_url: str = '/user.png', | |
| role: str = 'pending', | |
| oauth: dict | None = None, | |
| db: AsyncSession | None = None, | |
| ) -> UserModel | None: | |
| """Create an Auth + User pair inside a single transaction.""" | |
| async with get_async_db_context(db) as session: | |
| log.info('insert_new_auth') | |
| new_id = str(uuid.uuid4()) | |
| credential = Auth( | |
| id=new_id, | |
| email=email, | |
| password=password, | |
| active=True, | |
| ) | |
| session.add(credential) | |
| created_user = await Users.insert_new_user( | |
| new_id, | |
| name, | |
| email, | |
| profile_image_url, | |
| role, | |
| oauth=oauth, | |
| db=session, | |
| ) | |
| # persist both records and reload generated defaults | |
| await session.commit() | |
| await session.refresh(credential) | |
| return created_user if credential and created_user else None | |
| async def authenticate_user( | |
| self, | |
| email: str, | |
| verify_password: callable, | |
| db: AsyncSession | None = None, | |
| ) -> UserModel | None: | |
| """Verify email + password credentials and return the matching user.""" | |
| log.info('authenticate_user: %s', email) | |
| resolved = await Users.get_user_by_email(email, db=db) | |
| if not resolved: | |
| return | |
| # load the credential row and verify the password hash | |
| async with get_async_db_context(db) as session: | |
| credential = await session.get(Auth, resolved.id) | |
| if not credential or not credential.active: | |
| return | |
| if not verify_password(credential.password): | |
| return | |
| return resolved | |
| async def authenticate_user_by_api_key( | |
| self, | |
| api_key: str, | |
| db: AsyncSession | None = None, | |
| ) -> UserModel | None: | |
| """Look up the user that owns the given API key.""" | |
| log.info('authenticate_user_by_api_key') | |
| if not api_key: | |
| return | |
| # delegate to the Users model for the actual lookup | |
| return await Users.get_user_by_api_key(api_key, db=db) | |
| async def authenticate_user_by_email( | |
| self, | |
| email: str, | |
| db: AsyncSession | None = None, | |
| ) -> UserModel | None: | |
| """Single-query auth via JOIN on Auth ↔ User, filtered by active flag.""" | |
| log.info('authenticate_user_by_email: %s', email) | |
| # single JOIN avoids N+1 — returns (Auth, User) tuple or None | |
| async with get_async_db_context(db) as session: | |
| joined_query = ( | |
| select(Auth, User).join(User, Auth.id == User.id).where(Auth.email == email, Auth.active.is_(True)) | |
| ) | |
| match = (await session.execute(joined_query)).first() | |
| if not match: | |
| return | |
| _, found_user = match | |
| return UserModel.model_validate(found_user) | |
| async def update_email_by_id( | |
| self, | |
| user_id: str, | |
| email: str, | |
| db: AsyncSession | None = None, | |
| ) -> bool: | |
| """Set a new email on the auth record and propagate to the user row.""" | |
| async with get_async_db_context(db) as session: | |
| auth_row = await session.get(Auth, user_id) | |
| if auth_row is None: | |
| return False | |
| auth_row.email = email | |
| await session.commit() | |
| await Users.update_user_by_id(user_id, {'email': email}, db=session) | |
| return True | |
| # --- password modification --- | |
| async def update_user_password_by_id( | |
| self, | |
| user_id: str, | |
| new_password: str, | |
| db: AsyncSession | None = None, | |
| ) -> bool: | |
| """Set a new password hash for an existing user.""" | |
| async with get_async_db_context(db) as session: | |
| auth_row = await session.get(Auth, user_id) | |
| if auth_row is None: | |
| return False | |
| auth_row.password = new_password | |
| await session.commit() | |
| return True | |
| async def delete_auth_by_id( | |
| self, | |
| id: str, | |
| db: AsyncSession | None = None, | |
| ) -> bool: | |
| """Remove a user and their auth credential in one transaction.""" | |
| async with get_async_db_context(db) as session: | |
| if not await Users.delete_user_by_id(id, db=session): | |
| return False | |
| await session.execute(delete(Auth).where(Auth.id == id)) | |
| await session.commit() | |
| return True | |
| Auths = AuthsTable() # singleton — module-level instance | |