diff --git a/infrastructure.md b/infrastructure.md index 18238f8..b36abea 100644 --- a/infrastructure.md +++ b/infrastructure.md @@ -91,6 +91,14 @@ conda activate AniWorld - **python-jose**: JWT token handling - **bcrypt**: Secure password hashing +### Authentication Models & Sessions + +- Authentication request/response Pydantic models live in `src/server/models/auth.py`. +- Sessions are represented by `SessionModel` and can be backed by an in-memory + store or a persistent table depending on deployment needs. JWTs are used for + stateless authentication by default; a persistent session store may be + configured in production to enable revocation and long-lived sessions. + ## Configuration ### Data Storage diff --git a/instructions.md b/instructions.md index 0a20f8d..1e0a8a4 100644 --- a/instructions.md +++ b/instructions.md @@ -45,12 +45,12 @@ The tasks should be completed in the following order to ensure proper dependenci ### 2. Authentication System -#### [] Implement authentication models +#### [x] Implement authentication models -- []Create `src/server/models/auth.py` -- []Define LoginRequest, LoginResponse models -- []Add SetupRequest, AuthStatus models -- []Include session management models +- [x]Create `src/server/models/auth.py` +- [x]Define LoginRequest, LoginResponse models +- [x]Add SetupRequest, AuthStatus models +- [x]Include session management models #### [] Create authentication service diff --git a/src/server/models/__init__.py b/src/server/models/__init__.py new file mode 100644 index 0000000..4dc9ff0 --- /dev/null +++ b/src/server/models/__init__.py @@ -0,0 +1,3 @@ +"""Models package for server-side Pydantic models.""" + +__all__ = ["auth"] diff --git a/src/server/models/auth.py b/src/server/models/auth.py new file mode 100644 index 0000000..2bccefa --- /dev/null +++ b/src/server/models/auth.py @@ -0,0 +1,57 @@ +"""Authentication Pydantic models for the Aniworld web application. + +This module defines simple request/response shapes used by the auth API and +by the authentication service. Keep models small and focused so they are +easy to validate and test. +""" +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field, constr + + +class LoginRequest(BaseModel): + """Request body for a login attempt. + + Fields: + - password: master password string (minimum 8 chars recommended) + - remember: optional flag to request a long-lived session + """ + + password: constr(min_length=1) = Field(..., description="Master password") + remember: Optional[bool] = Field(False, description="Keep session alive") + + +class LoginResponse(BaseModel): + """Response returned after a successful login.""" + + access_token: str = Field(..., description="JWT access token") + token_type: str = Field("bearer", description="Token type") + expires_at: Optional[datetime] = Field(None, description="Optional expiry timestamp") + + +class SetupRequest(BaseModel): + """Request to initialize the master password during first-time setup.""" + + master_password: constr(min_length=8) = Field(..., description="New master password") + + +class AuthStatus(BaseModel): + """Public status about whether auth is configured and the current user state.""" + + configured: bool = Field(..., description="Whether a master password is set") + authenticated: bool = Field(False, description="Whether the caller is authenticated") + + +class SessionModel(BaseModel): + """Lightweight session representation stored/returned by the auth service. + + This model can be persisted if a persistent session store is used. + """ + + session_id: str = Field(..., description="Unique session identifier") + user: Optional[str] = Field(None, description="Username or identifier") + created_at: datetime = Field(default_factory=datetime.utcnow) + expires_at: Optional[datetime] = Field(None) diff --git a/tests/unit/test_auth_models.py b/tests/unit/test_auth_models.py new file mode 100644 index 0000000..54bb31b --- /dev/null +++ b/tests/unit/test_auth_models.py @@ -0,0 +1,46 @@ +from datetime import datetime, timedelta + +import pytest + +from server.models.auth import ( + AuthStatus, + LoginRequest, + LoginResponse, + SessionModel, + SetupRequest, +) + + +def test_login_request_validation(): + # password is required + with pytest.raises(ValueError): + LoginRequest(password="") + + req = LoginRequest(password="hunter2", remember=True) + assert req.password == "hunter2" + assert req.remember is True + + +def test_setup_request_requires_min_length(): + with pytest.raises(ValueError): + SetupRequest(master_password="short") + + good = SetupRequest(master_password="longenoughpassword") + assert good.master_password == "longenoughpassword" + + +def test_login_response_and_session_model(): + expires = datetime.utcnow() + timedelta(hours=1) + lr = LoginResponse(access_token="tok", expires_at=expires) + assert lr.token_type == "bearer" + assert lr.access_token == "tok" + + s = SessionModel(session_id="abc123", user="admin", expires_at=expires) + assert s.session_id == "abc123" + assert s.user == "admin" + + +def test_auth_status_defaults(): + status = AuthStatus(configured=False, authenticated=False) + assert status.configured is False + assert status.authenticated is False