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

39
src/forms/login.py Normal file
View File

@@ -0,0 +1,39 @@
"""Login form for admin authentication."""
from flask_wtf import FlaskForm
from wtforms import BooleanField, EmailField, PasswordField
from wtforms.validators import DataRequired, Email
class LoginForm(FlaskForm):
"""Form for admin login.
Validates email format and requires password.
Attributes:
email: Email address for admin account.
password: Password for admin account.
remember_me: Whether to extend session duration.
"""
email = EmailField(
"Email Address",
validators=[
DataRequired(message="Email address is required."),
Email(message="Please enter a valid email address."),
],
render_kw={"placeholder": "admin@example.com"},
)
password = PasswordField(
"Password",
validators=[
DataRequired(message="Password is required."),
],
render_kw={"placeholder": "Enter your password"},
)
remember_me = BooleanField(
"Remember me",
default=False,
)