Daoyuan Li b5cc3a81c3
fix(auth): resolve email accounts case-insensitively (#4101)
* fix(auth): handle email addresses case-insensitively

Normalize new and changed addresses, use case-insensitive lookup, and keep legacy case-variant rows stable during unrelated updates.

* fix(auth): reject legacy case-variant registrations

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-26 09:52:16 +08:00

108 lines
3.1 KiB
Python

"""User repository interface for abstracting database operations."""
from abc import ABC, abstractmethod
from app.gateway.auth.models import User
class UserNotFoundError(LookupError):
"""Raised when a user repository operation targets a non-existent row.
Subclass of :class:`LookupError` so callers that already catch
``LookupError`` for "missing entity" can keep working unchanged,
while specific call sites can pin to this class to distinguish
"concurrent delete during update" from other lookups.
"""
class UserRepository(ABC):
"""Abstract interface for user data storage.
Implement this interface to support different storage backends
(SQLite)
"""
@abstractmethod
async def create_user(self, user: User) -> User:
"""Create a new user.
Args:
user: User object to create
Returns:
Created User with ID assigned. ``email`` is the canonical
(lowercase) stored form, which may differ in case from what was
passed in -- implementations mutate the input ``user`` in place
to reflect this rather than returning a fresh object.
Raises:
ValueError: If email already exists
"""
raise NotImplementedError
@abstractmethod
async def get_user_by_id(self, user_id: str) -> User | None:
"""Get user by ID.
Args:
user_id: User UUID as string
Returns:
User if found, None otherwise
"""
raise NotImplementedError
@abstractmethod
async def get_user_by_email(self, email: str) -> User | None:
"""Get user by email.
Args:
email: User email address
Returns:
User if found, None otherwise
"""
raise NotImplementedError
@abstractmethod
async def update_user(self, user: User) -> User:
"""Update an existing user.
Args:
user: User object with updated fields
Returns:
Updated User. ``email`` is the canonical (lowercase) stored
form -- implementations mutate the input ``user`` in place to
reflect this rather than returning a fresh object.
Raises:
UserNotFoundError: If no row exists for ``user.id``. This is
a hard failure (not a no-op) so callers cannot mistake a
concurrent-delete race for a successful update.
"""
raise NotImplementedError
@abstractmethod
async def count_users(self) -> int:
"""Return total number of registered users."""
raise NotImplementedError
@abstractmethod
async def count_admin_users(self) -> int:
"""Return number of users with system_role == 'admin'."""
raise NotImplementedError
@abstractmethod
async def get_user_by_oauth(self, provider: str, oauth_id: str) -> User | None:
"""Get user by OAuth provider and ID.
Args:
provider: OAuth provider name (e.g. 'github', 'google')
oauth_id: User ID from the OAuth provider
Returns:
User if found, None otherwise
"""
raise NotImplementedError