From 388a7d486681b0e36c8174b1f812b2b1db08538b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 29 Mar 2026 10:00:46 +0200 Subject: [PATCH] Add account-settings OpenSpec change Settings page at /settings with profile editing, passkey management (list, add, delete), email change with verification, and account deletion. Includes comprehensive E2E test plan using virtual WebAuthn. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../changes/account-settings/.openspec.yaml | 2 + openspec/changes/account-settings/design.md | 105 ++++++++++++++++++ openspec/changes/account-settings/proposal.md | 32 ++++++ .../specs/account-settings/spec.md | 83 ++++++++++++++ .../specs/journal-auth/spec.md | 29 +++++ openspec/changes/account-settings/tasks.md | 63 +++++++++++ 6 files changed, 314 insertions(+) create mode 100644 openspec/changes/account-settings/.openspec.yaml create mode 100644 openspec/changes/account-settings/design.md create mode 100644 openspec/changes/account-settings/proposal.md create mode 100644 openspec/changes/account-settings/specs/account-settings/spec.md create mode 100644 openspec/changes/account-settings/specs/journal-auth/spec.md create mode 100644 openspec/changes/account-settings/tasks.md diff --git a/openspec/changes/account-settings/.openspec.yaml b/openspec/changes/account-settings/.openspec.yaml new file mode 100644 index 0000000..5e98b74 --- /dev/null +++ b/openspec/changes/account-settings/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-29 diff --git a/openspec/changes/account-settings/design.md b/openspec/changes/account-settings/design.md new file mode 100644 index 0000000..47762d5 --- /dev/null +++ b/openspec/changes/account-settings/design.md @@ -0,0 +1,105 @@ +## Context + +Users register and log in via passkeys or magic links. After that, there's no +way to manage their account. The user profile page (`/users/:username`) is +public-facing and read-only. Account management needs a private settings page. + +The `users` table already has `displayName` and `bio` columns. The +`credentials` table stores `deviceType`, `transports`, and `createdAt` per +passkey — enough to show a meaningful list. + +## Goals / Non-Goals + +**Goals:** +- Single settings page with sections for profile, security, and account +- Passkey management: list, add, delete +- Profile editing: display name, bio +- Email change with verification +- Account deletion with confirmation + +**Non-Goals:** +- Avatar/photo upload (requires S3 — see activity-photos spec) +- Notification preferences (no notifications yet) +- Privacy settings (route visibility is in route-sharing spec) +- Two-factor authentication (passkeys already provide strong auth) +- Session management / "log out everywhere" + +## Decisions + +### D1: Single settings page with sections + +One route at `/settings` with anchor-linked sections rather than separate +sub-pages. Keeps navigation simple and avoids route proliferation. + +Sections: +- **Profile** — display name, bio +- **Security** — passkeys list, add passkey button +- **Account** — email, delete account + +### D2: Profile editing + +Simple form with display name and bio fields. Submit via POST to +`/api/settings/profile`. No real-time validation needed — just save on submit. + +Bio is plain text, max 160 characters (like a social bio). + +### D3: Passkey management + +List all credentials for the current user, showing: +- Device type (from `deviceType` column, e.g., "singleDevice" / "multiDevice") +- Transport hints (from `transports`, e.g., "internal", "usb", "ble", "nfc") +- Date registered (from `createdAt`) +- Delete button (with confirmation) + +Friendly labels derived from transports: +- `internal` → "This device" +- `usb` → "Security key" +- `ble` → "Bluetooth" +- `hybrid` → "Phone or tablet" +- fallback → "Passkey" + +Add passkey button reuses the existing `addPasskeyStart` / `addPasskeyFinish` +flow from auth.server.ts. + +Deleting the last passkey is allowed — user can still log in via magic link. +Show a warning when deleting the last one. + +### D4: Email change + +Two-step flow: +1. User enters new email → server sends magic link to the **new** email +2. User clicks link → email is updated + +This ensures ownership of the new address. The old email is not notified +(simplicity; can add later if needed). + +New server function: `initiateEmailChange(userId, newEmail)` creates a +magic token with a `newEmail` field, sends verification to the new address. +New verify handler recognizes email-change tokens and updates the user record. + +### D5: Account deletion + +- Button at bottom of settings page, styled as danger +- Confirmation modal: "This will permanently delete your account and all your + data. Type your username to confirm." +- Server-side: cascading delete via DB foreign keys (credentials, magic tokens, + routes, activities all cascade from users) +- After deletion: destroy session, redirect to home page + +### D6: Navigation + +Add "Settings" link to the user dropdown/nav when logged in. Links to +`/settings`. Must be registered in `routes.ts`. + +### D7: Auth guard + +Settings page requires authentication. Loader checks session and redirects to +`/auth/login` if not logged in. + +## Risks / Trade-offs + +- **Email change without old-email notification**: Simpler but less secure. + If an attacker has session access, they could change email silently. Mitigate: + magic link to new address still required. Acceptable for now. +- **Deleting last passkey**: User relies entirely on magic links. Acceptable + since magic links are a first-class auth method. diff --git a/openspec/changes/account-settings/proposal.md b/openspec/changes/account-settings/proposal.md new file mode 100644 index 0000000..0510e3f --- /dev/null +++ b/openspec/changes/account-settings/proposal.md @@ -0,0 +1,32 @@ +## Why + +Users have no way to manage their account after registration. There's no +settings page to edit their profile, manage passkeys, change email, or delete +their account. Passkey count currently shows on the home page as a stopgap, +but it belongs in a dedicated settings area. + +## What Changes + +- **Account settings page** (`/settings`): central place for account management +- **Profile editing**: update display name and bio +- **Email change**: update email with verification via magic link +- **Passkey management**: view registered passkeys (device type, date added), + add new passkeys, delete existing ones +- **Account deletion**: delete account and all associated data with confirmation +- Move passkey status display from home page to settings + +## Capabilities + +### New Capabilities +- `account-settings`: Settings page with profile, security, and account sections + +### Modified Capabilities +- `journal-auth`: Passkey management moves from home page prompt to settings; + add passkey delete functionality + +## Impact + +- **Files**: New route `apps/journal/app/routes/settings.tsx`, API routes for + profile update, email change, passkey delete, account delete +- **Database**: No schema changes — uses existing users and credentials tables +- **Routes**: Must register new routes in `apps/journal/app/routes.ts` diff --git a/openspec/changes/account-settings/specs/account-settings/spec.md b/openspec/changes/account-settings/specs/account-settings/spec.md new file mode 100644 index 0000000..67a3e32 --- /dev/null +++ b/openspec/changes/account-settings/specs/account-settings/spec.md @@ -0,0 +1,83 @@ +## ADDED Requirements + +### Requirement: Settings page access +The Journal SHALL provide an account settings page at `/settings` accessible to authenticated users. + +#### Scenario: Authenticated access +- **WHEN** a logged-in user navigates to `/settings` +- **THEN** the settings page is displayed with profile, security, and account sections + +#### Scenario: Unauthenticated access +- **WHEN** an unauthenticated user navigates to `/settings` +- **THEN** they are redirected to `/auth/login` + +### Requirement: Profile editing +The settings page SHALL allow users to edit their display name and bio. + +#### Scenario: Update display name +- **WHEN** a user changes their display name and submits +- **THEN** the display name is updated and reflected on their public profile + +#### Scenario: Update bio +- **WHEN** a user enters a bio (max 160 characters) and submits +- **THEN** the bio is saved and visible on their public profile + +#### Scenario: Empty fields +- **WHEN** a user clears their display name +- **THEN** their username is used as the display name fallback + +### Requirement: Passkey management +The settings page SHALL list all registered passkeys and allow adding or deleting them. + +#### Scenario: View passkeys +- **WHEN** a user visits the security section +- **THEN** they see a list of their passkeys with device type, transport label, and registration date + +#### Scenario: Add passkey +- **WHEN** a user clicks "Add passkey" on a browser that supports WebAuthn +- **THEN** the browser passkey creation prompt appears and the new passkey is added to the list + +#### Scenario: Add passkey unsupported browser +- **WHEN** a user visits the security section on a browser without WebAuthn +- **THEN** the "Add passkey" button is disabled with a message explaining the browser limitation + +#### Scenario: Delete passkey +- **WHEN** a user clicks delete on a passkey and confirms +- **THEN** the passkey is removed and can no longer be used for login + +#### Scenario: Delete last passkey +- **WHEN** a user deletes their only passkey +- **THEN** a warning is shown that they will need to use magic links to sign in, and the deletion proceeds after confirmation + +### Requirement: Email change +The settings page SHALL allow users to change their email address with verification. + +#### Scenario: Initiate email change +- **WHEN** a user enters a new email and submits +- **THEN** a verification link is sent to the new email address + +#### Scenario: Verify new email +- **WHEN** the user clicks the verification link +- **THEN** their email is updated to the new address + +#### Scenario: Duplicate email +- **WHEN** a user enters an email already in use by another account +- **THEN** an error is shown indicating the email is taken + +### Requirement: Account deletion +The settings page SHALL allow users to permanently delete their account. + +#### Scenario: Delete account +- **WHEN** a user clicks "Delete account" and types their username to confirm +- **THEN** their account and all associated data are permanently deleted and they are logged out + +#### Scenario: Cancel deletion +- **WHEN** a user opens the delete confirmation but does not confirm +- **THEN** no data is deleted and they remain on the settings page + +### Requirement: Settings navigation +The Journal navigation SHALL include a link to the settings page for authenticated users. + +#### Scenario: Settings link visible +- **WHEN** a logged-in user views the navigation +- **THEN** a "Settings" link is present that navigates to `/settings` diff --git a/openspec/changes/account-settings/specs/journal-auth/spec.md b/openspec/changes/account-settings/specs/journal-auth/spec.md new file mode 100644 index 0000000..a088b87 --- /dev/null +++ b/openspec/changes/account-settings/specs/journal-auth/spec.md @@ -0,0 +1,29 @@ +## MODIFIED Requirements + +### Requirement: Add passkey from new device +The Journal SHALL allow logged-in users to register additional passkeys from the account settings page or via the post-login prompt. + +#### Scenario: Add passkey after magic link login +- **WHEN** a user logs in via magic link on a device that supports WebAuthn +- **THEN** the system prompts them to register a passkey for that device + +#### Scenario: Add passkey from settings +- **WHEN** a user clicks "Add passkey" in the security section of account settings +- **THEN** the browser passkey creation prompt appears and the new passkey is stored + +#### Scenario: Add passkey prompt on unsupported browser +- **WHEN** a user logs in via magic link on a device that does not support WebAuthn +- **THEN** the system shows the add-passkey prompt with a message that the browser does not support passkeys + +## ADDED Requirements + +### Requirement: Delete passkey +The Journal SHALL allow users to delete individual passkeys from account settings. + +#### Scenario: Delete passkey +- **WHEN** a user deletes a passkey from account settings +- **THEN** the credential is removed from the database and can no longer be used for authentication + +#### Scenario: Delete last passkey warning +- **WHEN** a user attempts to delete their only remaining passkey +- **THEN** a warning is shown explaining they will need to use magic links, and deletion proceeds after confirmation diff --git a/openspec/changes/account-settings/tasks.md b/openspec/changes/account-settings/tasks.md new file mode 100644 index 0000000..f685b36 --- /dev/null +++ b/openspec/changes/account-settings/tasks.md @@ -0,0 +1,63 @@ +## 1. Settings Route & Layout + +- [ ] 1.1 Create `apps/journal/app/routes/settings.tsx` with loader auth guard (redirect to `/auth/login` if unauthenticated) +- [ ] 1.2 Register route in `apps/journal/app/routes.ts` +- [ ] 1.3 Layout with three sections: Profile, Security, Account (anchor links for navigation) + +## 2. Profile Section + +- [ ] 2.1 Display name and bio form fields, pre-filled from current user data +- [ ] 2.2 Create `POST /api/settings/profile` action to update display name and bio +- [ ] 2.3 Register API route in `routes.ts` +- [ ] 2.4 Success/error feedback on save + +## 3. Security Section — Passkey Management + +- [ ] 3.1 Load user's credentials in settings loader (device type, transports, createdAt) +- [ ] 3.2 Render passkey list with friendly transport labels and registration date +- [ ] 3.3 Add passkey button using existing `addPasskeyStart`/`addPasskeyFinish` flow, hidden when WebAuthn unsupported +- [ ] 3.4 Create `POST /api/settings/passkey/delete` action to remove a credential by ID +- [ ] 3.5 Register API route in `routes.ts` +- [ ] 3.6 Delete confirmation dialog, with last-passkey warning when applicable + +## 4. Account Section — Email Change + +- [ ] 4.1 Email display with "Change" button revealing input for new email +- [ ] 4.2 Create `initiateEmailChange(userId, newEmail)` in auth.server.ts — generates magic token with `newEmail` metadata +- [ ] 4.3 Create `POST /api/settings/email` action to initiate email change +- [ ] 4.4 Register API route in `routes.ts` +- [ ] 4.5 Update `verifyMagicToken` to handle email-change tokens (update user email) +- [ ] 4.6 Send verification email to new address using existing email templates + +## 5. Account Section — Deletion + +- [ ] 5.1 "Delete account" danger button at bottom of settings page +- [ ] 5.2 Confirmation modal requiring username input to proceed +- [ ] 5.3 Create `POST /api/settings/delete-account` action — cascading delete, destroy session, redirect to home +- [ ] 5.4 Register API route in `routes.ts` + +## 6. Navigation + +- [ ] 6.1 Add "Settings" link to Journal nav for authenticated users +- [ ] 6.2 Move passkey count display from home page to settings security section + +## 7. i18n + +- [ ] 7.1 Add translation keys for all settings UI text (en + de) + +## 8. Testing + +### Unit tests (Vitest) +- [ ] 8.1 Profile update: valid input saves, empty display name falls back to username +- [ ] 8.2 Passkey delete: removes credential, rejects invalid credential ID, allows deleting last passkey +- [ ] 8.3 Email change: creates token for new email, rejects duplicate email, rejects same-as-current email +- [ ] 8.4 Account deletion: cascading delete removes user + credentials + tokens, destroys session + +### E2E tests (Playwright) +- [ ] 8.5 Auth guard: unauthenticated user visiting `/settings` is redirected to `/auth/login` +- [ ] 8.6 Profile editing: log in, navigate to settings, update display name and bio, verify changes persist after reload, verify public profile reflects changes +- [ ] 8.7 Passkey management: log in, navigate to security section, add passkey (virtual WebAuthn authenticator), verify it appears in list, delete it, verify removed +- [ ] 8.8 Delete last passkey: delete only passkey, verify warning modal appears, confirm deletion, verify magic link login still works +- [ ] 8.9 Email change: initiate email change, follow verification link (dev mode), verify email updated in settings +- [ ] 8.10 Account deletion: click delete, type wrong username (verify blocked), type correct username, confirm, verify redirected to home and session destroyed +- [ ] 8.11 Navigation: verify "Settings" link appears in nav when logged in, not visible when logged out