feat: implement Stories 5.2 & 5.3 - Magic Link Login and Participant Session
Implemented complete participant authentication flow with magic link login and session management. Story 5.2 - Magic Link Login: - Participants can click magic links to securely access their dashboard - Single-use tokens that expire after 1 hour - Session creation with participant_id, user_type, and exchange_id - Error handling for expired, used, or invalid tokens - Fixed timezone-aware datetime comparison for SQLite compatibility Story 5.3 - Participant Session: - Authenticated participants can access their exchange dashboard - participant_required decorator protects participant-only routes - Participants can only access their own exchange (403 for others) - Logout functionality clears session and redirects appropriately - Unauthenticated access returns 403 Forbidden Technical changes: - Added magic_login() route for token validation and session creation - Added dashboard() route with exchange and participant data - Added logout() route with smart redirect to request access page - Added participant_required decorator for route protection - Enhanced MagicToken.is_expired for timezone-naive datetime handling - Added participant.logout to setup check exclusions - Created templates: dashboard.html, magic_link_error.html, 403.html - Comprehensive test coverage for all user flows Acceptance Criteria Met: ✓ Valid magic links create authenticated sessions ✓ Invalid/expired/used tokens show appropriate errors ✓ Authenticated participants see their dashboard ✓ Participants cannot access other exchanges ✓ Unauthenticated users cannot access protected routes ✓ Logout clears session and provides feedback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,16 @@ class MagicToken(db.Model): # type: ignore[name-defined]
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if token has expired."""
|
||||
return bool(datetime.now(UTC) > self.expires_at)
|
||||
# Handle both timezone-aware and timezone-naive datetimes
|
||||
# SQLite stores datetimes as strings and may lose timezone info
|
||||
now = datetime.now(UTC)
|
||||
expires = self.expires_at
|
||||
|
||||
# If expires_at is timezone-naive, make now timezone-naive for comparison
|
||||
if expires.tzinfo is None:
|
||||
now = now.replace(tzinfo=None)
|
||||
|
||||
return bool(now > expires)
|
||||
|
||||
@property
|
||||
def is_used(self) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user