feat: implement admin login

Implement Story 1.2 (Admin Login) with full TDD approach including:

- RateLimit model for tracking authentication attempts
- LoginForm for admin authentication with email, password, and remember_me fields
- Rate limiting utility functions (check, increment, reset)
- admin_required decorator for route protection
- Login route with rate limiting (5 attempts per 15 minutes)
- Logout route with session clearing
- Admin dashboard now requires authentication
- Login template with flash message support
- 14 comprehensive integration tests covering all acceptance criteria
- Email normalization to lowercase
- Session persistence with configurable duration (7 or 30 days)

All acceptance criteria met:
- Login form accepts email and password
- Invalid credentials show appropriate error message
- Successful login redirects to admin dashboard
- Session persists across browser refreshes
- Rate limiting after 5 failed attempts

Test coverage: 90.67% (exceeds 80% requirement)
All linting and type checking passes

Story: 1.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-22 11:53:27 -07:00
parent 6a2ac7a8a7
commit 6764455703
12 changed files with 716 additions and 4 deletions

View File

@@ -5,5 +5,6 @@ This package contains all database models used by the application.
from src.models.admin import Admin
from src.models.exchange import Exchange
from src.models.rate_limit import RateLimit
__all__ = ["Admin", "Exchange"]
__all__ = ["Admin", "Exchange", "RateLimit"]

42
src/models/rate_limit.py Normal file
View File

@@ -0,0 +1,42 @@
"""Rate limiting model for Sneaky Klaus.
The RateLimit model tracks authentication attempts to prevent brute force attacks.
"""
from datetime import datetime
from sqlalchemy import DateTime, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from src.app import db
class RateLimit(db.Model): # type: ignore[name-defined]
"""Rate limiting for authentication attempts.
Tracks attempts per key (email/IP) within a time window
to prevent brute force attacks.
Attributes:
id: Auto-increment primary key.
key: Rate limit identifier (e.g., "login:admin:user@example.com").
attempts: Number of attempts in current window.
window_start: Start of current rate limit window.
expires_at: When rate limit resets.
"""
__tablename__ = "rate_limit"
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(
String(255), unique=True, nullable=False, index=True
)
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
window_start: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=datetime.utcnow
)
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
def __repr__(self) -> str:
"""String representation of RateLimit instance."""
return f"<RateLimit {self.key} ({self.attempts} attempts)>"