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) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-29 10:00:46 +02:00
parent 0230d93004
commit 388a7d4866
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
6 changed files with 314 additions and 0 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-29

View file

@ -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.

View file

@ -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`

View file

@ -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`

View file

@ -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

View file

@ -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