Files
signal-platform/app/services/auth_service.py
T
dennisthiessen 437ceacfc1
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 27s
refactor: dedupe scheduler logging/runtime, centralize SystemSetting access, fix rankings N+1
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.
2026-06-24 11:23:39 +02:00

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