# RTSport Auth Layer Implementation Summary **Completed:** 2026-05-02 **Owner:** Socrates (Backend) ## What Was Implemented ### 1. User Model (`app/models.py`) ```python class User(Base): id = Column(String, primary_key=True) school_id = Column(String, ForeignKey("schools.id"), nullable=False) email = Column(String(255), unique=True, nullable=False) hashed_password = Column(String(255), nullable=False) role = Column(String(20), nullable=False) # "at" | "coach" | "parent" | "admin" first_name = Column(String(100)) last_name = Column(String(100)) is_active = Column(Boolean, default=True) created_at = Column(DateTime) updated_at = Column(DateTime) ``` ### 2. Auth Module (`app/auth.py`) | Function | Description | |----------|-------------| | `hash_password(password)` | Bcrypt hash generation | | `verify_password(plain, hashed)` | Bcrypt verification | | `create_access_token(user_id, role, school_id)` | JWT token creation | | `verify_token(token)` | JWT validation and decoding | | `get_current_user(token)` | FastAPI dependency - returns User model | | `require_role(roles)` | FastAPI dependency factory - role checking | ### 3. Login Endpoint (`app/api/auth.py`) | Endpoint | Method | Auth Required | Returns | |----------|--------|---------------|---------| | `/api/v1/auth/login` | POST | No | `{access_token, token_type, role, school_id}` | | `/api/v1/auth/login/json` | POST | No | Same as above (JSON variant) | | `/api/v1/auth/me` | GET | Yes | Current user info | ### 4. Updated Endpoints All endpoints now require JWT authentication via `Authorization: Bearer ` header. | Endpoint | Required Role | Tenant Isolation | |----------|---------------|------------------| | GET /roster | at, coach, admin | school_id matches user | | POST /roster | at, admin | school_id matches user | | PATCH /roster/{id} | at, admin | school_id matches user | | DELETE /roster/{id} | at, admin | school_id matches user | | GET /cases/athlete/{id} | at, coach, parent, admin | school_id + parent linking | | GET /cases/{id}/timeline | at, coach, parent, admin | school_id + parent linking | | GET /cases/{id}/milestones | at, coach, parent, admin | school_id + parent linking | | POST /cases | at, admin | school_id matches user | | PATCH /cases/{id} | at, admin | school_id matches user | | DELETE /cases/{id} | at, admin | school_id matches user | | POST /events/sideline-entry | at, admin | school_id matches user | | POST /events/general | at, admin | school_id matches user | ### 5. Parent Linking In `cases.py`, parent access is filtered via: ```python if role == "parent": if user_id not in (athlete.parent_ids or []): raise HTTPException(403, "Parents can only view their own child's cases") ``` ### 6. Dependencies Added (`requirements.txt`) ``` python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 ``` ## JWT Token Format **Header:** ```json { "alg": "HS256", "typ": "JWT" } ``` **Payload:** ```json { "sub": "usr_001", "role": "at", "school_id": "schl_001", "exp": 1719878400, "iat": 1719874800, "type": "access" } ``` ## Testing See `backend/tests/generate_test_data.py` for: - Test user credentials - SQL INSERT statements - cURL examples - Role access matrix ## Files Created/Modified | File | Change | |------|--------| | `app/auth.py` | **NEW** - JWT and auth utilities | | `app/api/auth.py` | **NEW** - Login endpoints | | `app/models.py` | **MODIFIED** - Added User model | | `app/api/roster.py` | **MODIFIED** - JWT auth + school isolation | | `app/api/cases.py` | **MODIFIED** - JWT auth + parent linking | | `app/api/events.py` | **MODIFIED** - JWT auth + author_id from user | | `app/main.py` | **MODIFIED** - Added auth router | | `requirements.txt` | **MODIFIED** - Added JWT + bcrypt deps | ## Notes - **No refresh tokens** - as specified, out of scope - **Secret key** - Uses `settings.secret_key` from `config.py` (default: "dev-secret-key-change-in-production") - **Token expiry** - 7 days (configurable via `settings.access_token_expire_minutes`) - **Tenant isolation** - All endpoints verify `current_user.school_id == requested_school_id` - **Parent access** - Validated via `athlete.parent_ids.contains(current_user.id)`