Files
2026-04-11 00:04:09 -05:00

344 lines
10 KiB
Markdown

# User Management System
## Overview
A secure user management front-end that allows you to create and manage users. Each user record contains:
- **Username**: Unique identifier
- **Password**: Securely hashed using PBKDF2-SHA256 (industry-standard)
- **Default Location**: Primary location (required) - one of: Lindsborg, Iola, KC, or BMD
- **Active Status**: Whether the user account is active or inactive (toggleable in user list)
- **Location Settings**: Per-location configuration with:
- **Accessible**: Whether the user can access this location
## User Interface Features
### Table-Based Location Configuration
The form uses an intuitive table layout with:
- **Default Location Column**: Radio buttons to select ONE primary location (required)
- **Accessible Column**: Checkboxes to mark which locations the user can access
### User List with Active Toggle
Each user in the list shows:
- **Username** and **Location Information**
- **Active/Inactive Toggle**: Click to enable or disable the user account
- Active users have full border color
- Inactive users are dimmed with reduced opacity
- **Delete Button**: Remove the user permanently
### Smart Header Checkboxes
The accessible column has a header checkbox that:
- Shows three states: checked ✓, unchecked ☐, or indeterminate ⊟ (mixed)
- **Clicking cycles**: If off or mixed → all on, if on → all off
- **Auto-updates**: When you check/uncheck individual rows, the header shows:
- ✓ if all are checked
- ☐ if none are checked
- ⊟ if some are checked (mixed state)
### Extensible Design
The table structure is designed to easily add more columns in the future:
- Permission columns (Permission 1, Permission 2, etc.)
- Custom attributes
- Feature flags
- Any other per-location settings
Each new column can have the same header checkbox behavior.
## Security Features
- **Password Hashing**: Passwords are hashed using `pbkdf2:sha256` algorithm
- **Not Plain Text**: Passwords are never stored in plain text
- **Cryptographically Secure**: Uses Werkzeug's secure password hashing
- **Cannot Be Decoded**: Hashed passwords cannot be reversed back to plain text
## How to Use
### 1. Access the User Manager
Navigate to: `http://localhost:8080/users`
### 2. Add New Users
- Fill in the form with username, password, and location
- Click "Add User" button
- User will be added with a securely hashed password
### 3. View Users
- All users are displayed in a list showing username and location
- Password hashes are NOT displayed for security
### 4. Download Users JSON
- Click "📥 Download Users JSON" button
- Downloads a `users.json` file containing all users
- Password field contains the secure hash (not plain text)
### 5. Delete Users
- Click "Delete" next to any user to remove them
- Click "🗑️ Clear All Users" to remove all users at once
## API Endpoints
### GET /users
Displays the user management interface.
### GET /api/users
Returns all users in JSON format.
**Response:**
```json
{
"status": "success",
"users": [
{
"username": "john_doe",
"password": "pbkdf2:sha256:600000$...",
"defaultLocation": "LINDS",
"active": true,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": false },
"BMD": { "accessible": false }
}
}
]
}
```
### POST /api/users
Adds a new user.
**Request Body:**
```json
{
"username": "jane_smith",
"password": "mySecurePassword123",
"defaultLocation": "KC",
"active": true,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": true },
"BMD": { "accessible": true }
}
}
```
**Response:**
```json
{
"status": "success",
"message": "User added successfully",
"users": [...]
}
```
### DELETE /api/users/{index}
Deletes a user by their index position.
### PATCH /api/users/{index}/active
Toggle user active status.
**Request Body:**
```json
{
"active": true
}
```
### DELETE /api/users/clear
Clears all users from the system.
### GET /api/users/download
Downloads all users as a JSON file.
## Password Security
### How Passwords Are Stored
Passwords are hashed using PBKDF2-SHA256 with the following properties:
- **Algorithm**: PBKDF2 (Password-Based Key Derivation Function 2)
- **Hash Function**: SHA-256
- **Iterations**: 600,000+ (computationally expensive for attackers)
- **Salt**: Automatically generated unique salt per password
### Example Hash Format
```
pbkdf2:sha256:600000$AbCdEfGh$1234567890abcdef...
```
Components:
- `pbkdf2:sha256` - Algorithm identifier
- `600000` - Number of iterations
- `$AbCdEfGh` - Random salt
- `$1234567890abcdef...` - Actual hash
### Password Verification
To verify a password, use Werkzeug's `check_password_hash()`:
```python
from werkzeug.security import check_password_hash
# user['password'] contains the hash
if check_password_hash(user['password'], provided_password):
print("Password is correct!")
```
## Data Storage
Users are stored in: `app/data/users.json`
**Example users.json:**
```json
[
{
"username": "admin",
"password": "pbkdf2:sha256:600000$r7K8L9M0$a1b2c3d4e5f6...",
"defaultLocation": "LINDS",
"active": true,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": false },
"BMD": { "accessible": false }
}
},
{
"username": "user1",
"password": "pbkdf2:sha256:600000$n5O6P7Q8$x9y8z7w6v5u4...",
"defaultLocation": "KC",
"active": false,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": true },
"BMD": { "accessible": true }
}
}
]
```
### Location Codes
- **LINDS** = Lindsborg
- **IOLA** = Iola
- **KC** = KC
- **BMD** = BMD
## Integration Example
### Authenticate and Get User Info
```python
from werkzeug.security import check_password_hash
import json
def authenticate_user(username, password):
"""Authenticate a user by username and password"""
with open('app/data/users.json', 'r') as f:
users = json.load(f)
# Find user
user = next((u for u in users if u['username'] == username), None)
if user and check_password_hash(user['password'], password):
return True, user
return False, None
# Usage
success, user_data = authenticate_user('john_doe', 'password123')
if success:
print(f"Welcome {user_data['username']}!")
print(f"Default location: {user_data['defaultLocation']}")
print(f"Account active: {user_data.get('active', True)}")
# Check if user can access a location
if user_data['locationSettings']['KC']['accessible']:
print("User can access KC")
```
### Check User Permissions for a Location
```python
def can_access_location(user_data, location_code):
"""Check if user can access a specific location"""
return user_data.get('locationSettings', {}).get(location_code, {}).get('accessible', False)
def is_user_active(user_data):
"""Check if user account is active"""
return user_data.get('active', True)
# Usage
if not is_user_active(user_data):
print("User account is inactive")
return
if can_access_location(user_data, 'LINDS'):
print("User can access Lindsborg")
```
### Get All Accessible Locations for a User
```python
def get_accessible_locations(user_data):
"""Get all locations where user has access"""
accessible_locations = []
for location_code, settings in user_data.get('locationSettings', {}).items():
if settings.get('accessible', False):
accessible_locations.append(location_code)
return accessible_locations
# Usage
accessible = get_accessible_locations(user_data)
print(f"User can access: {', '.join(accessible)}")
```
## Notes
- Username must be unique
- Minimum password length: 6 characters
- Default location is required (one of: Lindsborg, Iola, KC, BMD)
- **Default location is automatically marked as accessible** when creating a user
- Accessible checkboxes are optional for other locations
- New users are **active by default**
- Toggle active status in the user list section
- Users are stored locally in JSON format
- This is a separate endpoint from the main Product Finder app
## Adding New Permissions/Columns
The system is designed to be easily extensible. To add new permission columns:
### 1. Update the HTML table
Add a new column header and cells in `user_manager.html`:
```html
<!-- In the table header -->
<th class="checkbox-header" onclick="toggleHeaderCheckbox('newPermission')">
<input type="checkbox" id="headerNewPermission"
onclick="event.stopPropagation(); toggleAllCheckboxes('newPermission')">
Permission Name
</th>
<!-- In each table row -->
<td class="checkbox-cell">
<input type="checkbox" class="newPermission-checkbox"
data-location="LINDS" onchange="updateHeaderCheckbox('newPermission')">
</td>
```
### 2. Update the JavaScript form submission
Modify the form submission handler to collect the new permission:
```javascript
locationSettings[loc.code] = {
active: activeCheckbox ? activeCheckbox.checked : false,
accessible: accessibleCheckbox ? accessibleCheckbox.checked : false,
newPermission: newPermCheckbox ? newPermCheckbox.checked : false // Add this
};
```
### 3. Update the backend (optional)
The backend already handles any properties in `locationSettings`, so no changes are required unless you want validation.
### 4. Reset header checkbox on form submit
Add to the form reset section:
```javascript
document.getElementById('headerNewPermission').checked = false;
document.getElementById('headerNewPermission').indeterminate = false;
```
That's it! The system will automatically save and load the new permission data.