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:
@@ -0,0 +1,9 @@
|
||||
"""Utility functions for Sneaky Klaus application."""
|
||||
|
||||
from src.utils.rate_limit import (
|
||||
check_rate_limit,
|
||||
increment_rate_limit,
|
||||
reset_rate_limit,
|
||||
)
|
||||
|
||||
__all__ = ["check_rate_limit", "increment_rate_limit", "reset_rate_limit"]
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Rate limiting utilities for authentication."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.app import db
|
||||
from src.models import RateLimit
|
||||
|
||||
|
||||
def check_rate_limit(key: str, max_attempts: int, window_minutes: int) -> bool:
|
||||
"""Check if rate limit has been exceeded.
|
||||
|
||||
Args:
|
||||
key: Rate limit key (e.g., "login:admin:user@example.com").
|
||||
max_attempts: Maximum allowed attempts within window.
|
||||
window_minutes: Time window in minutes.
|
||||
|
||||
Returns:
|
||||
True if rate limit exceeded, False otherwise.
|
||||
"""
|
||||
rate_limit = db.session.query(RateLimit).filter_by(key=key).first()
|
||||
|
||||
if not rate_limit:
|
||||
# No rate limit record exists yet
|
||||
return False
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Check if rate limit window has expired
|
||||
if rate_limit.expires_at <= now:
|
||||
# Window expired, reset
|
||||
rate_limit.attempts = 0
|
||||
rate_limit.window_start = now
|
||||
rate_limit.expires_at = now + timedelta(minutes=window_minutes)
|
||||
db.session.commit()
|
||||
return False
|
||||
|
||||
# Check if attempts exceeded
|
||||
return bool(rate_limit.attempts >= max_attempts)
|
||||
|
||||
|
||||
def increment_rate_limit(key: str, window_minutes: int) -> None:
|
||||
"""Increment rate limit attempt counter.
|
||||
|
||||
Args:
|
||||
key: Rate limit key (e.g., "login:admin:user@example.com").
|
||||
window_minutes: Time window in minutes.
|
||||
"""
|
||||
rate_limit = db.session.query(RateLimit).filter_by(key=key).first()
|
||||
now = datetime.utcnow()
|
||||
|
||||
if not rate_limit:
|
||||
# Create new rate limit record
|
||||
rate_limit = RateLimit(
|
||||
key=key,
|
||||
attempts=1,
|
||||
window_start=now,
|
||||
expires_at=now + timedelta(minutes=window_minutes),
|
||||
)
|
||||
db.session.add(rate_limit)
|
||||
else:
|
||||
# Check if window expired
|
||||
if rate_limit.expires_at <= now:
|
||||
# Reset window
|
||||
rate_limit.attempts = 1
|
||||
rate_limit.window_start = now
|
||||
rate_limit.expires_at = now + timedelta(minutes=window_minutes)
|
||||
else:
|
||||
# Increment attempts
|
||||
rate_limit.attempts += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def reset_rate_limit(key: str) -> None:
|
||||
"""Reset rate limit counter for a key.
|
||||
|
||||
Args:
|
||||
key: Rate limit key (e.g., "login:admin:user@example.com").
|
||||
"""
|
||||
rate_limit = db.session.query(RateLimit).filter_by(key=key).first()
|
||||
|
||||
if rate_limit:
|
||||
rate_limit.attempts = 0
|
||||
db.session.commit()
|
||||
Reference in New Issue
Block a user