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:
@@ -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
42
src/models/rate_limit.py
Normal 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)>"
|
||||
Reference in New Issue
Block a user