feat(auth): add Pydantic auth models and unit tests; update docs
This commit is contained in:
parent
539dd80e14
commit
92217301b5
@ -91,6 +91,14 @@ conda activate AniWorld
|
|||||||
- **python-jose**: JWT token handling
|
- **python-jose**: JWT token handling
|
||||||
- **bcrypt**: Secure password hashing
|
- **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
|
## Configuration
|
||||||
|
|
||||||
### Data Storage
|
### Data Storage
|
||||||
|
|||||||
@ -45,12 +45,12 @@ The tasks should be completed in the following order to ensure proper dependenci
|
|||||||
|
|
||||||
### 2. Authentication System
|
### 2. Authentication System
|
||||||
|
|
||||||
#### [] Implement authentication models
|
#### [x] Implement authentication models
|
||||||
|
|
||||||
- []Create `src/server/models/auth.py`
|
- [x]Create `src/server/models/auth.py`
|
||||||
- []Define LoginRequest, LoginResponse models
|
- [x]Define LoginRequest, LoginResponse models
|
||||||
- []Add SetupRequest, AuthStatus models
|
- [x]Add SetupRequest, AuthStatus models
|
||||||
- []Include session management models
|
- [x]Include session management models
|
||||||
|
|
||||||
#### [] Create authentication service
|
#### [] Create authentication service
|
||||||
|
|
||||||
|
|||||||
3
src/server/models/__init__.py
Normal file
3
src/server/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Models package for server-side Pydantic models."""
|
||||||
|
|
||||||
|
__all__ = ["auth"]
|
||||||
57
src/server/models/auth.py
Normal file
57
src/server/models/auth.py
Normal file
@ -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)
|
||||||
46
tests/unit/test_auth_models.py
Normal file
46
tests/unit/test_auth_models.py
Normal file
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user