Pre-github
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Blueprint package
|
||||
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Authentication Blueprint
|
||||
Handles login, logout, and session management
|
||||
"""
|
||||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for
|
||||
from werkzeug.security import check_password_hash
|
||||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
# Path to users file
|
||||
USERS_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'users.json')
|
||||
|
||||
def load_users():
|
||||
"""Load users from JSON file"""
|
||||
if os.path.exists(USERS_FILE):
|
||||
try:
|
||||
with open(USERS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return []
|
||||
return []
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require login for routes"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'user_id' not in session:
|
||||
return redirect(url_for('auth.login_page'))
|
||||
|
||||
# Check if user is active
|
||||
users = load_users()
|
||||
if session['user_id'] >= len(users):
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login_page', message='User not found'))
|
||||
|
||||
user = users[session['user_id']]
|
||||
if not user.get('active', True):
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login_page', message='Account is inactive'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def get_current_user():
|
||||
"""Get the currently logged in user"""
|
||||
if 'user_id' not in session:
|
||||
return None
|
||||
|
||||
users = load_users()
|
||||
if session['user_id'] >= len(users):
|
||||
return None
|
||||
|
||||
return users[session['user_id']]
|
||||
|
||||
def can_user(permission, location=None):
|
||||
"""
|
||||
Check if current user has a specific permission.
|
||||
|
||||
Args:
|
||||
permission (str): Permission name (e.g., 'manage_users')
|
||||
location (str, optional): Check permission for specific location
|
||||
|
||||
Returns:
|
||||
bool: True if user has permission
|
||||
"""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
if not user.get('active', True):
|
||||
return False
|
||||
|
||||
# Check global permissions
|
||||
global_permissions = user.get('permissions', {})
|
||||
if permission in global_permissions:
|
||||
return global_permissions[permission] is True
|
||||
|
||||
if location == 'global':
|
||||
return False
|
||||
|
||||
# Check location-specific permissions
|
||||
check_location = location if location else session.get('currentLocation')
|
||||
if not check_location:
|
||||
return False
|
||||
|
||||
location_settings = user.get('locationSettings', {})
|
||||
if check_location in location_settings:
|
||||
loc_permissions = location_settings[check_location].get('permissions', {})
|
||||
if permission in loc_permissions:
|
||||
return loc_permissions[permission] is True
|
||||
|
||||
return False
|
||||
|
||||
# Routes
|
||||
@auth_bp.route('/login')
|
||||
def login_page():
|
||||
"""Serve the login page"""
|
||||
if 'user_id' in session:
|
||||
return redirect(url_for('home'))
|
||||
return render_template('login.html')
|
||||
|
||||
@auth_bp.route('/select-location')
|
||||
def select_location_page():
|
||||
"""Serve the location selection page"""
|
||||
if 'user_id' not in session:
|
||||
return redirect(url_for('auth.login_page'))
|
||||
return render_template('select_location.html')
|
||||
|
||||
@auth_bp.route('/api/login', methods=['POST'])
|
||||
def login():
|
||||
"""Authenticate user and create session"""
|
||||
try:
|
||||
data = request.json
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Username and password are required'
|
||||
}), 400
|
||||
|
||||
users = load_users()
|
||||
|
||||
# Find user by username
|
||||
user_index = None
|
||||
user = None
|
||||
for i, u in enumerate(users):
|
||||
if u['username'] == username:
|
||||
user_index = i
|
||||
user = u
|
||||
break
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid username or password'
|
||||
}), 401
|
||||
|
||||
# Check if user is active
|
||||
if not user.get('active', True):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Account is inactive. Please contact an administrator.'
|
||||
}), 403
|
||||
|
||||
# Verify password
|
||||
if not check_password_hash(user['password'], password):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid username or password'
|
||||
}), 401
|
||||
|
||||
# Create session
|
||||
session['user_id'] = user_index
|
||||
session['username'] = user['username']
|
||||
session['defaultLocation'] = user.get('defaultLocation')
|
||||
|
||||
# Get accessible locations
|
||||
accessible_locations = []
|
||||
if 'locationSettings' in user:
|
||||
for loc_code, settings in user['locationSettings'].items():
|
||||
if settings.get('accessible', False):
|
||||
accessible_locations.append(loc_code)
|
||||
|
||||
session['accessibleLocations'] = accessible_locations
|
||||
|
||||
# Check if user needs to select a location
|
||||
requiresLocationSelection = len(accessible_locations) > 1
|
||||
|
||||
# If only one location or no accessible locations, auto-select default
|
||||
if not requiresLocationSelection:
|
||||
session['currentLocation'] = user.get('defaultLocation')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Login successful',
|
||||
'requiresLocationSelection': requiresLocationSelection
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/api/select-location', methods=['POST'])
|
||||
def select_location():
|
||||
"""Select a location for the current session"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not logged in'
|
||||
}), 401
|
||||
|
||||
try:
|
||||
data = request.json
|
||||
location = data.get('location')
|
||||
|
||||
# Verify user has access to this location
|
||||
accessible = session.get('accessibleLocations', [])
|
||||
if location not in accessible:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'You do not have access to this location'
|
||||
}), 403
|
||||
|
||||
session['currentLocation'] = location
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Location selected',
|
||||
'currentLocation': location
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
def logout():
|
||||
"""Log out user and clear session"""
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login_page'))
|
||||
|
||||
@auth_bp.route('/api/session', methods=['GET'])
|
||||
def get_session():
|
||||
"""Get current session information"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not logged in'
|
||||
}), 401
|
||||
|
||||
# Get user permissions
|
||||
user = get_current_user()
|
||||
permissions = {}
|
||||
if user:
|
||||
permissions = user.get('permissions', {})
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'username': session.get('username'),
|
||||
'defaultLocation': session.get('defaultLocation'),
|
||||
'currentLocation': session.get('currentLocation'),
|
||||
'accessibleLocations': session.get('accessibleLocations', []),
|
||||
'permissions': permissions
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Product Finder Blueprint
|
||||
Handles the product quiz and recommendations
|
||||
"""
|
||||
from flask import Blueprint, render_template, send_from_directory
|
||||
from blueprints.auth import login_required
|
||||
import os
|
||||
|
||||
products_bp = Blueprint('products', __name__, url_prefix='/quiz')
|
||||
|
||||
# Get base directory
|
||||
basedir = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
@products_bp.route('/')
|
||||
@login_required
|
||||
def product_finder():
|
||||
"""Serve the product finder quiz page"""
|
||||
return render_template('product_finder.html')
|
||||
|
||||
@products_bp.route('/css/<path:filename>')
|
||||
def serve_css(filename):
|
||||
"""Serve CSS files"""
|
||||
return send_from_directory(os.path.join(basedir, 'static', 'css'), filename)
|
||||
|
||||
@products_bp.route('/js/<path:filename>')
|
||||
def serve_js(filename):
|
||||
"""Serve JavaScript files"""
|
||||
return send_from_directory(os.path.join(basedir, 'static', 'js'), filename)
|
||||
|
||||
@products_bp.route('/images/<path:filename>')
|
||||
def serve_images(filename):
|
||||
"""Serve image files"""
|
||||
return send_from_directory(os.path.join(basedir, 'static', 'images'), filename)
|
||||
|
||||
@products_bp.route('/data/<path:filename>')
|
||||
def serve_data(filename):
|
||||
"""Serve data files (JSON)"""
|
||||
return send_from_directory(os.path.join(basedir, 'data'), filename)
|
||||
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
User Management Blueprint
|
||||
Handles user CRUD operations
|
||||
"""
|
||||
from flask import Blueprint, render_template, request, jsonify, send_file, session
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from blueprints.auth import login_required, can_user, get_current_user
|
||||
from io import BytesIO
|
||||
import json
|
||||
import os
|
||||
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
# Path to users file
|
||||
USERS_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'users.json')
|
||||
|
||||
def load_users():
|
||||
"""Load users from JSON file"""
|
||||
if os.path.exists(USERS_FILE):
|
||||
try:
|
||||
with open(USERS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return []
|
||||
return []
|
||||
|
||||
def save_users(users):
|
||||
"""Save users to JSON file"""
|
||||
os.makedirs(os.path.dirname(USERS_FILE), exist_ok=True)
|
||||
with open(USERS_FILE, 'w') as f:
|
||||
json.dump(users, f, indent=2)
|
||||
|
||||
def permission_required(permission):
|
||||
"""Decorator to require specific permission"""
|
||||
from functools import wraps
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not can_user(permission):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Permission denied'
|
||||
}), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
# Routes
|
||||
@users_bp.route('/')
|
||||
@login_required
|
||||
def user_manager():
|
||||
"""Serve the user management page"""
|
||||
if not can_user('manage_users'):
|
||||
return "<h1>403 - Permission Denied</h1><p>You don't have permission to manage users.</p>", 403
|
||||
current_user_id = session.get('user_id', -1)
|
||||
return render_template('user_manager.html', current_user_id=current_user_id)
|
||||
|
||||
@users_bp.route('/api', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def get_users():
|
||||
"""Get all users"""
|
||||
users = load_users()
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'users': users
|
||||
})
|
||||
|
||||
@users_bp.route('/api', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def add_user():
|
||||
"""Add a new user with hashed password"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# Validate required fields
|
||||
if not data.get('username') or not data.get('password') or not data.get('defaultLocation'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Username, password, and default location are required'
|
||||
}), 400
|
||||
|
||||
users = load_users()
|
||||
|
||||
# Check if username already exists
|
||||
if any(user['username'] == data['username'] for user in users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Username already exists'
|
||||
}), 400
|
||||
|
||||
# Hash the password
|
||||
hashed_password = generate_password_hash(data['password'], method='pbkdf2:sha256')
|
||||
|
||||
# Get location settings
|
||||
location_settings = data.get('locationSettings', {})
|
||||
default_location = data['defaultLocation']
|
||||
|
||||
# Ensure default location is accessible
|
||||
if default_location not in location_settings:
|
||||
location_settings[default_location] = {}
|
||||
location_settings[default_location]['accessible'] = True
|
||||
|
||||
# Create new user
|
||||
new_user = {
|
||||
'username': data['username'],
|
||||
'password': hashed_password,
|
||||
'defaultLocation': default_location,
|
||||
'locationSettings': location_settings,
|
||||
'permissions': data.get('permissions', {}),
|
||||
'active': data.get('active', True)
|
||||
}
|
||||
|
||||
users.append(new_user)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'User added successfully',
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@users_bp.route('/api/<int:user_index>', methods=['DELETE'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def delete_user(user_index):
|
||||
"""Delete a specific user by index"""
|
||||
try:
|
||||
users = load_users()
|
||||
|
||||
if user_index < 0 or user_index >= len(users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid user index'
|
||||
}), 400
|
||||
|
||||
users.pop(user_index)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'User deleted successfully',
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@users_bp.route('/api/<int:user_index>/active', methods=['PATCH'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def toggle_user_active(user_index):
|
||||
"""Toggle user active status"""
|
||||
try:
|
||||
data = request.json
|
||||
users = load_users()
|
||||
|
||||
if user_index < 0 or user_index >= len(users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid user index'
|
||||
}), 400
|
||||
|
||||
users[user_index]['active'] = data.get('active', True)
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'User status updated',
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@users_bp.route('/api/<int:user_index>/change-password', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def change_user_password(user_index):
|
||||
"""Change a user's password with current user verification"""
|
||||
try:
|
||||
data = request.json
|
||||
new_password = data.get('newPassword')
|
||||
current_user_password = data.get('currentUserPassword')
|
||||
|
||||
if not new_password or not current_user_password:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'New password and current user password are required'
|
||||
}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Password must be at least 6 characters long'
|
||||
}), 400
|
||||
|
||||
# Verify current user's password
|
||||
current_user = get_current_user()
|
||||
if not current_user:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not authenticated'
|
||||
}), 401
|
||||
|
||||
if not check_password_hash(current_user['password'], current_user_password):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Your password is incorrect. Password change denied for security reasons.'
|
||||
}), 403
|
||||
|
||||
# Load users and validate index
|
||||
users = load_users()
|
||||
if user_index < 0 or user_index >= len(users):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Invalid user index'
|
||||
}), 400
|
||||
|
||||
# Update password
|
||||
hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256')
|
||||
target_username = users[user_index]['username']
|
||||
users[user_index]['password'] = hashed_password
|
||||
save_users(users)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Password changed successfully for user "{target_username}"'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@users_bp.route('/api/download')
|
||||
@login_required
|
||||
@permission_required('manage_users')
|
||||
def download_users():
|
||||
"""Download users as JSON file"""
|
||||
try:
|
||||
users = load_users()
|
||||
|
||||
if not users:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No users to download'
|
||||
}), 400
|
||||
|
||||
json_data = json.dumps(users, indent=2)
|
||||
|
||||
return send_file(
|
||||
BytesIO(json_data.encode()),
|
||||
mimetype='application/json',
|
||||
as_attachment=True,
|
||||
download_name='users.json'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
Reference in New Issue
Block a user