diff --git a/docs/superpowers/specs/2026-04-17-persistent-login-unused-commands-design.md b/docs/superpowers/specs/2026-04-17-persistent-login-unused-commands-design.md new file mode 100644 index 0000000..e5f18c9 --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-persistent-login-unused-commands-design.md @@ -0,0 +1,128 @@ +# Design: Persistent Login + Unused Commands UI + +**Date:** 2026-04-17 +**Status:** Approved + +--- + +## Overview + +Two features: + +1. **Persistent login** — users sign in once per machine; the session is restored on next app launch unless they explicitly sign out. +2. **Unused commands** — wire `delete_message`, `update_profile`, and `add_contact` into the frontend; currently these backend commands have no UI. + +--- + +## Feature 1: Persistent Login + +### Architecture + +Use `tauri-plugin-store` (official Tauri v2 JSON store plugin) to persist the JWT token to disk in the app data directory. + +Store file: `session.json` +Store key: `"token"` (string value) + +### Backend changes + +**`Cargo.toml`** +- Add `tauri-plugin-store = "2"` + +**`lib.rs`** +- Register `.plugin(tauri_plugin_store::Builder::default().build())` in `tauri::Builder` + +**`commands/user.rs`** + +- `signin`: add `app_handle: AppHandle` parameter. After setting `state.token`, open the store and write the token string under key `"token"`. +- `signup`: same — after setting `state.token`, write token to store. +- `signout`: add `app_handle: AppHandle` parameter. After `db.invalidate()`, open the store and delete the `"token"` key. +- New command `restore_session`: opens the store, reads `"token"`. If absent → return error (frontend shows auth screen). If present → call `state.db.authenticate(jwt)` with the token, then query `SELECT * FROM $auth`. If the query returns a user → update `state.token` and return `User`. If authenticate fails (expired/invalid) → delete the token from the store, clear `state.token`, return error. + +**`lib.rs` invoke_handler** +- Add `commands::user::restore_session` to the handler list. + +### Frontend changes + +**`src/routes/+page.svelte`** + +- `init()`: replace `cmd('get_me')` with `cmd('restore_session')`. Behaviour is identical from the frontend's perspective — success → app view, error → auth view. + +No other frontend changes needed for this feature; token save/clear happens entirely on the Rust side inside the existing signin/signup/signout commands. + +### Data flow + +``` +App launch → init() → restore_session + ├─ token on disk, valid → User returned → app view + ├─ token on disk, expired → store cleared → error → auth view + └─ no token → error → auth view + +signin/signup → token saved to store automatically +signout → token removed from store automatically +``` + +--- + +## Feature 2: Unused Commands UI + +### 2a. Delete Message + +**Backend:** `delete_message(message_id: String)` already exists. Enforces `WHERE author = $auth` so only the author can delete. + +**`src/routes/+page.svelte`** +- Add `deleteMessage(msgId: string)`: calls `cmd('delete_message', { messageId: msgId })`, then filters the deleted message from local `messages` state on success. +- Pass `onDeleteMessage: (msgId: string) => void` prop to `ChatMain`. + +**`src/lib/components/ChatMain.svelte`** +- Accept `onDeleteMessage` prop. +- Message context menu: add `{ label: 'Delete message', action: () => onDeleteMessage(full(msg.id)) }` item, only when `full(msg.author) === full(user?.id)` (own messages only — avoids confusing menu items for others' messages; backend already enforces the constraint server-side). +- `ChatMain` needs `user` prop passed from parent to perform the author check. + +### 2b. Update Profile + +**Backend:** `update_profile(username?: String, avatar?: String)` already exists. Returns updated `User`. + +**`src/lib/components/Sidebar.svelte`** +- Add `showEditProfile: boolean` local state (default `false`). +- Clicking the user pill (name/avatar area) in the footer toggles `showEditProfile`. +- When open: render an inline edit form above the footer (same visual pattern as the new-room form — fade-in animation, small field + save button). + - Fields: `username` (pre-filled with current value), `avatar` (pre-filled, optional URL). + - Save button calls `onUpdateProfile({ username, avatar })` callback. + - Cancel button (or pressing Escape) closes the form without saving. +- Add `onUpdateProfile: (fields: { username?: string; avatar?: string }) => Promise` to Sidebar's Props interface. + +**`src/routes/+page.svelte`** +- Add `updateProfile(fields)` function: calls `cmd('update_profile', fields)` → updates `user` state with returned value. +- Pass `onUpdateProfile={updateProfile}` to `Sidebar`. + +### 2c. Add Contact + +**Backend:** `add_contact(user_id: String)` already exists. Returns a `Contact` record. + +**`src/lib/components/Sidebar.svelte`** +- Add `showAddContact: boolean` local state (default `false`). +- Add a "+" `icon-btn` next to the CONTACTS section label (always visible even when contacts list is empty) to toggle `showAddContact`. +- When open: render inline form (same pattern as new-room form) with a single text input labelled "user id". +- On submit: call `onAddContact(userId)` callback, then close form. +- Add `onAddContact: (userId: string) => Promise` to Sidebar's Props interface. + +**`src/routes/+page.svelte`** +- Add `addContact(userId: string)` function: calls `cmd('add_contact', { userId })` → on success calls `get_contacts` to refresh the contacts list and update `contacts` state. +- Pass `onAddContact={addContact}` to `Sidebar`. + +--- + +## Error handling + +- `restore_session`: any error (missing token, expired, network) → auth view. No toast/message needed — user just sees the login screen. +- `deleteMessage`: errors shown in existing `err` state variable (already displayed in channel header). +- `updateProfile`: errors surfaced inside the edit form (local error state in Sidebar). +- `addContact`: errors surfaced inside the add-contact form (local error state in Sidebar). + +--- + +## Out of scope + +- User search (no backend command exists; add_contact uses raw user IDs for now) +- Token refresh / expiry detection during an active session +- Avatar image upload (update_profile accepts URL strings only)