Files
cve-dashboard/.kiro/specs/user-profile/design.md

362 lines
17 KiB
Markdown

# Design Document: User Profile
## Overview
This feature adds a self-service user profile to the STEAM Security Dashboard. It introduces three capabilities: a profile panel accessible from the UserMenu dropdown (displaying account details and a password change form), a dedicated backend API endpoint for fetching profile data, and visual fixes to the UserMenu component to match the dark dashboard theme.
The scope is intentionally narrow — no new database tables or migrations are required. The existing `users` table already stores all needed fields (`username`, `email`, `user_group`, `created_at`, `last_login`). The backend adds two new routes to the existing auth router. The frontend adds one new component (`UserProfilePanel`) and modifies the existing `UserMenu` component for theming and profile access.
### Key Design Decisions
1. **Profile endpoint on the auth router** — The profile data is user-scoped and session-authenticated, so it belongs alongside `/api/auth/me` rather than on the admin-only `/api/users` router. This avoids granting non-admin users access to the users management routes.
2. **Password change on the auth router** — Self-service password change is an auth concern, not a user-management concern. Placing it at `POST /api/auth/change-password` keeps it separate from the admin `PATCH /api/users/:id` endpoint that already supports admin-initiated password resets.
3. **Rate limiting via express-rate-limit** — The project already uses `express-rate-limit` for login throttling. The password change endpoint reuses the same library with a tighter limit (5 attempts per 15 minutes) scoped per session cookie using a custom `keyGenerator`.
4. **No new database tables or migrations** — All required data already exists in the `users` table. The `created_at` and `last_login` columns are present in the schema. No schema changes are needed.
5. **Modal-based profile panel** — The profile panel is implemented as a modal overlay (consistent with existing modals like NvdSyncModal, UserManagement) rather than a separate page, since the app uses no client-side router.
6. **Inline style objects for theming** — Consistent with the project's existing pattern where components define style constants as JavaScript objects. The UserMenu theming fix converts Tailwind utility classes to inline styles matching the design system.
---
## Architecture
```mermaid
sequenceDiagram
participant U as User
participant UM as UserMenu
participant UPP as UserProfilePanel
participant API as Auth API
participant DB as SQLite
U->>UM: Clicks "My Profile"
UM->>UPP: Opens modal (showProfile=true)
UPP->>API: GET /api/auth/profile
API->>DB: SELECT user by session
DB-->>API: User row
API-->>UPP: { id, username, email, group, created_at, last_login }
UPP-->>U: Displays profile data
U->>UPP: Submits password change form
UPP->>UPP: Client-side validation (match, length)
UPP->>API: POST /api/auth/change-password
API->>API: Rate limit check (5/15min)
API->>DB: SELECT password_hash WHERE id = ?
DB-->>API: password_hash
API->>API: bcrypt.compare(currentPassword, hash)
API->>API: bcrypt.hash(newPassword, 10)
API->>DB: UPDATE users SET password_hash = ?
API->>DB: INSERT audit_logs (password_change)
API-->>UPP: { message: 'Password changed successfully' }
UPP-->>U: Success message, form cleared
```
### Component Hierarchy
```
App.js
├── UserMenu.js (modified — dark theme, "My Profile" option)
│ └── UserProfilePanel.js (new — modal component)
│ ├── Profile Info Section
│ └── Password Change Form
```
### Backend Route Structure
```
/api/auth/
├── POST /login (existing)
├── POST /logout (existing)
├── GET /me (existing)
├── GET /profile (new — full profile data)
├── POST /change-password (new — self-service password change)
└── POST /cleanup-sessions (existing)
```
---
## Components and Interfaces
### Backend: New Routes in `routes/auth.js`
#### `GET /api/auth/profile`
Returns the full profile for the authenticated user.
| Aspect | Detail |
|--------|--------|
| Auth | `requireAuth(db)` |
| Query | `SELECT id, username, email, user_group, created_at, last_login FROM users WHERE id = ? AND is_active = 1` |
| Success | `200 { id, username, email, group, created_at, last_login }` |
| Inactive account | `401 { error }` + clear session cookie |
| No session | `401 { error: 'Authentication required' }` |
#### `POST /api/auth/change-password`
Allows the authenticated user to change their own password.
| Aspect | Detail |
|--------|--------|
| Auth | `requireAuth(db)` |
| Rate limit | 5 requests per 15 minutes, keyed by `req.cookies.session_id` |
| Body | `{ currentPassword: string, newPassword: string }` |
| Validation | `newPassword.length >= 8` (server-side) |
| Flow | 1. Verify account active 2. `bcrypt.compare` current password 3. `bcrypt.hash` new password (cost 10) 4. `UPDATE users SET password_hash` 5. `logAudit` with action `password_change` |
| Success | `200 { message: 'Password changed successfully' }` |
| Wrong password | `401 { error: 'Current password is incorrect' }` |
| Too short | `400 { error: 'New password must be at least 8 characters' }` |
| Rate limited | `429 { error: 'Too many password change attempts. Please try again later.' }` |
| Inactive | `401 { error: 'Account is disabled' }` |
### Frontend: New Component `UserProfilePanel.js`
A modal component rendered when the user clicks "My Profile" in the UserMenu dropdown.
**Props:**
| Prop | Type | Description |
|------|------|-------------|
| `isOpen` | `boolean` | Controls modal visibility |
| `onClose` | `function` | Callback to close the modal |
**Internal State:**
| State | Type | Purpose |
|-------|------|---------|
| `profile` | `object \| null` | Profile data from API |
| `loading` | `boolean` | Loading state for profile fetch |
| `error` | `string \| null` | Error message from profile fetch |
| `currentPassword` | `string` | Current password field |
| `newPassword` | `string` | New password field |
| `confirmPassword` | `string` | Confirm password field |
| `changeLoading` | `boolean` | Loading state for password change |
| `changeError` | `string \| null` | Error from password change API |
| `changeSuccess` | `string \| null` | Success message after password change |
**Behavior:**
- On open (`isOpen` transitions to `true`), fetches `GET /api/auth/profile`
- Displays profile fields in a read-only info section
- Password change form validates client-side before submitting:
- New password and confirm must match
- New password must be >= 8 characters
- On successful password change, shows success message and clears form fields
- Click-outside or X button closes the modal
- Uses design system dark theme styling (intel-card background, accent borders, light text)
### Frontend: Modified Component `UserMenu.js`
**Changes:**
1. Add "My Profile" menu item with `User` icon between the dropdown header and admin actions
2. Convert all Tailwind light-theme classes to inline dark-theme styles:
- Button: `hover:bg-gray-100``hover: rgba(14, 165, 233, 0.1)`
- Username text: `text-gray-900``color: var(--text-primary)` / `#F8FAFC`
- Group text: `text-gray-500``color: var(--text-secondary)` / `#E2E8F0`
- Chevron: `text-gray-500``color: var(--text-secondary)` / `#E2E8F0`
- Dropdown panel: `bg-white` → intel-card gradient background
- Dropdown border: `border-gray-200``rgba(14, 165, 233, 0.3)`
- Menu items: `text-gray-700``color: var(--text-primary)` / `#F8FAFC`
- Menu hover: `hover:bg-gray-50``rgba(14, 165, 233, 0.1)`
- Sign out: `text-red-600``#F87171` (design system danger text)
3. Add state and handler for profile panel visibility
4. Render `UserProfilePanel` component
---
## Data Models
No new database tables or migrations are required. The feature reads from the existing `users` table:
```sql
-- Existing users table (relevant columns)
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
user_group VARCHAR(20) NOT NULL DEFAULT 'Read_Only',
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
);
```
### API Response Shapes
**GET /api/auth/profile response:**
```json
{
"id": 1,
"username": "admin",
"email": "admin@localhost",
"group": "Admin",
"created_at": "2026-01-15 10:30:00",
"last_login": "2026-07-20 14:22:00"
}
```
**POST /api/auth/change-password request:**
```json
{
"currentPassword": "oldpass123",
"newPassword": "newpass456"
}
```
**POST /api/auth/change-password success response:**
```json
{
"message": "Password changed successfully"
}
```
**Audit log entry for password change:**
```json
{
"userId": 1,
"username": "admin",
"action": "password_change",
"entityType": "auth",
"entityId": null,
"details": null,
"ipAddress": "::1"
}
```
---
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Profile panel displays all required fields
*For any* valid profile object (with arbitrary username, email, group, created_at, and last_login values), rendering the UserProfilePanel with that data SHALL result in all five field values being present in the rendered output.
**Validates: Requirements 1.2**
### Property 2: Profile API returns complete user data matching database
*For any* active user record in the database, a GET request to `/api/auth/profile` with that user's valid session SHALL return an object containing `id`, `username`, `email`, `group`, `created_at`, and `last_login` fields, where each value matches the corresponding column in the `users` table.
**Validates: Requirements 4.1**
### Property 3: Password change round-trip
*For any* valid current password and *any* new password of 8 or more characters, after a successful `POST /api/auth/change-password`, the stored `password_hash` in the database SHALL be a valid bcrypt hash and `bcrypt.compare(newPassword, storedHash)` SHALL return `true`.
**Validates: Requirements 2.2, 2.7**
### Property 4: Incorrect current password is always rejected
*For any* password string that does not match the user's current password, submitting it as `currentPassword` to `POST /api/auth/change-password` SHALL return HTTP 401 and the user's stored `password_hash` SHALL remain unchanged.
**Validates: Requirements 2.3**
### Property 5: Mismatched password confirmation is rejected client-side
*For any* two distinct strings used as `newPassword` and `confirmPassword` in the Password_Change_Form, the form SHALL display a validation error and SHALL NOT submit a request to the Auth_API.
**Validates: Requirements 2.4**
### Property 6: Short passwords are rejected at both client and server
*For any* string of length 0 through 7, the Password_Change_Form SHALL display a minimum-length validation error (client-side), and `POST /api/auth/change-password` SHALL return HTTP 400 (server-side). In both cases, the user's stored `password_hash` SHALL remain unchanged.
**Validates: Requirements 2.5, 5.4**
---
## Error Handling
### Backend Errors
| Scenario | HTTP Status | Response | Behavior |
|----------|-------------|----------|----------|
| No session cookie on `/profile` | 401 | `{ error: 'Authentication required' }` | Handled by `requireAuth` middleware |
| Expired session on `/profile` | 401 | `{ error: 'Session expired or invalid' }` | Handled by `requireAuth` middleware |
| Deactivated account on `/profile` | 401 | `{ error: 'Account is disabled' }` | Clear session cookie, return 401 |
| No session on `/change-password` | 401 | `{ error: 'Authentication required' }` | Handled by `requireAuth` middleware |
| Rate limit exceeded on `/change-password` | 429 | `{ error: 'Too many password change attempts. Please try again later.' }` | `express-rate-limit` middleware |
| Missing `currentPassword` or `newPassword` | 400 | `{ error: 'Current password and new password are required' }` | Early return validation |
| New password < 8 characters | 400 | `{ error: 'New password must be at least 8 characters' }` | Early return validation |
| Incorrect current password | 401 | `{ error: 'Current password is incorrect' }` | After `bcrypt.compare` fails |
| Deactivated account on `/change-password` | 401 | `{ error: 'Account is disabled' }` | Check `is_active` before processing |
| Database error during profile fetch | 500 | `{ error: 'Failed to fetch profile' }` | Catch block, log to console |
| Database error during password update | 500 | `{ error: 'Failed to change password' }` | Catch block, log to console |
### Frontend Error Handling
| Scenario | Behavior |
|----------|----------|
| Profile fetch fails (network error) | Display error message in panel, offer retry |
| Profile fetch returns 401 | Redirect to login (session expired) |
| Password change returns 401 (wrong password) | Display "Current password is incorrect" in form |
| Password change returns 429 | Display "Too many attempts. Please try again later." in form |
| Password change returns 400 | Display server validation error message in form |
| Password change returns 500 | Display "An error occurred. Please try again." in form |
| Client-side validation failure (mismatch) | Display "Passwords do not match" below confirm field |
| Client-side validation failure (too short) | Display "Password must be at least 8 characters" below new password field |
---
## Testing Strategy
### Property-Based Tests
This feature is suitable for property-based testing. The password change logic involves pure input/output behavior (password validation, hashing, comparison) with a large input space (arbitrary strings). The profile data retrieval has clear invariants (all fields present, values match database).
**Library:** [fast-check](https://github.com/dubzzz/fast-check) — the standard PBT library for JavaScript.
**Configuration:**
- Minimum 100 iterations per property test
- Each test tagged with: `Feature: user-profile, Property {number}: {property_text}`
**Properties to implement:**
1. Profile panel renders all fields (Property 1) — generate random profile objects, render component, assert all values present
2. Profile API returns complete data (Property 2) — generate random user records, insert into test DB, fetch profile, assert field match
3. Password change round-trip (Property 3) — generate random valid passwords, change password, verify bcrypt.compare succeeds
4. Wrong password rejection (Property 4) — generate random wrong passwords, attempt change, verify 401 and hash unchanged
5. Mismatched confirmation rejection (Property 5) — generate pairs of distinct strings, verify client validation rejects
6. Short password rejection (Property 6) — generate strings of length 0-7, verify rejection at both client and server
### Unit Tests (Example-Based)
| Test | What it verifies |
|------|-----------------|
| "My Profile" click opens panel | Requirement 1.1 — UI interaction |
| Close button dismisses panel | Requirement 1.3 — close mechanism |
| Click-outside dismisses panel | Requirement 1.3 — close mechanism |
| Password form has three fields | Requirement 2.1 — form structure |
| Success message shown after change | Requirement 2.6 — success feedback |
| Form fields cleared after success | Requirement 2.6 — form reset |
| Unauthenticated profile request returns 401 | Requirement 4.2 — auth guard |
| Deactivated account profile request returns 401 | Requirement 4.3 — account check |
| Deactivated account password change rejected | Requirement 5.3 — account check |
| Rate limit triggers after 5 attempts | Requirements 5.1, 5.2 — rate limiting |
### Integration Tests
| Test | What it verifies |
|------|-----------------|
| Audit log entry created on password change | Requirement 2.8 — audit logging |
| Rate limiter resets after 15-minute window | Requirement 5.1 — rate limit window |
### Manual Testing
| Test | What it verifies |
|------|-----------------|
| Username visible on dark header without hover | Requirement 3.1 — WCAG AA contrast |
| Group label visible on dark header | Requirement 3.2 — contrast |
| Hover state uses dark-themed highlight | Requirement 3.3 — theming |
| Chevron icon uses light color | Requirement 3.4 — theming |
| Dropdown uses dark background | Requirement 6.1 — theming |
| Dropdown text uses light colors | Requirement 6.2 — theming |
| Dropdown hover uses accent highlight | Requirement 6.3 — theming |
| Dropdown border uses accent style | Requirement 6.4 — theming |
| Group badge retains color coding | Requirement 6.5 — theming |