feat/batch-2026-03-28 - #79
Merged
Merged
Conversation
Implement application-level encryption using EF Core Value Converters to protect PII and credentials at rest. Encrypted columns: User (Email, Name, GoogleAccessToken, GoogleRefreshToken, StripeCustomerId, StripeSubscriptionId), Habit (Title, Description), HabitLog (Note), UserFact (FactText), Goal (Title, Description), GoalProgressLog (Note), PushSubscription (Endpoint, P256dh, Auth). Key changes: - EncryptionService with AES-256-GCM encrypt/decrypt and HMAC-SHA256 for deterministic email lookups - EF Core ValueConverters applied transparently in DbContext - EmailHash column on User for login lookups (replaces Email unique index) - Auth commands (VerifyCode, GoogleAuth) updated to use EmailHash - DataEncryptionMigrationService for one-time migration of existing data - Graceful passthrough mode when encryption keys not configured - 15 unit tests covering roundtrip, HMAC, nullability, and edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the bonus-days referral reward system with Stripe discount coupons: - New IReferralRewardService interface (replaces ISubscriptionRewardService) - StripeCouponRewardService creates per-user 10% Stripe coupons via Promotion Codes - ProcessReferralCodeCommand gives referred user (B) a coupon at signup - CheckReferralCompletionCommand gives referrer (A) a coupon on completion - GetReferralStatsQuery returns rewardType/discountPercent instead of rewardDays - SubscriptionController auto-applies coupon at checkout, clears after redemption - User entity gains ReferralCouponId property with EF Core migration - Updated all unit tests for new coupon-based flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends DataEncryptionMigrationService to re-save all entities with encrypted columns on first startup. Uses AppConfig flag to run only once. Processes in batches of 50 with per-batch error handling. Handles UserFact query filter (soft deletes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The heuristic IsEncrypted() check (valid Base64 + length > 40) was matching plaintext data like URLs and long emails, causing AES-GCM decryption failures. Now encrypted values use an "enc:" prefix for reliable detection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove encryption from Email, Name, StripeCustomerId, StripeSubscriptionId, and PushSubscription fields. These are functional identifiers needed for support and lookups. Keep encryption on: Habit Title/Description, HabitLog Note, UserFact FactText, Goal Title/Description, GoalProgressLog Note, GoogleAccessToken, GoogleRefreshToken. Removes EmailHash column, HMAC key, and related complexity. Simplifies to a single Encryption__Key env var. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Data encrypted before the prefix was added is valid AES-GCM ciphertext but missing the "enc:" prefix. Decrypt now falls back to trying AES-GCM decryption on unprefixed values -- if it fails, the value is treated as plaintext. On next save, the ValueConverter re-encrypts with the prefix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some data was encrypted without the prefix, then the migration re-encrypted the Base64 ciphertext with the prefix (double layer). After decrypting the outer enc: layer, now tries to decrypt the result as legacy format to handle the inner layer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Jun 4, 2026
thomasluizon
added a commit
that referenced
this pull request
Jun 4, 2026
…iTools (#79) (#175) MCP habit mutations now run through McpExecutorBridge -> IAgentOperationExecutor (Surface=Mcp), so they share the agent policy layer (read-only-credential denial, ownership pre-check, confirmation gating) and the AgentAuditLogs trail instead of calling IMediator.Send directly. Reads stay on MediatR. bulk_log_habits and bulk_skip_habits stay on MediatR because their MCP contract supports an explicit per-instance date the chat IAiTools do not model. Authors the 6 previously-unbacked habit IAiTools (update_checklist, reorder_habits, move_habit_parent, link_goals_to_habit, bulk_create_habits, bulk_delete_habits), registers them in DI, and maps them in AgentCatalogService so the build-time catalog enforcement passes. Overlaps wave-2 #89, which needs the same 6 tools. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Combined Changes
This PR bundles the following worktree branches:
Individual Changes
Encrypt Data at Rest
EncryptionServicewith AES-256-GCM encrypt/decrypt + HMAC-SHA256 for searchable hashesEncryptionValueConverterfor EF Core -- transparent encryption of 15+ sensitive columns across 7 entitiesEmailHashcolumn for user lookup (since encrypted email can't be searched)DataEncryptionMigrationServicefor one-time EmailHash population on startupReferral Discount Coupon
StripeCouponRewardServicereplacesStripeSubscriptionRewardServiceProcessReferralCodeCommandgives referee a Stripe coupon instead of trial daysCheckReferralCompletionCommandgives referrer a coupon instead of trial/subscription extensionSubscriptionController.CreateCheckoutauto-applies pending coupon at checkoutReferralCouponIdcolumn on User entityDeployment Notes
Encryption__KeyandEncryption__HmacKeyenv vars on Render (generate withopenssl rand -base64 32)Test Plan
🤖 Generated with Claude Code