437ceacfc1
Behavior-preserving cleanup (345 tests pass, ruff clean):
- scheduler: replace 62 inline logger.x(json.dumps({...})) calls with a
_log_event helper, and collapse 11 identical _job_runtime dicts into an
_idle_runtime() factory over _JOB_NAMES.
- settings: add app/services/settings_store.py (get_setting/get_value/get_map/
upsert_setting) and route ~13 hand-rolled SystemSetting queries + two
identical _settings_map helpers through it.
- scoring.get_rankings: collapse the per-ticker N+1 (3-4 queries + a commit each)
into 2 bulk reads + a single conditional commit; drop the redundant re-fetch.
Lazy recompute-on-read is preserved. Adds first tests for get_rankings.
Net ~ -245 lines across the touched modules.
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""Auth service: registration, login, and JWT token generation."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from jose import jwt
|
|
from passlib.hash import bcrypt
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.dependencies import JWT_ALGORITHM
|
|
from app.exceptions import AuthenticationError, AuthorizationError, DuplicateError
|
|
from app.models.user import User
|
|
from app.services import settings_store
|
|
|
|
|
|
async def register(db: AsyncSession, username: str, password: str) -> User:
|
|
"""Register a new user.
|
|
|
|
Checks if registration is enabled via SystemSetting, rejects duplicates,
|
|
and creates a user with role='user' and has_access=False.
|
|
"""
|
|
# Check registration toggle
|
|
setting = await settings_store.get_setting(db, "registration_enabled")
|
|
if setting is not None and setting.value.lower() == "false":
|
|
raise AuthorizationError("Registration is closed")
|
|
|
|
# Check duplicate username
|
|
result = await db.execute(select(User).where(User.username == username))
|
|
if result.scalar_one_or_none() is not None:
|
|
raise DuplicateError(f"Username already exists: {username}")
|
|
|
|
user = User(
|
|
username=username,
|
|
password_hash=bcrypt.hash(password),
|
|
role="user",
|
|
has_access=False,
|
|
)
|
|
db.add(user)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
return user
|
|
|
|
|
|
async def login(db: AsyncSession, username: str, password: str) -> str:
|
|
"""Authenticate user and return a JWT access token.
|
|
|
|
Returns the same error message for wrong username or wrong password
|
|
to avoid leaking which field is incorrect.
|
|
"""
|
|
result = await db.execute(select(User).where(User.username == username))
|
|
user = result.scalar_one_or_none()
|
|
|
|
if user is None or not bcrypt.verify(password, user.password_hash):
|
|
raise AuthenticationError("Invalid credentials")
|
|
|
|
payload = {
|
|
"sub": str(user.id),
|
|
"username": user.username,
|
|
"role": user.role,
|
|
"exp": datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expiry_minutes),
|
|
}
|
|
token = jwt.encode(payload, settings.jwt_secret, algorithm=JWT_ALGORITHM)
|
|
return token
|