# Plan: Finish Unstaged Work + Profile Settings + Content Expiry ## Context Three items from the current sprint: 1. **Unstaged work** needs to be committed -- paste->txt rename, profile page, platform-operator, and favicons are all substantially complete but have a Containerfile path bug. 2. **Profile/settings page** exists but needs a content settings section. 3. **Content expiry** -- pro users' content should default to 90-day expiry with an opt-out to unlimited. Currently pro txt pastes never expire and pics have no expiry at all. Additionally, txt and pics currently hardcode `plan: None` in their OIDC callbacks, meaning `is_pro()` always returns false. All users are treated as free tier. This must be fixed for content expiry to work correctly. ## Part 1: Fix and Commit Unstaged Work ### 1.1 Fix txt Containerfile `crates/txt/Containerfile` lines 9-10 reference `crates/paste/` -- update to `crates/txt/`. ### 1.2 Commit all unstaged work Stage and commit: paste->txt rename, profile page + route registration, platform-operator crate, favicon/OG assets across all services, and all other pending changes. --- ## Part 2: Sync Plan and Content Expiry via OIDC Claims ### 2.1 Database migration File: `sql/003_add_content_expires.sql` ```sql ALTER TABLE users ADD COLUMN content_expires BOOLEAN NOT NULL DEFAULT true; ``` ### 2.2 Keycloak configuration (manual) In the `irc-now` realm: - Add user attribute `plan` with protocol mapper -> ID token claim (type: String) - Add user attribute `content_expires` with protocol mapper -> ID token claim (type: String, value "true"/"false") - Set defaults: `plan=free`, `content_expires=true` ### 2.3 common/auth.rs - Add `content_expires: Option` field to `UserClaims` - Add helper: `pub fn content_expires(&self) -> bool { self.content_expires.unwrap_or(true) }` ### 2.4 web-api: Keycloak attribute sync Add `update_keycloak_user_attributes()` to `profile.rs` (reuses existing `get_keycloak_admin_token`). Calls: `PUT /admin/realms/irc-now/users/{sub}` with `{"attributes": {"plan": "...", "content_expires": "..."}}` Call sites: - **profile.rs update()**: sync `content_expires` on save - **billing.rs webhook**: sync `plan` when Stripe changes it - **auth.rs callback**: sync `plan` on login (ensures Keycloak stays in sync with DB) ### 2.5 web-api: Profile page updates File: `crates/web-api/src/routes/profile.rs` - Add `content_expires` to `ProfileRow`, `ProfileTemplate`, `ProfileForm` - On update: save to DB + sync to Keycloak attributes - Show toggle only for pro users File: `crates/web-api/templates/profile.html` - Add "content settings" card after the password card: - Checkbox: "auto-expire content after 90 days" (checked by default) - Note: "uncheck to keep your txt and pics content indefinitely" - Only visible when plan=pro ### 2.6 txt/pics: Extract custom claims from OIDC token Files: `crates/txt/src/routes/auth.rs`, `crates/pics/src/routes/auth.rs` The `openid` crate's `id_token.payload()` returns standard claims. Custom claims need to be extracted from the raw JWT. Decode the ID token JWT payload (base64 middle segment) and parse `plan` and `content_expires` from it. Update the `UserClaims` construction in both callbacks to populate `plan` and `content_expires` from the decoded custom claims. --- ## Part 3: Content Expiry Logic ### 3.1 txt expiry changes File: `crates/txt/src/routes/paste.rs` Add constant: `const PRO_EXPIRY_DAYS: i64 = 90;` Change expiry logic in `create()` (lines 103-107): - Free: 24h (unchanged) - Pro + content_expires=true: 90 days - Pro + content_expires=false: None (unlimited) ### 3.2 pics expiry changes #### Migration File: `crates/pics/migrations/002_add_expires_at.sql` ```sql ALTER TABLE images ADD COLUMN expires_at TIMESTAMPTZ; UPDATE images SET expires_at = created_at + INTERVAL '90 days'; ``` #### Upload expiry File: `crates/pics/src/routes/image.rs` - Add constants: `const FREE_EXPIRY_DAYS: i64 = 90;`, `const PRO_EXPIRY_DAYS: i64 = 90;` - Set `expires_at` on insert: - Free: 90 days - Pro + content_expires=true: 90 days - Pro + content_expires=false: None #### Cleanup task File: `crates/pics/src/main.rs` - Add hourly cleanup task (same pattern as txt): - Query expired images - Delete from S3 (original + thumbnail) - Delete from DB ### 3.3 Backfill existing content Run manually after deploy: - txt (paste-db): `UPDATE pastes SET expires_at = created_at + INTERVAL '90 days' WHERE expires_at IS NULL;` - pics (pics-db): Handled by migration in 3.2 --- ## Files to Modify | File | Change | |------|--------| | `crates/txt/Containerfile` | Fix paths from paste -> txt | | `sql/003_add_content_expires.sql` | New migration | | `crates/common/src/auth.rs` | Add content_expires field | | `crates/web-api/src/routes/profile.rs` | Add content_expires toggle, Keycloak sync | | `crates/web-api/templates/profile.html` | Add content settings card | | `crates/web-api/src/routes/billing.rs` | Sync plan to Keycloak on change | | `crates/web-api/src/routes/auth.rs` | Sync plan to Keycloak on login | | `crates/txt/src/routes/auth.rs` | Extract plan + content_expires from token | | `crates/txt/src/routes/paste.rs` | Pro 90-day expiry logic | | `crates/pics/src/routes/auth.rs` | Extract plan + content_expires from token | | `crates/pics/src/routes/image.rs` | Set expires_at on upload | | `crates/pics/src/main.rs` | Add expired image cleanup task | | `crates/pics/migrations/002_add_expires_at.sql` | New migration | ## Verification 1. `cargo build --workspace` -- everything compiles 2. `cargo test --workspace` -- existing tests pass 3. Deploy web-api, verify profile page shows content settings toggle for pro users 4. Run migration on accounts-db 5. Configure Keycloak attributes and mappers 6. Deploy txt, create a paste as pro user -- verify 90-day expiry 7. Deploy pics, upload an image as pro user -- verify 90-day expiry 8. Toggle content_expires=false on profile, re-login to txt -- verify unlimited 9. Run backfill SQL on paste-db 10. Verify cleanup task deletes expired content