Initial
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user