344 lines
10 KiB
Markdown
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.
|