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
AuthFormcomponent 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
ParentConsentPendingpage (shown after minor signup) - Created
ParentConsentVerifypage (parent clicks email link) - Added routes to App.tsx:
/auth/parent-consent-pendingand/auth/parent-consent/:token - Added login blocking redirect for minors without verified consent
- Updated
types/index.ts: Addedparental_consent_pendingto AuthResponse - Updated
lib/api.ts: New signup signature + 3 parental consent API methods
- Updated
-
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.tsanduseAuth.test.tsfor 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:EncryptionServiceclass: encrypt/decrypt strings and bytesENCRYPTED_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_KEYenv var (Fernet key format) - Updated
chat/service.py:add_message(): encrypts content before INSERTget_messages(): decrypts content after SELECT via_decrypt_message()update_message(): re-encrypts content on editpin_message(),unpin_message(),get_pinned_messages(): decrypt returned Messages
- Updated
memory/vector_store.py:insert(): encrypts content before INSERTsearch(),search_multi_scope(): decrypts content in resultsget_by_id(): decrypts contentupdate_content(): re-encrypts content on update
- Updated
memory/git_store.py:save_conversation(): encrypts entire Markdown file contentsave_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-runand--batch-sizeflags - 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_KEYand 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