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
-
Profile endpoint on the auth router — The profile data is user-scoped and session-authenticated, so it belongs alongside
/api/auth/merather than on the admin-only/api/usersrouter. This avoids granting non-admin users access to the users management routes. -
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-passwordkeeps it separate from the adminPATCH /api/users/:idendpoint that already supports admin-initiated password resets. -
Rate limiting via express-rate-limit — The project already uses
express-rate-limitfor 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 customkeyGenerator. -
No new database tables or migrations — All required data already exists in the
userstable. Thecreated_atandlast_logincolumns are present in the schema. No schema changes are needed. -
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.
-
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 (
isOpentransitions totrue), fetchesGET /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:
- Add "My Profile" menu item with
Usericon between the dropdown header and admin actions - 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)
- Button:
- Add state and handler for profile panel visibility
- Render
UserProfilePanelcomponent
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:
- Profile panel renders all fields (Property 1) — generate random profile objects, render component, assert all values present
- Profile API returns complete data (Property 2) — generate random user records, insert into test DB, fetch profile, assert field match
- Password change round-trip (Property 3) — generate random valid passwords, change password, verify bcrypt.compare succeeds
- Wrong password rejection (Property 4) — generate random wrong passwords, attempt change, verify 401 and hash unchanged
- Mismatched confirmation rejection (Property 5) — generate pairs of distinct strings, verify client validation rejects
- 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 |