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

17 KiB

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

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-100hover: rgba(14, 165, 233, 0.1)
    • Username text: text-gray-900color: var(--text-primary) / #F8FAFC
    • Group text: text-gray-500color: var(--text-secondary) / #E2E8F0
    • Chevron: text-gray-500color: var(--text-secondary) / #E2E8F0
    • Dropdown panel: bg-white → intel-card gradient background
    • Dropdown border: border-gray-200rgba(14, 165, 233, 0.3)
    • Menu items: text-gray-700color: var(--text-primary) / #F8FAFC
    • Menu hover: hover:bg-gray-50rgba(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:

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

{
  "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:

{
  "currentPassword": "oldpass123",
  "newPassword": "newpass456"
}

POST /api/auth/change-password success response:

{
  "message": "Password changed successfully"
}

Audit log entry for password change:

{
  "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 — 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