modularize service layer, add mobile nav drawer, and sync docs
This commit is contained in:
parent
7f7a8ce2b0
commit
6b6ed91b92
72 changed files with 8122 additions and 7501 deletions
1
app/services/domains/users/__init__.py
Normal file
1
app/services/domains/users/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from __future__ import annotations
|
||||
98
app/services/domains/users/application.py
Normal file
98
app/services/domains/users/application.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import User
|
||||
|
||||
EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
|
||||
|
||||
class UserServiceError(ValueError):
|
||||
"""Raised for expected user-management validation failures."""
|
||||
|
||||
|
||||
def normalize_email(value: str) -> str:
|
||||
return value.strip().lower()
|
||||
|
||||
|
||||
def validate_email(value: str) -> str:
|
||||
email = normalize_email(value)
|
||||
if not EMAIL_PATTERN.fullmatch(email):
|
||||
raise UserServiceError("Enter a valid email address.")
|
||||
return email
|
||||
|
||||
|
||||
def validate_password(value: str) -> str:
|
||||
password = value.strip()
|
||||
if len(password) < 8:
|
||||
raise UserServiceError("Password must be at least 8 characters.")
|
||||
return password
|
||||
|
||||
|
||||
async def get_user_by_id(db_session: AsyncSession, user_id: int) -> User | None:
|
||||
result = await db_session.execute(select(User).where(User.id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_user_by_email(db_session: AsyncSession, email: str) -> User | None:
|
||||
result = await db_session.execute(
|
||||
select(User).where(User.email == normalize_email(email))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_users(db_session: AsyncSession) -> list[User]:
|
||||
result = await db_session.execute(select(User).order_by(User.email.asc()))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def create_user(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
email: str,
|
||||
password_hash: str,
|
||||
is_admin: bool,
|
||||
) -> User:
|
||||
user = User(
|
||||
email=validate_email(email),
|
||||
password_hash=password_hash,
|
||||
is_admin=is_admin,
|
||||
is_active=True,
|
||||
)
|
||||
db_session.add(user)
|
||||
try:
|
||||
await db_session.commit()
|
||||
except IntegrityError as exc:
|
||||
await db_session.rollback()
|
||||
raise UserServiceError("A user with that email already exists.") from exc
|
||||
await db_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
async def set_user_active(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
user: User,
|
||||
is_active: bool,
|
||||
) -> User:
|
||||
user.is_active = is_active
|
||||
await db_session.commit()
|
||||
await db_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
async def set_user_password_hash(
|
||||
db_session: AsyncSession,
|
||||
*,
|
||||
user: User,
|
||||
password_hash: str,
|
||||
) -> User:
|
||||
user.password_hash = password_hash
|
||||
await db_session.commit()
|
||||
await db_session.refresh(user)
|
||||
return user
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue