feat(api): encrypt Google tokens backfill + user data export (#115) - #181
Conversation
WebApplicationFactory<Program> loads config from the Orbit.Api content
root, where appsettings.json carries a 27-byte "REPLACE-IN-DEVELOPMENT-JSON"
placeholder for Jwt:SecretKey. JwtSettings.Validate() requires >=32 bytes,
so every integration test threw at startup
("Jwt:SecretKey must be at least 32 bytes (27 provided)").
The test project's own appsettings.json is never loaded by the factory
(content root resolves to src/Orbit.Api), so its valid key was dead config.
Inject a test-only secret via UseSetting in the factory instead. Production
validator is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add User to the DataEncryptionMigrationService backfill loop (versioned the EncryptionMigrationComplete flag to EncryptionMigrationComplete_v2 so existing User rows with plaintext Google OAuth tokens are re-encrypted once). Verified the GoogleAccessToken/GoogleRefreshToken converter wiring with a real-key round-trip test. Add an authenticated GET /api/profile/export (ExportUserDataQuery + UserDataExport model) returning all of the user's habits, logs, goals, progress logs, tags, facts, and settings as a downloadable JSON attachment (LGPD Art. 18 / GDPR Art. 20). Integration-tested for content and per-user scoping. Refs thomasluizon/orbit-ui-mobile#115 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Solid compliance feature. The data export query is correct — FindAsync takes Expression<Func<T,bool>> so the habitIds.Contains/goalIds.Contains predicates translate to SQL IN clauses, not in-memory filters. Authorization inherits from the class-level [Authorize] on ProfileController (confirmed, and the 401 integration test verifies it). Sensitive fields (Google tokens, Stripe IDs) are correctly excluded from the export model. The encryption backfill versioning strategy (EncryptionMigrationComplete_v2) is sound: EF Core value converters decrypt-on-read/encrypt-on-write, so re-running over already-encrypted rows is idempotent. Integration tests cover the three cases that matter — auth gating, file attachment headers, and user-data isolation. Nothing to block.
Part of the Legal & Compliance umbrella. API side of paired PRs.
Scope
Userto theDataEncryptionMigrationServicebackfill loop. Versioned the one-shot flag (EncryptionMigrationComplete→EncryptionMigrationComplete_v2) so existingUserrows with plaintextGoogleAccessToken/GoogleRefreshTokenare re-encrypted once even where the original flag was already set. Converter wiring (OrbitDbContext) verified.ExportUserDataQuery+UserDataExportmodel aggregating all of the user's habits (with logs), goals (with progress logs), tags, AI memory facts, and settings. New authenticatedGET /api/profile/exportreturns it as a downloadable JSON attachment (Content-Disposition: attachment). Registered the action inAgentCatalogService(account-lifecycle capability). LGPD Art. 18 / GDPR Art. 20 portability.Tests
OrbitDbContextTests: model wires User Google tokens to the nullable encrypting converter + real-key round-trip proves ciphertext-at-rest (enc:prefix) and decrypt-on-read.ExportUserDataTests(integration): returns a downloadable JSON attachment, contains created habit/log/goal/tag, and is scoped to the requesting user (a second user's export is empty).Validation
dotnet buildclean.dotnet test: Application (1679) + Infrastructure (917) green; integration tests boot (stacked on the JWT fix) and connect to Postgresorbit_test— Profile/Export/Encryption/Auth/Habits suites pass.Production note
The export excludes secrets (Google tokens, Stripe IDs). The encryption backfill is a no-op unless
Encryption:Keyis a real 256-bit key in prod (placeholder key = passthrough by design); confirm the prod env var is set so converters encrypt rather than pass through.Refs thomasluizon/orbit-ui-mobile#115
Stacked on #176 (JWT integration-test fix).
Paired UI PR (DRAFT, needs-legal-review): thomasluizon/orbit-ui-mobile#131
🤖 Generated with Claude Code