Add Kiro specs, hooks, and steering files — development tooling archive
This commit is contained in:
1
.kiro/specs/user-profile/.config.kiro
Normal file
1
.kiro/specs/user-profile/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "74f6201d-ed0f-4df3-86a2-4a0767dd497c", "workflowType": "requirements-first", "specType": "feature"}
|
||||
361
.kiro/specs/user-profile/design.md
Normal file
361
.kiro/specs/user-profile/design.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# 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 |
|
||||
87
.kiro/specs/user-profile/requirements.md
Normal file
87
.kiro/specs/user-profile/requirements.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
The STEAM Security Dashboard currently lacks a self-service user profile. Users cannot view their own account details or change their own password — only admins can reset passwords through the User Management panel. Additionally, the username text in the top-right UserMenu is rendered in black (`text-gray-900`) against the dark dashboard background, making it invisible until hovered. This feature adds a user profile panel accessible from the UserMenu, enables self-service password changes for all authenticated users, and fixes the username visibility issue in the header.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Dashboard**: The STEAM Security Dashboard application, consisting of a React 19 SPA frontend and a Node.js/Express backend with SQLite3 storage.
|
||||
- **User_Profile_Panel**: A modal or slide-over panel that displays the authenticated user's account information and provides a password change form.
|
||||
- **UserMenu**: The existing dropdown component (`UserMenu.js`) in the top-right corner of the header that shows the user icon, username, group badge, and navigation actions (Manage Users, Audit Log, Sign Out).
|
||||
- **Password_Change_Form**: A form within the User_Profile_Panel that accepts the current password and a new password (with confirmation) to allow users to change their own credentials.
|
||||
- **Auth_API**: The backend Express routes under `/api/auth` that handle login, logout, session validation, and (with this feature) self-service password changes.
|
||||
- **Authenticated_User**: Any user with a valid, non-expired session cookie and an active account.
|
||||
- **Header_Username_Display**: The text element in the UserMenu button that shows the current user's username and group label.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: User Profile Panel Access
|
||||
|
||||
**User Story:** As an authenticated user, I want to access a profile panel from the UserMenu dropdown, so that I can view my account details without needing admin assistance.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the Authenticated_User clicks the "My Profile" option in the UserMenu dropdown, THE Dashboard SHALL display the User_Profile_Panel.
|
||||
2. THE User_Profile_Panel SHALL display the following account fields: username, email address, user group, account creation date, and last login timestamp.
|
||||
3. WHEN the User_Profile_Panel is open, THE Dashboard SHALL provide a visible close mechanism (close button or click-outside) to dismiss the panel.
|
||||
4. THE User_Profile_Panel SHALL use the dark theme styling defined in the Dashboard design system (intel-card backgrounds, accent borders, light text colors).
|
||||
|
||||
### Requirement 2: Self-Service Password Change
|
||||
|
||||
**User Story:** As an authenticated user, I want to change my own password from my profile, so that I can maintain my account security without requesting an admin to reset it.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE User_Profile_Panel SHALL include a Password_Change_Form with three fields: current password, new password, and confirm new password.
|
||||
2. WHEN the Authenticated_User submits the Password_Change_Form with a valid current password and matching new password fields, THE Auth_API SHALL update the password hash for that user in the database.
|
||||
3. WHEN the Authenticated_User submits the Password_Change_Form with an incorrect current password, THE Auth_API SHALL return an error and THE Password_Change_Form SHALL display a message stating the current password is incorrect.
|
||||
4. WHEN the new password and confirm new password fields do not match, THE Password_Change_Form SHALL display a validation error before submitting to the Auth_API.
|
||||
5. WHEN the new password is fewer than 8 characters, THE Password_Change_Form SHALL display a validation error stating the minimum length requirement.
|
||||
6. WHEN a password change succeeds, THE Dashboard SHALL display a success confirmation message and clear the Password_Change_Form fields.
|
||||
7. THE Auth_API SHALL hash the new password using bcryptjs before storing it in the database.
|
||||
8. WHEN a password change succeeds, THE Auth_API SHALL log an audit entry with action `password_change` for the Authenticated_User.
|
||||
|
||||
### Requirement 3: Header Username Visibility Fix
|
||||
|
||||
**User Story:** As a user, I want to see my username in the top-right header area at all times, so that I can confirm which account is logged in without hovering.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Header_Username_Display SHALL render the username text using a light color (design system `--text-primary` or equivalent) that meets WCAG AA contrast ratio against the dark header background.
|
||||
2. THE Header_Username_Display SHALL render the group label text using a secondary light color (design system `--text-secondary` or equivalent) that is visible against the dark header background.
|
||||
3. THE UserMenu button hover state SHALL use a dark-themed highlight (e.g., `rgba(14, 165, 233, 0.1)`) instead of the current light-themed `hover:bg-gray-100`.
|
||||
4. THE UserMenu dropdown chevron icon SHALL use a light color consistent with the header text colors.
|
||||
|
||||
### Requirement 4: Profile API Endpoint
|
||||
|
||||
**User Story:** As a frontend developer, I want a dedicated API endpoint that returns the full profile data for the currently authenticated user, so that the User_Profile_Panel can display account details not available in the session payload.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the Authenticated_User requests their profile, THE Auth_API SHALL return the user's id, username, email, user group, account creation date, and last login timestamp.
|
||||
2. IF an unauthenticated request is made to the profile endpoint, THEN THE Auth_API SHALL return HTTP 401 with an error message.
|
||||
3. IF the Authenticated_User's account has been deactivated, THEN THE Auth_API SHALL return HTTP 401 and clear the session cookie.
|
||||
|
||||
### Requirement 5: Password Change Security
|
||||
|
||||
**User Story:** As a security-conscious administrator, I want password changes to be rate-limited and validated, so that brute-force attempts against the current password field are mitigated.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Auth_API SHALL enforce a rate limit on the password change endpoint of no more than 5 attempts per 15-minute window per session.
|
||||
2. IF the rate limit is exceeded, THEN THE Auth_API SHALL return HTTP 429 with a message indicating the user should try again later.
|
||||
3. THE Auth_API SHALL verify that the Authenticated_User's account is active before processing a password change.
|
||||
4. THE Auth_API SHALL validate that the new password is at least 8 characters long on the server side.
|
||||
|
||||
### Requirement 6: UserMenu Dropdown Theming
|
||||
|
||||
**User Story:** As a user, I want the UserMenu dropdown to match the dark dashboard theme, so that the interface is visually consistent.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE UserMenu dropdown panel SHALL use a dark background consistent with the Dashboard design system (intel-card gradient or equivalent dark surface).
|
||||
2. THE UserMenu dropdown text items SHALL use light text colors from the design system (`--text-primary` for labels, `--text-secondary` for metadata).
|
||||
3. THE UserMenu dropdown hover states SHALL use a subtle accent highlight (e.g., `rgba(14, 165, 233, 0.1)`) instead of the current light-themed `hover:bg-gray-50`.
|
||||
4. THE UserMenu dropdown border SHALL use the design system accent border style (`rgba(14, 165, 233, 0.3)` or equivalent).
|
||||
5. THE UserMenu group badge in the dropdown header SHALL retain its existing color-coded styling for group identification.
|
||||
119
.kiro/specs/user-profile/tasks.md
Normal file
119
.kiro/specs/user-profile/tasks.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Implementation Plan: User Profile
|
||||
|
||||
## Overview
|
||||
|
||||
This plan implements the user profile feature in three phases: backend API routes first (profile endpoint and password change endpoint on the existing auth router), then the frontend components (UserProfilePanel modal and UserMenu theming/integration), and finally wiring everything together. Each task builds incrementally on the previous one, and testing tasks are placed close to the code they validate.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] 1. Add backend profile and password change routes to `routes/auth.js`
|
||||
- [x] 1.1 Add `GET /api/auth/profile` route
|
||||
- Add a new route inside `createAuthRouter` that queries the `users` table for `id, username, email, user_group, created_at, last_login` using the session user's ID
|
||||
- Return `{ id, username, email, group, created_at, last_login }` on success
|
||||
- Return 401 if the account is inactive (with `is_active = 0`), clearing the session cookie
|
||||
- Use the existing `requireAuth(db)` middleware for authentication
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 1.2 Add `POST /api/auth/change-password` route with rate limiting
|
||||
- Add `express-rate-limit` middleware scoped to this route: 5 requests per 15-minute window, keyed by `req.cookies.session_id`
|
||||
- Validate request body has `currentPassword` and `newPassword` fields; return 400 if missing
|
||||
- Validate `newPassword` is at least 8 characters; return 400 if too short
|
||||
- Query the user's `password_hash` and `is_active` from the database; return 401 if account is inactive
|
||||
- Use `bcrypt.compare` to verify `currentPassword`; return 401 if incorrect
|
||||
- Hash the new password with `bcrypt.hash(newPassword, 10)` and update the `password_hash` column
|
||||
- Call `logAudit` with action `password_change`, entityType `auth`
|
||||
- Return `{ message: 'Password changed successfully' }` on success
|
||||
- Return 429 with appropriate message when rate limit is exceeded
|
||||
- _Requirements: 2.2, 2.3, 2.7, 2.8, 5.1, 5.2, 5.3, 5.4_
|
||||
|
||||
- [x] 1.3 Write property tests for password change round-trip (backend)
|
||||
- **Property 3: Password change round-trip** — For any valid current password and any new password of 8+ characters, after a successful change, `bcrypt.compare(newPassword, storedHash)` returns true
|
||||
- **Validates: Requirements 2.2, 2.7**
|
||||
|
||||
- [x] 1.4 Write property tests for incorrect password rejection (backend)
|
||||
- **Property 4: Incorrect current password is always rejected** — For any password string that does not match the user's current password, the endpoint returns 401 and the stored hash remains unchanged
|
||||
- **Validates: Requirements 2.3**
|
||||
|
||||
- [x] 1.5 Write property tests for short password rejection (backend)
|
||||
- **Property 6 (server-side): Short passwords are rejected** — For any string of length 0–7, `POST /api/auth/change-password` returns 400 and the stored hash remains unchanged
|
||||
- **Validates: Requirements 2.5, 5.4**
|
||||
|
||||
- [x] 2. Checkpoint — Verify backend routes
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 3. Create `UserProfilePanel.js` frontend component
|
||||
- [x] 3.1 Create the `UserProfilePanel` modal component
|
||||
- Create `frontend/src/components/UserProfilePanel.js`
|
||||
- Accept `isOpen` and `onClose` props
|
||||
- On open, fetch `GET /api/auth/profile` with `credentials: 'include'` and display loading state
|
||||
- Render profile info section showing: username, email, group, created_at (formatted), last_login (formatted)
|
||||
- Use dark theme inline styles matching the design system (intel-card gradient background, accent borders, light text colors from `DESIGN_SYSTEM.md`)
|
||||
- Include a close button (X icon from lucide-react) and click-outside-to-close behavior
|
||||
- Display error state with retry option if profile fetch fails
|
||||
- Use icons from lucide-react (User, Mail, Shield, Calendar, Clock)
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4_
|
||||
|
||||
- [x] 3.2 Add password change form to `UserProfilePanel`
|
||||
- Add a password change section below the profile info with three fields: current password, new password, confirm new password
|
||||
- Implement client-side validation: new password must match confirm password; new password must be >= 8 characters
|
||||
- Display inline validation errors below the relevant fields
|
||||
- On submit, call `POST /api/auth/change-password` with `{ currentPassword, newPassword }`
|
||||
- Handle API error responses (401 wrong password, 429 rate limited, 400 validation, 500 server error) and display appropriate messages
|
||||
- On success, show success message and clear all form fields
|
||||
- Style the form with dark theme inline styles (intel-input styling, intel-button-primary for submit)
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
|
||||
|
||||
- [x] 3.3 Write property test for profile panel field rendering
|
||||
- **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 UserProfilePanel displays all five values in the output
|
||||
- **Validates: Requirements 1.2**
|
||||
|
||||
- [x] 3.4 Write property test for mismatched password confirmation
|
||||
- **Property 5: Mismatched password confirmation is rejected client-side** — For any two distinct strings used as newPassword and confirmPassword, the form displays a validation error and does not submit a request
|
||||
- **Validates: Requirements 2.4**
|
||||
|
||||
- [x] 3.5 Write property test for short password client-side rejection
|
||||
- **Property 6 (client-side): Short passwords are rejected** — For any string of length 0–7, the form displays a minimum-length validation error and does not submit a request
|
||||
- **Validates: Requirements 2.5**
|
||||
|
||||
- [x] 4. Checkpoint — Verify frontend component
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 5. Modify `UserMenu.js` for dark theming and profile integration
|
||||
- [x] 5.1 Convert `UserMenu.js` from light theme to dark theme
|
||||
- Replace Tailwind light-theme classes with inline dark-theme style objects
|
||||
- Button: `hover:bg-gray-100` → `rgba(14, 165, 233, 0.1)` hover background
|
||||
- Username text: `text-gray-900` → `#F8FAFC` (design system `--text-primary`)
|
||||
- Group label text: `text-gray-500` → `#E2E8F0` (design system `--text-secondary`)
|
||||
- Chevron icon: `text-gray-500` → `#E2E8F0`
|
||||
- Dropdown panel: `bg-white` → intel-card gradient background; `border-gray-200` → `rgba(14, 165, 233, 0.3)`
|
||||
- Dropdown items: `text-gray-700` → `#F8FAFC`; `hover:bg-gray-50` → `rgba(14, 165, 233, 0.1)`
|
||||
- Sign out text: `text-red-600` → `#F87171`; `hover:bg-red-50` → `rgba(239, 68, 68, 0.1)`
|
||||
- Dropdown header: `text-gray-900` → `#F8FAFC`; `text-gray-500` → `#94A3B8`; `border-gray-100` → `rgba(14, 165, 233, 0.2)`
|
||||
- Retain existing group badge color-coding logic
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
|
||||
- [x] 5.2 Add "My Profile" menu item and wire `UserProfilePanel`
|
||||
- Import `UserProfilePanel` component
|
||||
- Add `showProfile` state variable
|
||||
- Add a "My Profile" menu item with `User` icon between the dropdown header and the admin-only actions
|
||||
- On click, close the dropdown and set `showProfile` to `true`
|
||||
- Render `<UserProfilePanel isOpen={showProfile} onClose={() => setShowProfile(false)} />` in the component output
|
||||
- _Requirements: 1.1, 1.3_
|
||||
|
||||
- [x] 5.3 Write property test for profile API data completeness
|
||||
- **Property 2: Profile API returns complete user data matching database** — For any active user record, a GET request to `/api/auth/profile` with that user's valid session returns an object with `id`, `username`, `email`, `group`, `created_at`, and `last_login` fields matching the database
|
||||
- **Validates: Requirements 4.1**
|
||||
|
||||
- [x] 6. Final checkpoint — Ensure all tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- Each task references specific requirements for traceability
|
||||
- Checkpoints ensure incremental validation
|
||||
- Property tests use `fast-check` (already installed in `frontend/package.json` as a devDependency)
|
||||
- Backend tests for properties 3, 4, and 6 (server-side) will need `fast-check` added to the root `package.json` devDependencies
|
||||
- The existing `express-rate-limit` package (already in root `package.json`) is used for the password change rate limiter
|
||||
- No database migrations are needed — the existing `users` table has all required columns
|
||||
- All styling follows the dark theme design system documented in `DESIGN_SYSTEM.md`
|
||||
Reference in New Issue
Block a user