Skip to main content

Implementation Guide: C-CONSENT-004 + C-STORE-002

Status: COMPLETE Started: 2026-02-13 Completed: 2026-02-14

Completed

C-CONSENT-004 — COMPLETE (100%) — Session 4 Completed

  • Migration 012: Age verification fields added to users table (birthdate, is_minor, parent_consent_*)

  • auth/age_verification.py: Age calculation service with regional thresholds (EU 16, US 13, etc.)

  • auth/parental_consent.py: Parental consent workflow (request, verify, resend, check_status)

  • Updated auth/models.py: Added birthdate, parent_email, country_code to AuthSignUp

  • Updated auth/models.py: Added parental_consent_pending to AuthResponse

  • Updated auth/service.py: sign_up() calculates age, validates parent_email, sends consent email

  • Updated auth/service.py: sign_in() blocks login for minors without verified parental consent

  • Added API endpoints in api/auth.py:

    • /parent-consent/verify/{token} (public endpoint for parent)
    • /parent-consent/resend (authenticated, for minors)
    • /parent-consent/status (authenticated, check consent status)
  • Created emails/email_service.py: SMTP email sending with HTML parental consent template

  • Updated config.py: Added SMTP configuration (SMTP_HOST, SMTP_PORT, SMTP_USER, etc.)

  • Frontend Implementation (Session 3):

    • Updated AuthForm component with birthdate picker (native HTML date input)
    • Added conditional parent_email field when age < threshold (16 for EU, 13 for US)
    • Added age calculation logic (watches birthdate field)
    • Updated signup flow to include birthdate, parent_email, country_code
    • Added parental_consent_pending response handling
    • Created ParentConsentPending page (shown after minor signup)
    • Created ParentConsentVerify page (parent clicks email link)
    • Added routes to App.tsx: /auth/parent-consent-pending and /auth/parent-consent/:token
    • Added login blocking redirect for minors without verified consent
    • Updated types/index.ts: Added parental_consent_pending to AuthResponse
    • Updated lib/api.ts: New signup signature + 3 parental consent API methods
  • Tests (Session 4):

    • test_age_verification.py: 24 tests — calculate_age (leap year, birthday edge cases), get_age_threshold (all regions), is_minor (boundary checks), requires_parental_consent
    • test_parental_consent.py: 22 tests — request/verify/resend/status flows, token security, expiry validation
    • test_auth_minors.py: 14 tests — minor signup ±parent_email, adult signup, login blocking, consent verification, event emission
    • AuthForm.test.tsx: 21 tests — birthdate field, parent_email conditional, age threshold UI, GDPR hint, max date
    • ParentConsentPending.test.tsx: 7 tests — rendering, resend API, toast notifications
    • ParentConsentVerify.test.tsx: 11 tests — consent info, verify flow, success/error/loading states
    • Updated api.test.ts and useAuth.test.ts for new signup signature

C-STORE-002 — COMPLETE (100%) — Session 5 Completed

Design Decisions

  • Service-layer encryption: Encrypt/decrypt in service code (not model layer) for flexibility
  • Fernet symmetric encryption: cryptography.fernet.Fernet — authenticated encryption with random IV
  • ENC: prefix: Distinguishes encrypted from plaintext data, enables gradual migration
  • Pass-through when disabled: No ENCRYPTION_KEY → no encryption (development mode)
  • Git files encrypted directly: File content encrypted with same Fernet key (simpler than git-crypt, no system dependency)

Implementation

  • Created crypto/__init__.py + crypto/encryption_service.py:
    • EncryptionService class: encrypt/decrypt strings and bytes
    • ENCRYPTED_PREFIX = "ENC:" for data identification
    • Singleton via get_encryption_service() + reset_encryption_service()
    • Graceful failure: wrong key → [decryption failed], disabled → [encrypted]
    • Invalid key → disables encryption (no crash)
  • Updated config.py: ENCRYPTION_KEY env var (Fernet key format)
  • Updated chat/service.py:
    • add_message(): encrypts content before INSERT
    • get_messages(): decrypts content after SELECT via _decrypt_message()
    • update_message(): re-encrypts content on edit
    • pin_message(), unpin_message(), get_pinned_messages(): decrypt returned Messages
  • Updated memory/vector_store.py:
    • insert(): encrypts content before INSERT
    • search(), search_multi_scope(): decrypts content in results
    • get_by_id(): decrypts content
    • update_content(): re-encrypts content on update
  • Updated memory/git_store.py:
    • save_conversation(): encrypts entire Markdown file content
    • save_memory(): encrypts entire Markdown file content
  • Updated memory/summarizer.py:
    • _get_existing_memories(): decrypts content for LLM comparison
  • Updated auth/service.py:
    • export_user_data(): decrypts message content in GDPR data export
  • Created scripts/encrypt_existing_data.py:
    • Batch migration script for existing plaintext data
    • Supports --dry-run and --batch-size flags
    • Idempotent — skips already-encrypted rows (checks ENC: prefix)

Tests (48 new tests)

  • test_encryption_service.py: 29 tests
    • Encrypt/decrypt round-trip (string, unicode, emoji, JSON, multiline, long text)
    • Different encryptions differ (random IV)
    • Disabled mode passthrough
    • Invalid key disables encryption
    • Wrong key returns placeholder
    • Plaintext passthrough (migration support)
    • Edge cases (empty, None, whitespace)
    • Bytes encryption/decryption
    • Singleton management
  • test_chat_encryption.py: 7 tests
    • Content stored encrypted in DB, returned decrypted
    • Disabled mode stores plaintext
    • Pre-migration plaintext messages still work
    • Edited content re-encrypted
    • Pinned messages decrypted
  • test_vector_store_encryption.py: 7 tests
    • Memory content stored encrypted
    • Search returns decrypted content
    • Multi-scope search decrypted
    • Get by ID decrypted
    • Updated content re-encrypted
    • Mixed plaintext/encrypted coexistence (migration)
  • test_git_encryption.py: 5 tests
    • Conversation files encrypted on disk
    • Memory files encrypted on disk
    • Files decryptable with same key
    • Disabled mode stores plaintext

Deployment Checklist

  • Run migration 012 (C-CONSENT-004)
  • Generate ENCRYPTION_KEY and add to .env.prod:
    python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
  • Run data migration script (encrypt existing data):
    docker compose exec backend python scripts/encrypt_existing_data.py --dry-run
    docker compose exec backend python scripts/encrypt_existing_data.py
  • Document key backup procedures — losing this key = all encrypted data unrecoverable
  • Test backup/restore with encrypted data

Test Results

  • Backend: 1,423 tests pass, 84% coverage
  • Frontend: 754 tests pass, build clean
  • Total new tests: 48 (C-STORE-002) + ~99 (C-CONSENT-004) = ~147 tests added