Implement security hardening, notification pipeline, and CI test suite
All checks were successful
CI / test (push) Successful in 9m2s

Security hardening:

- Added IP/user rate limiting with Redis-backed counters and fail-open behavior.

- Added message anti-spam controls (per-chat rate + duplicate cooldown).

- Implemented refresh token rotation with JTI tracking and revoke support.

Notification pipeline:

- Added Celery app and async notification tasks for mention/offline delivery.

- Added Redis-based presence tracking and integrated it into realtime connect/disconnect.

- Added notification dispatch from message flow and notifications listing endpoint.

Quality gates and CI:

- Added pytest async integration tests for auth and chat/message lifecycle.

- Added pytest config, test fixtures, and GitHub Actions CI workflow.

- Fixed bcrypt/passlib compatibility by pinning bcrypt version.

- Documented worker and quality-gate commands in README.
This commit is contained in:
2026-03-07 21:46:30 +03:00
parent a879ba7b50
commit 85631b566a
29 changed files with 723 additions and 11 deletions

View File

@@ -1,12 +1,15 @@
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import repository as auth_repository
from app.auth.token_store import get_refresh_token_user_id, revoke_refresh_token_jti, store_refresh_token_jti
from app.auth.schemas import (
LoginRequest,
RefreshTokenRequest,
RegisterRequest,
RequestPasswordResetRequest,
ResendVerificationRequest,
@@ -31,6 +34,10 @@ from app.utils.security import (
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api_v1_prefix}/auth/login")
def _refresh_ttl_seconds() -> int:
return settings.refresh_token_expire_days * 24 * 60 * 60
async def register_user(
db: AsyncSession,
payload: RegisterRequest,
@@ -105,9 +112,46 @@ async def login_user(db: AsyncSession, payload: LoginRequest) -> TokenResponse:
if not user.email_verified:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Email not verified")
refresh_jti = str(uuid4())
refresh_token = create_refresh_token(str(user.id), jti=refresh_jti)
await store_refresh_token_jti(user_id=user.id, jti=refresh_jti, ttl_seconds=_refresh_ttl_seconds())
return TokenResponse(
access_token=create_access_token(str(user.id)),
refresh_token=create_refresh_token(str(user.id)),
refresh_token=refresh_token,
)
async def refresh_tokens(db: AsyncSession, payload: RefreshTokenRequest) -> TokenResponse:
credentials_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
)
try:
token_payload = decode_token(payload.refresh_token)
except ValueError as exc:
raise credentials_error from exc
if token_payload.get("type") != "refresh":
raise credentials_error
user_id = token_payload.get("sub")
refresh_jti = token_payload.get("jti")
if not user_id or not str(user_id).isdigit() or not refresh_jti:
raise credentials_error
active_user_id = await get_refresh_token_user_id(jti=refresh_jti)
if active_user_id is None or active_user_id != int(user_id):
raise credentials_error
user = await get_user_by_id(db, int(user_id))
if not user:
raise credentials_error
await revoke_refresh_token_jti(jti=refresh_jti)
new_jti = str(uuid4())
await store_refresh_token_jti(user_id=int(user_id), jti=new_jti, ttl_seconds=_refresh_ttl_seconds())
return TokenResponse(
access_token=create_access_token(str(user_id)),
refresh_token=create_refresh_token(str(user_id), jti=new_jti),
)