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
co-authored by Claude
parent 6a2ac7a8a7
commit 6764455703
12 changed files with 716 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
"""Decorators for Sneaky Klaus application."""
from src.decorators.auth import admin_required
__all__ = ["admin_required"]
+28
View File
@@ -0,0 +1,28 @@
"""Authentication decorators for route protection."""
from functools import wraps
from flask import flash, redirect, session, url_for
def admin_required(f):
"""Decorator to require admin authentication for a route.
Checks if user is logged in as admin. If not, redirects to login page
with appropriate flash message.
Args:
f: The function to decorate.
Returns:
Decorated function that checks authentication.
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if "admin_id" not in session:
flash("You must be logged in as admin to access this page.", "error")
return redirect(url_for("admin.login"))
return f(*args, **kwargs)
return decorated_function