PromptBase
Upgrade
Close icon
General
Home
Marketplace
Create
Hire
Login
Chat
Sell
Explore

Prompt Details

Model
(claude-4-6-opus)
Token size
933
Example input
[USER_STORY]: Add Email Verification β€” Users must verify their email before logging in. Verification link expires in 24h. Unverified users are blocked from login. [TECH_STACK]: Python, Django, PostgreSQL, Redis, deployed on AWS ECS [CODEBASE_CONTEXT]: Monolith, service layer pattern, Celery for async tasks, no custom auth β€” using Django built-in auth [DETAIL_LEVEL]: Mid-level
Example output
# Implementation Plan: Email Verification ## 1. Business Objective Email verification ensures only users with valid, owned email addresses can access the system β€” reducing fake accounts, improving deliverability for transactional emails, and meeting baseline security expectations. The core risk is friction: a broken or delayed verification email means a lost user. Secondary risk is locking out existing users if migration isn't handled carefully. ## 2. Requirements **Functional:** - On registration, send a verification email with a unique, signed link - Verification link expires after 24 hours - Unverified users are blocked at login with a clear message - Users can request a new verification email (INFERRED β€” without this, expired links create a dead end) - Clicking a valid link marks the user as verified and redirects to login (INFERRED) **Non-Functional:** - Email sending must be async (Celery) β€” registration response time must not depend on SMTP - Verification tokens must be tamper-proof (signed, not just random UUIDs in a table) - Rate-limit resend requests to prevent abuse (INFERRED β€” 3 per hour per email) ## 3. Architecture Approach Extend the existing Django auth `User` model via a one-to-one profile or, more cleanly, add a field to whatever user model is in use. Use Django's `TimestampSigner` for tokens rather than storing tokens in the database β€” this eliminates a table, avoids cleanup jobs, and embeds expiration in the signature itself. The verification flow is: register β†’ Celery task sends email β†’ user clicks link β†’ view validates signature β†’ sets `is_email_verified = True`. **Tradeoffs:** Signed tokens mean you can't "revoke" a specific token (e.g., invalidate old link when user requests a new one) β€” but for email verification this is acceptable since verifying twice is a no-op. If you needed single-use tokens, you'd need a database-backed token model with an `is_used` flag, adding a table and a cleanup task. The signer approach buys simplicity and zero DB overhead at the cost of that revocation capability. ## 4. Impacted Layers | Layer | Components | |---|---| | **Model** | `User` model β€” add `is_email_verified` field, migration | | **Service** | New `verification_service.py` β€” token generation, validation, resend logic | | **Tasks** | New Celery task `send_verification_email` | | **Views/API** | Modify registration view, new `verify-email` endpoint, new `resend-verification` endpoint, modify login view | | **Templates** | Verification email template (HTML + plain text), login error messaging | | **Middleware** | None β€” enforcement at the login view is sufficient | ## 5. Design Patterns | Pattern | Where | Why | Tradeoff | |---|---|---|---| | Service Layer | `verification_service.py` | Keeps token logic out of views; testable in isolation | Extra indirection for what starts as simple logic | | Signed Tokens (HMAC) | Token generation/validation | Stateless, no DB table, tamper-proof, expiration built-in | Can't revoke individual tokens | | Async Task (Celery) | Email dispatch | Registration isn't blocked by SMTP latency | Email delivery is eventual; user might check inbox before it arrives | | Template Method | Email rendering | Django's `render_to_string` for both HTML and text variants | Must maintain two templates | The service layer generates signed tokens and validates them; views delegate to the service and handle HTTP concerns only. The Celery task is fired from the service after registration, decoupling email delivery. This keeps the registration view thin β€” it calls `create_user` then `send_verification_email.delay()`, and returns immediately. ## 6. Data Model Changes ```python # In your accounts/models.py (or wherever User is defined) # If using default auth.User, switch to a custom user model or use a profile. # Assuming you have a custom User model already (AbstractUser): from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): # ... existing fields ... is_email_verified = models.BooleanField(default=False) ``` **Migration:** ```python # accounts/migrations/XXXX_add_is_email_verified.py # Auto-generated, but the critical decision: from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('accounts', 'XXXX_previous'), ] operations = [ migrations.AddField( model_name='user', name='is_email_verified', field=models.BooleanField(default=False), ), ] ``` **Backward compatibility:** `default=False` means existing users are unverified. You need a **data migration** to set `is_email_verified=True` for all existing users β€” otherwise you lock out your entire user base on deploy. ```python def backfill_existing_users(apps, schema_editor): User = apps.get_model('accounts', 'User') User.objects.filter(is_active=True).update(is_email_verified=True) ``` ## 7. Implementation Steps **Step 1 β€” Add `is_email_verified` field and migrations** `BLOCKER` ```python # accounts/models.py class User(AbstractUser): is_email_verified = models.BooleanField(default=False) # Then: # python manage.py makemigrations accounts # Create a second migration for the backfill: from django.db import migrations def backfill(apps, schema_editor): User = apps.get_model('accounts', 'User') User.objects.filter(is_active=True).update(is_email_verified=True) class Migration(migrations.Migration): dependencies = [ ('accounts', 'XXXX_add_is_email_verified'), ] operations = [ migrations.RunPython(backfill, migrations.RunPython.noop), ] ``` **Step 2 β€” Build verification service** `BLOCKER` ```python # accounts/services/verification_service.py from django.core.signing import TimestampSigner, SignatureExpired, BadSignature from django.conf import settings from datetime import timedelta VERIFICATION_TIMEOUT = timedelta(hours=24) signer = TimestampSigner(salt='email-verification') def generate_verification_token(user_id: int) -> str: return signer.sign(str(user_id)) def verify_token(token: str) -> int: """Returns user_id or raises.""" try: user_id_str = signer.unsign( token, max_age=VERIFICATION_TIMEOUT.total_seconds() ) return int(user_id_str) except SignatureExpired: raise ValueError("Verification link has expired.") except BadSignature: raise ValueError("Invalid verification link.") def build_verification_url(user_id: int, base_url: str) -> str: token = generate_verification_token(user_id) return f"{base_url}/accounts/verify-email/?token={token}" ``` **Step 3 β€” Create Celery task for sending email** ```python # accounts/tasks.py from celery import shared_task from django.core.mail import send_mail from django.template.loader import render_to_string from django.conf import settings from accounts.services.verification_service import build_verification_url @shared_task(bind=True, max_retries=3, default_retry_delay=60) def send_verification_email(self, user_id: int, email: str, base_url: str): try: url = build_verification_url(user_id, base_url) html = render_to_string('accounts/emails/verify_email.html', {'url': url}) text = render_to_string('accounts/emails/verify_email.txt', {'url': url}) send_mail( subject='Verify your email address', message=text, html_message=html, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[email], fail_silently=False, ) except Exception as exc: raise self.retry(exc=exc) ``` **Step 4 β€” Modify registration view to trigger verification** `RISK: must get base_url right across environments` ```python # In your registration view, after user creation: from accounts.tasks import send_verification_email def register_view(request): if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): user = form.save(commit=False) user.is_email_verified = False user.save() base_url = request.build_absolute_uri('/')[:-1] # e.g. https://example.com send_verification_email.delay(user.id, user.email, base_url) return render(request, 'accounts/registration_pending.html') # ... GET handling ``` **Step 5 β€” Create verify-email endpoint** ```python # accounts/views.py from django.shortcuts import redirect, render from django.contrib.auth import get_user_model from accounts.services.verification_service import verify_token from django.contrib import messages User = get_user_model() def verify_email_view(request): token = request.GET.get('token', '') try: user_id = verify_token(token) user = User.objects.get(pk=user_id) if not user.is_email_verified: user.is_email_verified = True user.save(update_fields=['is_email_verified']) messages.success(request, 'Email verified. You can now log in.') return redirect('login') except (ValueError, User.DoesNotExist) as e: return render(request, 'accounts/verification_failed.html', { 'error': str(e), }, status=400) ``` **Step 6 β€” Block unverified users at login** ```python # accounts/backends.py or directly in login view # Cleanest approach: custom authentication backend from django.contrib.auth.backends import ModelBackend class VerifiedEmailBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): user = super().authenticate(request, username=username, password=password, **kwargs) if user and not user.is_email_verified: return None # Block login return user ``` ```python # settings.py AUTHENTICATION_BACKENDS = ['accounts.backends.VerifiedEmailBackend'] ``` The tradeoff of returning `None` from the backend: the user sees a generic "invalid credentials" message. To give a specific "please verify your email" message, intercept in the login view instead: ```python # Alternative: override login view from django.contrib.auth import authenticate def login_view(request): if request.method == 'POST': user = authenticate(request, username=..., password=...) # use default backend if user is None: # invalid credentials ... elif not user.is_email_verified: messages.error(request, 'Please verify your email before logging in.') return render(request, 'accounts/login.html') else: login(request, user) return redirect('home') ``` Pick the login view approach β€” it gives a better UX. Keep the default `ModelBackend`. **Step 7 β€” Add resend-verification endpoint with rate limiting** ```python # accounts/views.py from django.core.cache import cache # backed by Redis def resend_verification_view(request): if request.method == 'POST': email = request.POST.get('email', '').lower().strip() cache_key = f'resend_verification:{email}' count = cache.get(cache_key, 0) if count >= 3: messages.error(request, 'Too many requests. Try again later.') return render(request, 'accounts/resend_verification.html', status=429) try: user = User.objects.get(email=email, is_email_verified=False) base_url = request.build_absolute_uri('/')[:-1] send_verification_email.delay(user.id, user.email, base_url) except User.DoesNotExist: pass # Don't reveal whether email exists cache.set(cache_key, count + 1, timeout=3600) messages.info(request, 'If that email is registered, a verification link has been sent.') return render(request, 'accounts/resend_verification.html') return render(request, 'accounts/resend_verification.html') ``` **Step 8 β€” Wire URLs** ```python # accounts/urls.py from accounts.views import verify_email_view, resend_verification_view urlpatterns = [ # ... existing path('verify-email/', verify_email_view, name='verify-email'), path('resend-verification/', resend_verification_view, name='resend-verification'), ] ``` **Step 9 β€” Create email and page templates** Four templates: `verify_email.html`, `verify_email.txt`, `registration_pending.html`, `verification_failed.html`. Standard Django template work β€” no architectural decisions here. ## 8. Effort Estimate | Step | Size | Uncertainty | |---|---|---| | 1. Model + migrations + backfill | S | Low | | 2. Verification service | S | Low | | 3. Celery task | S | Low | | 4. Modify registration view | S | Low | | 5. Verify-email endpoint | S | Low | | 6. Login blocking | S | Low | | 7. Resend with rate limit | M | Low | | 8. URL wiring | S | Low | | 9. Templates | M | Low | | Testing (all) | L | Medium | | **Total** | **~3–4 days** | | ## 9. Validation and Error Handling **Validations:** email format (Django handles), token signature integrity (TimestampSigner), rate limit on resend (Redis counter), `update_fields` on save to avoid race conditions. **User-facing errors:** | Scenario | Response | Status | |---|---|---| | Unverified user tries to login | "Please verify your email before logging in." + resend link | 200 (re-render form) | | Expired verification link | "Link has expired. Request a new one." + resend link | 400 | | Tampered/invalid token | "Invalid verification link." | 400 | | Resend rate limit exceeded | "Too many requests. Try again later." | 429 | **System errors:** | Scenario | Response | Status | |---|---|---| | SMTP failure | Celery retries 3x, then task fails silently; user can resend | N/A (async) | | Redis unavailable (rate limit) | Fail open β€” allow the resend, log error | 200 | ## 10. Testing Strategy **Critical path (integration test):** ```python # accounts/tests/test_verification_flow.py from django.test import TestCase, override_settings from django.contrib.auth import get_user_model from accounts.services.verification_service import generate_verification_token User = get_user_model() @override_settings(CELERY_TASK_ALWAYS_EAGER=True) class TestVerificationFlow(TestCase): def test_full_flow(self): # Register resp = self.client.post('/accounts/register/', { 'username': 'testuser', 'email': 'test@example.com', 'password1': 'Str0ngP@ss!', 'password2': 'Str0ngP@ss!', }) user = User.objects.get(username='testuser') self.assertFalse(user.is_email_verified) # Login blocked resp = self.client.post('/accounts/login/', { 'username': 'testuser', 'password': 'Str0ngP@ss!', }) self.assertContains(resp, 'verify your email') # Verify token = generate_verification_token(user.id) resp = self.client.get(f'/accounts/verify-email/?token={token}') user.refresh_from_db() self.assertTrue(user.is_email_verified) # Login works self.assertTrue(self.client.login(username='testuser', password='Str0ngP@ss!')) ``` **Additional tests:** - *Unit:* `verify_token` rejects expired tokens (mock time), rejects tampered tokens, returns correct user_id - *Unit:* Resend rate limiting β€” 4th request in same hour returns 429 - *Unit:* Resend for unknown email returns same success message (no enumeration) - *Integration:* Celery task retries on SMTP failure (`@override_settings` with broken email backend) - *Edge:* User clicks verification link twice β€” second click is a no-op, still redirects to login with success - *Edge:* Backfill migration β€” existing users have `is_email_verified=True` ## 11. Risks and Mitigation | Risk | Likelihood | Impact | Mitigation | Step | |---|---|---|---|---| | Existing users locked out | High (if backfill missed) | Critical | Data migration backfills all active users to verified; deploy migration before code | 1 | | Emails land in spam | Medium | High | Use verified SES domain, proper SPF/DKIM; plain text fallback | 3 | | `base_url` wrong behind ALB/proxy | Medium | High | Use `request.build_absolute_uri` + ensure `SECURE_PROXY_SSL_HEADER` and `USE_X_FORWARDED_HOST` are set | 4 | | TimestampSigner breaks on SECRET_KEY rotation | Low | High | Document: rotating SECRET_KEY invalidates all outstanding links; users can resend | 2 | ## 12. Refactoring Opportunities - **Now:** If the project still uses `auth.User` directly (no custom user model), you must switch to a custom user model. This is a significant migration but is a one-time cost that Django strongly recommends doing early. - **Later:** Extract email sending into a generic `notifications` service if other transactional emails exist or are planned. - **Later:** Replace `request.build_absolute_uri` with a `SITE_BASE_URL` setting to avoid proxy-related bugs and make Celery tasks independent of request context. ## 13. Deployment Considerations **Sequence matters:** 1. **Deploy migration first** (can be done independently β€” adding a boolean column with a default is safe, backfill is idempotent). Run via ECS one-off task or pre-deploy hook. 2. **Deploy code behind a feature flag** (Django Waffle or a simple settings boolean `EMAIL_VERIFICATION_ENABLED`). Login check becomes: ```python if settings.EMAIL_VERIFICATION_ENABLED and not user.is_email_verified: # block ``` 3. **Phase 1:** Flag off. Deploy everything. Verify Celery task works by registering a test user in staging. 4. **Phase 2:** Flag on. New registrations require verification. Existing users (backfilled as verified) are unaffected. 5. **Phase 3:** Remove flag, clean up conditional logic. **Configuration:** Ensure `DEFAULT_FROM_EMAIL`, SES credentials, and Redis cache backend are configured in ECS task definitions / environment variables. No new infrastructure β€” Celery and Redis already exist.
πŸŒ€ Claude

User Story Implementation Planner

Add to Cart
Instant accessInstant access
Usage rightsCommercial use
Money-back guaranteeMoney‑back
By purchasing this prompt, you agree to our terms of service
CLAUDE-4-6-OPUS
Tested icon
Guide icon
4 examples icon
Free credits icon
πŸ—οΈ Turn any user story into a full implementation plan β€” for any tech stack. βœ… Architecture approach with tradeoffs analysis βœ… Step-by-step plan with blockers and risk flags βœ… Effort estimates, testing strategy, and deployment rollout βœ… Code snippets adapted to your language and framework πŸ’‘ Great for: feature planning, sprint prep, technical design reviews, onboarding senior devs, consulting deliverables, architecture documentation πŸ“ Instructions and tips included.
...more
Added 3 weeks ago
Report
Browse Marketplace