Skip to content

feat/batch-2026-03-28 - #79

Merged
thomasluizon merged 8 commits into
mainfrom
feat/batch-2026-03-28
Mar 28, 2026
Merged

feat/batch-2026-03-28#79
thomasluizon merged 8 commits into
mainfrom
feat/batch-2026-03-28

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Combined Changes

This PR bundles the following worktree branches:

  • feat/encrypt-data-at-rest (1 commit) - AES-256-GCM column-level encryption for sensitive user data via EF Core Value Converters
  • feat/referral-discount-coupon (1 commit) - Replace referral trial extension reward with 10% Stripe discount coupons

Individual Changes

Encrypt Data at Rest

  • EncryptionService with AES-256-GCM encrypt/decrypt + HMAC-SHA256 for searchable hashes
  • EncryptionValueConverter for EF Core -- transparent encryption of 15+ sensitive columns across 7 entities
  • EmailHash column for user lookup (since encrypted email can't be searched)
  • DataEncryptionMigrationService for one-time EmailHash population on startup
  • Passthrough mode when keys not configured (safe for local dev)
  • 15 integration tests

Referral Discount Coupon

  • StripeCouponRewardService replaces StripeSubscriptionRewardService
  • ProcessReferralCodeCommand gives referee a Stripe coupon instead of trial days
  • CheckReferralCompletionCommand gives referrer a coupon instead of trial/subscription extension
  • SubscriptionController.CreateCheckout auto-applies pending coupon at checkout
  • New ReferralCouponId column on User entity
  • Updated referral test files

Deployment Notes

  • Set Encryption__Key and Encryption__HmacKey env vars on Render (generate with openssl rand -base64 32)
  • Create Stripe coupon (10% off, duration: once) -- the service creates per-user promotion codes

Test Plan

  • Local testing of combined changes
  • Verify encryption roundtrip with test keys
  • Verify referral coupon creation via Stripe dashboard
  • Verify no regressions from branch interactions

🤖 Generated with Claude Code

thomasluizon and others added 8 commits March 28, 2026 12:38
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>
@thomasluizon
thomasluizon merged commit 7fa27d8 into main Mar 28, 2026
2 checks passed
@thomasluizon
thomasluizon deleted the feat/batch-2026-03-28 branch March 28, 2026 16:58
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant