Initial commit - Mold Cost Calculator Application (Security Fixed)
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app, jsonify, make_response
|
||||
from flask_login import login_required, current_user
|
||||
from app import create_app, db
|
||||
from ..forms import QuotationForm
|
||||
from ..models import User, Quotation, CostFactor, CostFactorHistory
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from sqlalchemy import desc, text
|
||||
import logging
|
||||
from app import limiter, cache
|
||||
|
||||
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@bp.before_request
|
||||
def before_request():
|
||||
if not current_user.is_authenticated or not current_user.is_admin:
|
||||
flash('Access denied. Admin privileges required.', 'danger')
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
@bp.route('/dashboard')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
@cache.cached(timeout=60, key_prefix='admin_dashboard')
|
||||
def dashboard():
|
||||
try:
|
||||
# Get statistics
|
||||
total_users = User.query.count()
|
||||
total_quotations = Quotation.query.count()
|
||||
recent_quotations = Quotation.query.order_by(desc(Quotation.created_at)).limit(5).all()
|
||||
|
||||
# Get user statistics
|
||||
active_users = User.query.filter(
|
||||
User.last_login >= datetime.utcnow() - timedelta(days=30)
|
||||
).count()
|
||||
|
||||
# Get quotation statistics by country (using mold_shop_country)
|
||||
country_stats = db.session.query(
|
||||
Quotation.mold_shop_country,
|
||||
db.func.count(Quotation.id)
|
||||
).group_by(Quotation.mold_shop_country).all()
|
||||
|
||||
return render_template('admin/dashboard.html',
|
||||
total_users=total_users,
|
||||
total_quotations=total_quotations,
|
||||
active_users=active_users,
|
||||
recent_quotations=recent_quotations,
|
||||
country_stats=country_stats
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin dashboard error: {str(e)}')
|
||||
flash('An error occurred while loading the dashboard.', 'error')
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
@bp.route('/users')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
@cache.cached(timeout=60, key_prefix='admin_users')
|
||||
def users():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
|
||||
users = User.query.order_by(User.created_at.desc()).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
return render_template('admin/users.html', users=users)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin users error: {str(e)}')
|
||||
flash('An error occurred while loading users.', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
@bp.route('/quotations')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def quotations():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
|
||||
# Get filter parameters
|
||||
country = request.args.get('country')
|
||||
mold_type = request.args.get('mold_type')
|
||||
date_from_str = request.args.get('date_from')
|
||||
date_to_str = request.args.get('date_to')
|
||||
|
||||
# Convert date strings to datetime objects
|
||||
date_from = None
|
||||
date_to = None
|
||||
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
try:
|
||||
if date_from_str:
|
||||
date_from = datetime.strptime(date_from_str, '%Y-%m-%d')
|
||||
if date_to_str:
|
||||
date_to = datetime.strptime(date_to_str, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
flash('Invalid date format. Please use YYYY-MM-DD format.', 'error')
|
||||
date_from = None
|
||||
date_to = None
|
||||
|
||||
# Build query
|
||||
query = Quotation.query
|
||||
|
||||
if country:
|
||||
query = query.filter(Quotation.mold_shop_country == country)
|
||||
if mold_type:
|
||||
query = query.filter(Quotation.mold_main_type == mold_type)
|
||||
if date_from:
|
||||
query = query.filter(Quotation.created_at >= date_from)
|
||||
if date_to:
|
||||
# Add one day to include the end date fully
|
||||
next_day = date_to + timedelta(days=1)
|
||||
query = query.filter(Quotation.created_at < next_day)
|
||||
|
||||
# Get unique countries and mold types for filters
|
||||
countries = db.session.query(Quotation.mold_shop_country).distinct().all()
|
||||
countries = [c[0] for c in countries if c[0]]
|
||||
|
||||
mold_types = db.session.query(Quotation.mold_main_type).distinct().all()
|
||||
mold_types = [t[0] for t in mold_types if t[0]]
|
||||
|
||||
quotations = query.order_by(desc(Quotation.created_at)).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
return render_template('admin/quotations.html',
|
||||
quotations=quotations,
|
||||
countries=sorted(countries),
|
||||
mold_types=sorted(mold_types),
|
||||
today=today,
|
||||
filters={
|
||||
'country': country,
|
||||
'mold_type': mold_type,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin quotations error: {str(e)}')
|
||||
flash('An error occurred while loading quotations.', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
@bp.route('/user/<int:user_id>')
|
||||
@login_required
|
||||
def user_detail(user_id):
|
||||
try:
|
||||
user = User.query.get_or_404(user_id)
|
||||
quotations = Quotation.query.filter_by(user_id=user_id).order_by(
|
||||
desc(Quotation.created_at)
|
||||
).all()
|
||||
|
||||
return render_template('admin/user_detail.html',
|
||||
user=user,
|
||||
quotations=quotations
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin user detail error: {str(e)}')
|
||||
flash('An error occurred while loading user details.', 'error')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
@bp.route('/quotation/<int:id>')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
@cache.cached(timeout=60, key_prefix='admin_quotation_detail')
|
||||
def quotation_detail(id):
|
||||
try:
|
||||
quotation = Quotation.query.get_or_404(id)
|
||||
return render_template('admin/quotation_detail.html',
|
||||
quotation=quotation
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin quotation detail error: {str(e)}')
|
||||
flash('An error occurred while loading quotation details.', 'error')
|
||||
return redirect(url_for('admin.quotations'))
|
||||
|
||||
@bp.route('/user/<int:user_id>/toggle', methods=['POST'])
|
||||
@login_required
|
||||
def toggle_user(user_id):
|
||||
try:
|
||||
user = User.query.get_or_404(user_id)
|
||||
user.is_active = not user.is_active
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'is_active': user.is_active
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin toggle user error: {str(e)}')
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred while updating user status'
|
||||
}), 500
|
||||
|
||||
@bp.route('/delete_user', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("10 per minute")
|
||||
def delete_user():
|
||||
if not request.is_json:
|
||||
return jsonify({'error': 'Invalid request format'}), 400
|
||||
|
||||
data = request.get_json()
|
||||
user_id = data.get('user_id')
|
||||
|
||||
if not user_id:
|
||||
return jsonify({'error': 'User ID is required'}), 400
|
||||
|
||||
try:
|
||||
user = User.query.get_or_404(user_id)
|
||||
if user.id == current_user.id:
|
||||
return jsonify({'error': 'Cannot delete your own account'}), 400
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
cache.delete_memoized(users)
|
||||
current_app.logger.info(f'User {user.email} deleted by admin {current_user.email}')
|
||||
return jsonify({'message': 'User deleted successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Error deleting user: {str(e)}')
|
||||
return jsonify({'error': 'An error occurred while deleting the user'}), 500
|
||||
|
||||
@bp.route('/delete_quotation', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("10 per minute")
|
||||
def delete_quotation():
|
||||
if not request.is_json:
|
||||
return jsonify({'error': 'Invalid request format'}), 400
|
||||
|
||||
data = request.get_json()
|
||||
quotation_id = data.get('quotation_id')
|
||||
|
||||
if not quotation_id:
|
||||
return jsonify({'error': 'Quotation ID is required'}), 400
|
||||
|
||||
try:
|
||||
quotation = Quotation.query.get_or_404(quotation_id)
|
||||
db.session.delete(quotation)
|
||||
db.session.commit()
|
||||
cache.delete_memoized(quotations)
|
||||
current_app.logger.info(f'Quotation {quotation.id} deleted by admin {current_user.email}')
|
||||
return jsonify({'message': 'Quotation deleted successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Error deleting quotation: {str(e)}')
|
||||
return jsonify({'error': 'An error occurred while deleting the quotation'}), 500
|
||||
|
||||
@bp.route('/cost-factors')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def cost_factors():
|
||||
"""Display and manage cost factors"""
|
||||
try:
|
||||
from ..utils.cost_factor_loader import initialize_default_cost_factors
|
||||
from flask import current_app
|
||||
|
||||
# Add debug logging
|
||||
current_app.logger.info("Accessing cost_factors route")
|
||||
|
||||
# Verify database connection
|
||||
try:
|
||||
db.session.execute(text('SELECT 1'))
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Database connection error: {str(e)}")
|
||||
flash('Database connection error', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Initialize default factors if none exist
|
||||
try:
|
||||
initialize_default_cost_factors()
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error initializing default cost factors: {str(e)}")
|
||||
flash('Error initializing cost factors', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Get all cost factors grouped by category
|
||||
try:
|
||||
mold_factors = CostFactor.query.filter_by(category='mold', is_active=True).order_by(CostFactor.name).all()
|
||||
complexity_factors = CostFactor.query.filter_by(category='complexity', is_active=True).order_by(CostFactor.name).all()
|
||||
texture_factors = CostFactor.query.filter_by(category='texture', is_active=True).order_by(CostFactor.name).all()
|
||||
tax_factors = CostFactor.query.filter_by(category='tax', is_active=True).order_by(CostFactor.name).all()
|
||||
base_factors = CostFactor.query.filter_by(category='base', is_active=True).order_by(CostFactor.name).all()
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error querying cost factors: {str(e)}")
|
||||
flash('Error loading cost factors from database', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Log the number of factors found
|
||||
current_app.logger.info(f"Found factors - Mold: {len(mold_factors)}, Complexity: {len(complexity_factors)}, "
|
||||
f"Texture: {len(texture_factors)}, Tax: {len(tax_factors)}, Base: {len(base_factors)}")
|
||||
|
||||
# Log individual factors for debugging
|
||||
current_app.logger.debug("Base Factors: %s", [f"{f.name}: {f.factor_value}" for f in base_factors])
|
||||
current_app.logger.debug("Mold Factors: %s", [f"{f.name}: {f.factor_value}" for f in mold_factors])
|
||||
current_app.logger.debug("Complexity Factors: %s", [f"{f.name}: {f.factor_value}" for f in complexity_factors])
|
||||
current_app.logger.debug("Texture Factors: %s", [f"{f.name}: {f.factor_value}" for f in texture_factors])
|
||||
current_app.logger.debug("Tax Factors: %s", [f"{f.name}: {f.factor_value}" for f in tax_factors])
|
||||
|
||||
# Prepare template context
|
||||
template_context = {
|
||||
'mold_factors': mold_factors,
|
||||
'complexity_factors': complexity_factors,
|
||||
'texture_factors': texture_factors,
|
||||
'tax_factors': tax_factors,
|
||||
'base_factors': base_factors
|
||||
}
|
||||
|
||||
# Log template context
|
||||
current_app.logger.debug("Template context: %s", template_context)
|
||||
|
||||
try:
|
||||
response = make_response(render_template(
|
||||
'admin/cost_factors.html',
|
||||
**template_context
|
||||
))
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Template rendering error: {str(e)}")
|
||||
flash('Error rendering the page', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Add headers to prevent caching
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Unhandled error in cost_factors route: {str(e)}")
|
||||
flash('An unexpected error occurred while loading cost factors.', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
@bp.route('/cost-factors/edit/<int:factor_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def edit_cost_factor(factor_id):
|
||||
"""Edit a specific cost factor"""
|
||||
try:
|
||||
factor = CostFactor.query.get_or_404(factor_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
old_value = factor.factor_value
|
||||
new_value = float(request.form.get('factor_value'))
|
||||
change_reason = request.form.get('change_reason', '')
|
||||
|
||||
# Update the factor
|
||||
factor.factor_value = new_value
|
||||
factor.updated_by = current_user.id
|
||||
|
||||
# Create history record
|
||||
history = CostFactorHistory(
|
||||
cost_factor_id=factor.id,
|
||||
old_value=old_value,
|
||||
new_value=new_value,
|
||||
changed_by=current_user.id,
|
||||
change_reason=change_reason
|
||||
)
|
||||
|
||||
db.session.add(history)
|
||||
db.session.commit()
|
||||
|
||||
# Clear the cache
|
||||
cache.delete_memoized(cost_factors)
|
||||
cache.delete('admin_cost_factors')
|
||||
|
||||
flash(f'Cost factor "{factor.name}" updated successfully.', 'success')
|
||||
return redirect(url_for('admin.cost_factors'))
|
||||
|
||||
return render_template('admin/edit_cost_factor.html', factor=factor)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin edit cost factor error: {str(e)}')
|
||||
flash('An error occurred while editing the cost factor.', 'error')
|
||||
return redirect(url_for('admin.cost_factors'))
|
||||
|
||||
@bp.route('/cost-factors/history/<int:factor_id>')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def cost_factor_history(factor_id):
|
||||
"""View history of changes for a cost factor"""
|
||||
try:
|
||||
factor = CostFactor.query.get_or_404(factor_id)
|
||||
history = CostFactorHistory.query.filter_by(cost_factor_id=factor_id)\
|
||||
.order_by(CostFactorHistory.changed_at.desc()).all()
|
||||
|
||||
return render_template('admin/cost_factor_history.html',
|
||||
factor=factor,
|
||||
history=history
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin cost factor history error: {str(e)}')
|
||||
flash('An error occurred while loading cost factor history.', 'error')
|
||||
return redirect(url_for('admin.cost_factors'))
|
||||
|
||||
@bp.route('/cost-factors/reset', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("10 per minute")
|
||||
def reset_cost_factors():
|
||||
"""Reset cost factors to default values"""
|
||||
try:
|
||||
from ..utils.cost_factor_loader import get_default_cost_factors
|
||||
|
||||
# Get default values
|
||||
default_factors, default_tax_rates, default_base = get_default_cost_factors()
|
||||
|
||||
# Create a mapping of all default values
|
||||
default_values = {}
|
||||
|
||||
# Add mold factors
|
||||
for name, value in default_factors['mold'].items():
|
||||
default_values[name] = value
|
||||
|
||||
# Add complexity factors
|
||||
for name, value in default_factors['complexity'].items():
|
||||
default_values[name] = value
|
||||
|
||||
# Add texture factors
|
||||
for name, value in default_factors['texture'].items():
|
||||
default_values[name] = value
|
||||
|
||||
# Add tax factors
|
||||
for name, value in default_tax_rates.items():
|
||||
default_values[name] = value
|
||||
|
||||
# Add base factor
|
||||
default_values['DEFAULT_NET_BASE'] = default_base
|
||||
|
||||
# Update existing cost factors to default values
|
||||
for factor in CostFactor.query.all():
|
||||
if factor.name in default_values:
|
||||
old_value = factor.factor_value
|
||||
new_value = default_values[factor.name]
|
||||
|
||||
if old_value != new_value: # Only update if value changed
|
||||
factor.factor_value = new_value
|
||||
factor.updated_by = current_user.id
|
||||
|
||||
# Create history record for the reset
|
||||
history = CostFactorHistory(
|
||||
cost_factor_id=factor.id,
|
||||
old_value=old_value,
|
||||
new_value=new_value,
|
||||
changed_by=current_user.id,
|
||||
change_reason='Reset to default value'
|
||||
)
|
||||
db.session.add(history)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Clear the cache
|
||||
cache.delete_memoized(cost_factors)
|
||||
cache.delete('admin_cost_factors')
|
||||
|
||||
flash('Cost factors have been reset to default values.', 'success')
|
||||
return redirect(url_for('admin.cost_factors'))
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Admin reset cost factors error: {str(e)}')
|
||||
db.session.rollback()
|
||||
flash('An error occurred while resetting cost factors.', 'error')
|
||||
return redirect(url_for('admin.cost_factors'))
|
||||
|
||||
@bp.route('/debug-endpoints')
|
||||
def debug_endpoints():
|
||||
from flask import current_app
|
||||
with open('logs/endpoints.log', 'w') as f:
|
||||
for rule in current_app.url_map.iter_rules():
|
||||
f.write(f"{rule.endpoint} {rule.methods} {rule.rule}\n")
|
||||
return 'Endpoints written to logs/endpoints.log', 200
|
||||
@@ -0,0 +1,171 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from app.forms import LoginForm, RegistrationForm, RequestPasswordResetForm, ResetPasswordForm
|
||||
from app import create_app, db
|
||||
from app import limiter
|
||||
from datetime import datetime, timezone
|
||||
from app.config import ALLOWED_EMAILS
|
||||
import logging
|
||||
from flask_mail import Message
|
||||
from app import mail
|
||||
from app.models import User
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
@limiter.limit("5 per minute")
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.calculator'))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
email = form.email.data.strip().lower()
|
||||
password = form.password.data
|
||||
|
||||
# Log the login attempt
|
||||
current_app.logger.info(f'Login attempt for email: {email}')
|
||||
|
||||
user = User.authenticate(email, password)
|
||||
if user:
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
user.last_login = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
current_app.logger.info(f'User {user.email} logged in successfully')
|
||||
next_page = request.args.get('next') or url_for('main.calculator')
|
||||
return redirect(next_page)
|
||||
else:
|
||||
current_app.logger.warning(f'Failed login attempt for email: {email}')
|
||||
flash('Invalid email or password', 'danger')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Login error: {str(e)}', exc_info=True)
|
||||
flash('An error occurred during login. Please try again.', 'danger')
|
||||
|
||||
return render_template('auth/login.html', form=form)
|
||||
|
||||
@bp.route('/register', methods=['GET', 'POST'])
|
||||
@limiter.limit(lambda: None if current_app.config.get('DEBUG', False) else "10 per hour")
|
||||
def register():
|
||||
try:
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
form = RegistrationForm()
|
||||
logger.info('Registration form initialized')
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
email = form.email.data.strip().lower()
|
||||
username = form.username.data.strip().lower()
|
||||
company = form.company.data.strip().upper()
|
||||
logger.info(f'Processing registration for email: {email}, username: {username}, company: {company}')
|
||||
|
||||
# Check if email is in allowed list or has adidas.com domain
|
||||
if email not in ALLOWED_EMAILS and not email.endswith('@adidas.com'):
|
||||
logger.warning(f'Registration attempt with non-allowed email: {email}')
|
||||
flash('Registration is not allowed for this email domain', 'danger')
|
||||
return render_template('auth/register.html', form=form)
|
||||
|
||||
try:
|
||||
logger.info('Creating new user...')
|
||||
user = User.create(
|
||||
username=username,
|
||||
email=email,
|
||||
company=company,
|
||||
password=form.password.data
|
||||
)
|
||||
logger.info('User created, committing to database...')
|
||||
db.session.commit()
|
||||
logger.info(f'New user registered successfully: {user.email}')
|
||||
flash('Registration successful! Please login', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
except ValueError as e:
|
||||
logger.warning(f'Registration validation error: {str(e)}')
|
||||
flash(str(e), 'danger')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f'Registration error: {str(e)}', exc_info=True)
|
||||
flash('An error occurred during registration', 'danger')
|
||||
except Exception as e:
|
||||
logger.error(f'Unexpected error during registration: {str(e)}', exc_info=True)
|
||||
flash('An unexpected error occurred', 'danger')
|
||||
elif form.errors:
|
||||
logger.warning(f'Form validation errors: {form.errors}')
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
flash(f'{field}: {error}', 'danger')
|
||||
logger.warning(f'Field {field} validation error: {error}')
|
||||
|
||||
logger.info('Rendering registration template')
|
||||
return render_template('auth/register.html', form=form)
|
||||
except Exception as e:
|
||||
logger.error(f'Critical error in registration route: {str(e)}', exc_info=True)
|
||||
flash('A critical error occurred', 'danger')
|
||||
return render_template('auth/register.html', form=form)
|
||||
|
||||
@bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
current_app.logger.info(f'User {current_user.email} logged out')
|
||||
logout_user()
|
||||
flash('You have been logged out.', 'info')
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
@bp.route('/request-reset', methods=['GET', 'POST'])
|
||||
@limiter.limit("3 per hour")
|
||||
def request_reset():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.calculator'))
|
||||
|
||||
form = RequestPasswordResetForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
email = form.email.data.strip().lower()
|
||||
user = User.query.filter_by(email=email).first()
|
||||
|
||||
if user:
|
||||
token = user.generate_reset_token()
|
||||
db.session.commit()
|
||||
|
||||
reset_url = url_for('auth.reset_password', token=token, _external=True)
|
||||
flash(f'Your password reset link is: {reset_url}', 'info')
|
||||
flash('This link will expire in 15 minutes.', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Always show success message to prevent email enumeration
|
||||
flash('If an account exists with that email, you will receive password reset instructions.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Error generating reset token: {str(e)}', exc_info=True)
|
||||
flash('An error occurred while processing your request.', 'danger')
|
||||
|
||||
return render_template('auth/request_reset.html', form=form)
|
||||
|
||||
@bp.route('/reset-password/<token>', methods=['GET', 'POST'])
|
||||
@limiter.limit("3 per hour")
|
||||
def reset_password(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.calculator'))
|
||||
|
||||
form = ResetPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
user = User.query.filter_by(reset_token=token).first()
|
||||
if not user or not user.verify_reset_token(token):
|
||||
flash('The password reset link is invalid or has expired.', 'danger')
|
||||
return redirect(url_for('auth.request_reset'))
|
||||
|
||||
user.set_password(form.password.data)
|
||||
db.session.commit()
|
||||
current_app.logger.info(f'Password reset successful for user {user.email}')
|
||||
flash('Your password has been reset. You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Error resetting password: {str(e)}', exc_info=True)
|
||||
flash('An error occurred while resetting your password.', 'danger')
|
||||
|
||||
return render_template('auth/reset_password.html', form=form)
|
||||
@@ -0,0 +1,383 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, current_app, flash
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
from app.models import Quotation, User
|
||||
from app import db
|
||||
from app.forms import LoginForm, QuotationForm
|
||||
from app.utils.cost_factor_loader import get_cost_factors
|
||||
from app.config import ALLOWED_EMAILS
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from app import limiter, cache
|
||||
import json
|
||||
from ..utils.calculator import calculate_base_price
|
||||
from ..health import get_health_status
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('/', methods=['GET', 'POST'])
|
||||
def home():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.calculator'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@bp.route('/calculator')
|
||||
@login_required
|
||||
def calculator():
|
||||
try:
|
||||
form = QuotationForm()
|
||||
|
||||
# Set today's date as default for issue_date
|
||||
if not form.issue_date.data:
|
||||
form.issue_date.data = datetime.now(timezone.utc).date()
|
||||
|
||||
# Get cost factors for the form
|
||||
cost_factors, tax_rates, default_net_base = get_cost_factors()
|
||||
|
||||
# Initialize form with default values
|
||||
if form.mold_main_type.data in form.MOLD_TYPE_MAPPING:
|
||||
form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[form.mold_main_type.data]]
|
||||
|
||||
# Get quote history with error handling
|
||||
try:
|
||||
quote_history = Quotation.query.filter_by(user_id=current_user.id)\
|
||||
.order_by(Quotation.created_at.desc())\
|
||||
.limit(5)\
|
||||
.all()
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error fetching quote history: {str(e)}")
|
||||
quote_history = []
|
||||
|
||||
return render_template('calculator.html',
|
||||
form=form,
|
||||
quote_history=quote_history,
|
||||
tax_rates=tax_rates,
|
||||
quotation=None)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Calculator error: {str(e)}", exc_info=True)
|
||||
flash('Error loading calculator data', 'danger')
|
||||
# Create a new form with default values
|
||||
form = QuotationForm()
|
||||
if form.mold_main_type.data in form.MOLD_TYPE_MAPPING:
|
||||
form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[form.mold_main_type.data]]
|
||||
# Set today's date as default even in error case
|
||||
form.issue_date.data = datetime.now(timezone.utc).date()
|
||||
return render_template('calculator.html',
|
||||
form=form,
|
||||
quote_history=[],
|
||||
tax_rates={},
|
||||
quotation=None)
|
||||
|
||||
@bp.route('/calculate', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
@cache.cached(timeout=300, key_prefix='calculate_form')
|
||||
def calculate():
|
||||
"""Calculate mold cost based on form data"""
|
||||
try:
|
||||
form = QuotationForm()
|
||||
|
||||
# Log incoming request data
|
||||
current_app.logger.info('Incoming request data:')
|
||||
current_app.logger.info(f'Request method: {request.method}')
|
||||
current_app.logger.info(f'Request form data: {dict(request.form)}')
|
||||
current_app.logger.info(f'Request JSON data: {request.get_json(silent=True)}')
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# Log form data before validation
|
||||
current_app.logger.info('Form data before validation:')
|
||||
current_app.logger.info(f'Form data: {form.data}')
|
||||
current_app.logger.info(f'Form errors: {form.errors}')
|
||||
|
||||
# Log the selected mold type
|
||||
current_app.logger.info(f'Selected mold_main_type: {form.mold_main_type.data}')
|
||||
current_app.logger.info(f'Selected mold_sub_type: {form.mold_sub_type.data}')
|
||||
current_app.logger.info(f'Selected mold_specific_type: {form.mold_specific_type.data}')
|
||||
|
||||
# Update mold_sub_type choices based on selected mold_main_type
|
||||
main_type = request.form.get('mold_main_type')
|
||||
current_app.logger.info(f'Updating sub_type choices for main_type: {main_type}')
|
||||
if main_type in form.MOLD_TYPE_MAPPING:
|
||||
form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[main_type]]
|
||||
current_app.logger.info(f'Updated sub_type choices: {form.mold_sub_type.choices}')
|
||||
|
||||
# Update mold_specific_type choices based on selected mold_sub_type
|
||||
sub_type = request.form.get('mold_sub_type')
|
||||
if sub_type in form.MOLD_SPECIFIC_TYPE_OPTIONS:
|
||||
form.mold_specific_type.choices = [('', 'Select Specific Type')] + [(x, x) for x in form.MOLD_SPECIFIC_TYPE_OPTIONS[sub_type]]
|
||||
else:
|
||||
form.mold_specific_type.choices = [('', 'Select Specific Type')]
|
||||
|
||||
# Check if specific type is needed
|
||||
if sub_type and sub_type in form.MOLD_SPECIFIC_TYPE_OPTIONS:
|
||||
# If specific type is needed but not provided, set it to empty string
|
||||
if not request.form.get('mold_specific_type'):
|
||||
form.mold_specific_type.data = ''
|
||||
else:
|
||||
# If specific type is not needed, set it to empty string
|
||||
form.mold_specific_type.data = ''
|
||||
|
||||
if not form.validate_on_submit():
|
||||
current_app.logger.warning('Form validation failed:')
|
||||
current_app.logger.warning(f'Form errors: {form.errors}')
|
||||
current_app.logger.warning(f'Form data after validation: {form.data}')
|
||||
current_app.logger.warning(f'Request form data: {dict(request.form)}')
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid form data',
|
||||
'message': 'Please check the form for errors',
|
||||
'errors': form.errors,
|
||||
'form_data': dict(request.form)
|
||||
}), 400
|
||||
|
||||
# Get form data
|
||||
form_data = form.data
|
||||
current_app.logger.info(f'Processing form data: {form_data}')
|
||||
|
||||
# Calculate costs
|
||||
try:
|
||||
result = calculate_base_price(
|
||||
mold_main_type=form.mold_main_type.data,
|
||||
mold_sub_type=form.mold_sub_type.data,
|
||||
mold_specific_type=form.mold_specific_type.data,
|
||||
sliders_count=int(form.sliders_count.data),
|
||||
cavity_count=float(form.cavity_count.data),
|
||||
digital_texture_type=form.digital_texture_type.data,
|
||||
complexity_high_sidewall=(form.complexity_high_sidewall.data == 'yes')
|
||||
)
|
||||
current_app.logger.info(f'Calculation result: {result}')
|
||||
|
||||
# Get cost factors for tax rate
|
||||
cost_factors, tax_rates, default_net_base = get_cost_factors()
|
||||
|
||||
# Get tax rate for the country
|
||||
tax_rate = tax_rates.get(form.mold_shop_country.data, 0.0)
|
||||
|
||||
# Calculate total cost (base price + tax)
|
||||
total_cost = result['total_cost'] * (1 + tax_rate)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'base_price': float(result['base_price']),
|
||||
'complexity_cost': float(result['complexity_cost']),
|
||||
'texture_cost': float(result['texture_cost']),
|
||||
'tax_rate': float(tax_rate),
|
||||
'total_cost': float(total_cost),
|
||||
'mold_shop_name': form.mold_shop_name.data,
|
||||
'mold_shop_country': form.mold_shop_country.data,
|
||||
't1_shoe_factory_name': form.t1_shoe_factory_name.data,
|
||||
't1_shoe_factory_country': form.t1_shoe_factory_country.data,
|
||||
't2_component_factory_name': form.t2_component_factory_name.data,
|
||||
't2_component_factory_country': form.t2_component_factory_country.data,
|
||||
'model_name': form.model_name.data,
|
||||
'tooling_id': form.tooling_id.data,
|
||||
'season': form.season.data,
|
||||
'stage': form.stage.data,
|
||||
'type': form.type.data,
|
||||
'issue_date': form.issue_date.data.strftime('%Y-%m-%d'),
|
||||
'approved_by_mold_shop': form.approved_by_mold_shop.data,
|
||||
'approved_by_t1_tooling': form.approved_by_t1_tooling.data,
|
||||
'approved_by_lo_tooling': form.approved_by_lo_tooling.data,
|
||||
'mold_main_type': form.mold_main_type.data,
|
||||
'mold_sub_type': form.mold_sub_type.data,
|
||||
'mold_specific_type': form.mold_specific_type.data,
|
||||
'complexity_high_sidewall': 'yes' if form.complexity_high_sidewall.data == 'yes' else 'no',
|
||||
'sliders_count': form.sliders_count.data,
|
||||
'cavity_count': form.cavity_count.data,
|
||||
'digital_texture_type': form.digital_texture_type.data
|
||||
})
|
||||
except Exception as calc_error:
|
||||
current_app.logger.error(f'Error calculating costs: {str(calc_error)}', exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Calculation error',
|
||||
'message': str(calc_error)
|
||||
}), 500
|
||||
|
||||
except Exception as form_error:
|
||||
current_app.logger.error(f'Error processing form: {str(form_error)}', exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Form processing error',
|
||||
'message': str(form_error)
|
||||
}), 500
|
||||
|
||||
# GET request - render the form
|
||||
return render_template('calculator.html', form=form)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Unexpected error in calculate route: {str(e)}', exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Unexpected error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@bp.route('/quotations')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
@cache.cached(timeout=60, key_prefix='user_quotations')
|
||||
def quotations():
|
||||
quotations = Quotation.query.filter_by(user_id=current_user.id)\
|
||||
.order_by(Quotation.created_at.desc()).all()
|
||||
return render_template('quotations.html', quotations=quotations)
|
||||
|
||||
@bp.route('/quotation/<int:id>')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
@cache.cached(timeout=60, key_prefix='quotation_detail')
|
||||
def quotation_detail(id):
|
||||
quotation = Quotation.query.get_or_404(id)
|
||||
if quotation.user_id != current_user.id and not current_user.is_admin:
|
||||
flash('Access denied.', 'danger')
|
||||
return redirect(url_for('main.quotations'))
|
||||
return render_template('quotation_detail.html', quotation=quotation)
|
||||
|
||||
@bp.route('/health')
|
||||
def health_check():
|
||||
"""Health check endpoint that returns the status of various system components."""
|
||||
# Get health status
|
||||
health_status = get_health_status()
|
||||
|
||||
# Set appropriate status code based on overall health
|
||||
status_code = 200 if health_status['status'] == 'healthy' else 503
|
||||
|
||||
# Add rate limiting headers
|
||||
response = jsonify(health_status)
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
|
||||
return response, status_code
|
||||
|
||||
@bp.route('/api/recent-quotes')
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def recent_quotes():
|
||||
try:
|
||||
recent_quotes = Quotation.query.filter_by(user_id=current_user.id)\
|
||||
.order_by(Quotation.created_at.desc())\
|
||||
.limit(5)\
|
||||
.all()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'quotes': [{
|
||||
'id': quote.id,
|
||||
'model_name': quote.model_name,
|
||||
'tooling_id': quote.tooling_id,
|
||||
'mold_sub_type': quote.mold_sub_type,
|
||||
'cavity_count': float(quote.cavity_count),
|
||||
'sliders_count': quote.sliders_count,
|
||||
'created_at': quote.created_at.strftime('%Y-%m-%d %H:%M'),
|
||||
'mold_shop_country': quote.mold_shop_country,
|
||||
'digital_texture_type': quote.digital_texture_type,
|
||||
'season': quote.season,
|
||||
'stage': quote.stage,
|
||||
'base_price': float(quote.base_price),
|
||||
'tax_rate': float(quote.tax_rate) if quote.tax_rate is not None else 0.0,
|
||||
'total_cost': float(quote.total_cost)
|
||||
} for quote in recent_quotes]
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Error fetching recent quotes: {str(e)}', exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'An error occurred while fetching recent quotes',
|
||||
'quotes': []
|
||||
}), 500
|
||||
|
||||
@bp.route('/save-quotation', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def save_quotation():
|
||||
"""Save a quotation to the database"""
|
||||
try:
|
||||
# Get form data
|
||||
form_data = request.get_json()
|
||||
|
||||
if not form_data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'No data provided'
|
||||
}), 400
|
||||
|
||||
# Check for duplicate submission (same user, same data within last 30 seconds)
|
||||
try:
|
||||
recent_duplicate = Quotation.query.filter(
|
||||
Quotation.user_id == current_user.id,
|
||||
Quotation.model_name == form_data.get('model_name'),
|
||||
Quotation.tooling_id == form_data.get('tooling_id'),
|
||||
Quotation.mold_main_type == form_data.get('mold_main_type'),
|
||||
Quotation.mold_sub_type == form_data.get('mold_sub_type'),
|
||||
Quotation.created_at >= datetime.now(timezone.utc) - timedelta(seconds=30)
|
||||
).first()
|
||||
|
||||
if recent_duplicate:
|
||||
current_app.logger.warning(f'Duplicate submission detected for user {current_user.id}, tooling_id {form_data.get("tooling_id")}')
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Duplicate submission',
|
||||
'message': 'This quotation was already submitted recently. Please wait a moment before trying again.',
|
||||
'quotation_id': recent_duplicate.id
|
||||
}), 409
|
||||
except Exception as duplicate_check_error:
|
||||
current_app.logger.error(f'Error checking for duplicates: {str(duplicate_check_error)}')
|
||||
# Continue with submission even if duplicate check fails
|
||||
|
||||
# Ensure mold_specific_type is not empty or missing
|
||||
mold_specific_type = form_data.get('mold_specific_type')
|
||||
if not mold_specific_type:
|
||||
mold_specific_type = 'N/A'
|
||||
|
||||
# Create new quotation
|
||||
quotation = Quotation(
|
||||
user_id=current_user.id,
|
||||
mold_shop_name=form_data.get('mold_shop_name'),
|
||||
mold_shop_country=form_data.get('mold_shop_country'),
|
||||
t1_shoe_factory_name=form_data.get('t1_shoe_factory_name'),
|
||||
t1_shoe_factory_country=form_data.get('t1_shoe_factory_country'),
|
||||
t2_component_factory_name=form_data.get('t2_component_factory_name'),
|
||||
t2_component_factory_country=form_data.get('t2_component_factory_country'),
|
||||
model_name=form_data.get('model_name'),
|
||||
tooling_id=form_data.get('tooling_id'),
|
||||
season=form_data.get('season'),
|
||||
stage=form_data.get('stage'),
|
||||
type=form_data.get('type'),
|
||||
issue_date=datetime.strptime(form_data.get('issue_date'), '%Y-%m-%d').date(),
|
||||
approved_by_mold_shop=form_data.get('approved_by_mold_shop'),
|
||||
approved_by_t1_tooling=form_data.get('approved_by_t1_tooling'),
|
||||
approved_by_lo_tooling=form_data.get('approved_by_lo_tooling'),
|
||||
mold_main_type=form_data.get('mold_main_type'),
|
||||
mold_sub_type=form_data.get('mold_sub_type'),
|
||||
mold_specific_type=mold_specific_type,
|
||||
complexity_high_sidewall=(form_data.get('complexity_high_sidewall') == 'yes'),
|
||||
sliders_count=int(form_data.get('sliders_count')),
|
||||
cavity_count=float(form_data.get('cavity_count')),
|
||||
digital_texture_type=form_data.get('digital_texture_type'),
|
||||
base_price=float(form_data.get('base_price')),
|
||||
tax_rate=float(form_data.get('tax_rate')),
|
||||
total_cost=float(form_data.get('total_cost'))
|
||||
)
|
||||
|
||||
# Save to database
|
||||
db.session.add(quotation)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f'Quotation saved with ID: {quotation.id}')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'quotation_id': quotation.id,
|
||||
'message': 'Quotation saved successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f'Error saving quotation: {str(e)}', exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Failed to save quotation',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
Reference in New Issue
Block a user