# 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<bool>` 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
