242 lines
8 KiB
Python
242 lines
8 KiB
Python
|
|
from flask import Blueprint, request, jsonify, current_app
|
||
|
|
from flask_login import login_required, current_user
|
||
|
|
from werkzeug.utils import secure_filename
|
||
|
|
from app import db, bcrypt
|
||
|
|
from app.models import User
|
||
|
|
import os
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
bp = Blueprint('settings', __name__, url_prefix='/api/settings')
|
||
|
|
|
||
|
|
# Allowed avatar image types
|
||
|
|
ALLOWED_AVATAR_TYPES = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||
|
|
MAX_AVATAR_SIZE = 20 * 1024 * 1024 # 20MB
|
||
|
|
|
||
|
|
def allowed_avatar(filename):
|
||
|
|
"""Check if file extension is allowed for avatars"""
|
||
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AVATAR_TYPES
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/profile', methods=['GET'])
|
||
|
|
@login_required
|
||
|
|
def get_profile():
|
||
|
|
"""
|
||
|
|
Get current user profile information
|
||
|
|
Security: Returns only current user's data
|
||
|
|
"""
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'profile': {
|
||
|
|
'username': current_user.username,
|
||
|
|
'email': current_user.email,
|
||
|
|
'language': current_user.language,
|
||
|
|
'currency': current_user.currency,
|
||
|
|
'avatar': current_user.avatar,
|
||
|
|
'is_admin': current_user.is_admin,
|
||
|
|
'two_factor_enabled': current_user.two_factor_enabled,
|
||
|
|
'created_at': current_user.created_at.isoformat()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/profile', methods=['PUT'])
|
||
|
|
@login_required
|
||
|
|
def update_profile():
|
||
|
|
"""
|
||
|
|
Update user profile information
|
||
|
|
Security: Updates only current user's profile
|
||
|
|
"""
|
||
|
|
data = request.get_json()
|
||
|
|
|
||
|
|
if not data:
|
||
|
|
return jsonify({'success': False, 'error': 'No data provided'}), 400
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Update language
|
||
|
|
if 'language' in data:
|
||
|
|
if data['language'] in ['en', 'ro']:
|
||
|
|
current_user.language = data['language']
|
||
|
|
else:
|
||
|
|
return jsonify({'success': False, 'error': 'Invalid language'}), 400
|
||
|
|
|
||
|
|
# Update currency
|
||
|
|
if 'currency' in data:
|
||
|
|
current_user.currency = data['currency']
|
||
|
|
|
||
|
|
# Update username (check uniqueness)
|
||
|
|
if 'username' in data and data['username'] != current_user.username:
|
||
|
|
existing = User.query.filter_by(username=data['username']).first()
|
||
|
|
if existing:
|
||
|
|
return jsonify({'success': False, 'error': 'Username already taken'}), 400
|
||
|
|
current_user.username = data['username']
|
||
|
|
|
||
|
|
# Update email (check uniqueness)
|
||
|
|
if 'email' in data and data['email'] != current_user.email:
|
||
|
|
existing = User.query.filter_by(email=data['email']).first()
|
||
|
|
if existing:
|
||
|
|
return jsonify({'success': False, 'error': 'Email already taken'}), 400
|
||
|
|
current_user.email = data['email']
|
||
|
|
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'message': 'Profile updated successfully',
|
||
|
|
'profile': {
|
||
|
|
'username': current_user.username,
|
||
|
|
'email': current_user.email,
|
||
|
|
'language': current_user.language,
|
||
|
|
'currency': current_user.currency,
|
||
|
|
'avatar': current_user.avatar
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
db.session.rollback()
|
||
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/avatar', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def upload_avatar():
|
||
|
|
"""
|
||
|
|
Upload custom avatar image
|
||
|
|
Security: Associates avatar with current_user.id, validates file type and size
|
||
|
|
"""
|
||
|
|
if 'avatar' not in request.files:
|
||
|
|
return jsonify({'success': False, 'error': 'No file provided'}), 400
|
||
|
|
|
||
|
|
file = request.files['avatar']
|
||
|
|
|
||
|
|
if not file or not file.filename:
|
||
|
|
return jsonify({'success': False, 'error': 'No file selected'}), 400
|
||
|
|
|
||
|
|
if not allowed_avatar(file.filename):
|
||
|
|
return jsonify({
|
||
|
|
'success': False,
|
||
|
|
'error': 'Invalid file type. Allowed: PNG, JPG, JPEG, GIF, WEBP'
|
||
|
|
}), 400
|
||
|
|
|
||
|
|
# Check file size
|
||
|
|
file.seek(0, os.SEEK_END)
|
||
|
|
file_size = file.tell()
|
||
|
|
file.seek(0)
|
||
|
|
|
||
|
|
if file_size > MAX_AVATAR_SIZE:
|
||
|
|
return jsonify({
|
||
|
|
'success': False,
|
||
|
|
'error': f'File too large. Maximum size: {MAX_AVATAR_SIZE // (1024*1024)}MB'
|
||
|
|
}), 400
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Delete old custom avatar if exists (not default avatars)
|
||
|
|
if current_user.avatar and not current_user.avatar.startswith('icons/avatars/'):
|
||
|
|
old_path = os.path.join(current_app.root_path, 'static', current_user.avatar)
|
||
|
|
if os.path.exists(old_path):
|
||
|
|
os.remove(old_path)
|
||
|
|
|
||
|
|
# Generate secure filename
|
||
|
|
file_ext = file.filename.rsplit('.', 1)[1].lower()
|
||
|
|
timestamp = int(datetime.utcnow().timestamp())
|
||
|
|
filename = f"user_{current_user.id}_{timestamp}.{file_ext}"
|
||
|
|
|
||
|
|
# Create avatars directory in uploads
|
||
|
|
avatars_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'avatars')
|
||
|
|
os.makedirs(avatars_dir, exist_ok=True)
|
||
|
|
|
||
|
|
# Save file
|
||
|
|
file_path = os.path.join(avatars_dir, filename)
|
||
|
|
file.save(file_path)
|
||
|
|
|
||
|
|
# Update user avatar (store relative path from static folder)
|
||
|
|
current_user.avatar = f"uploads/avatars/{filename}"
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'message': 'Avatar uploaded successfully',
|
||
|
|
'avatar': current_user.avatar
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
db.session.rollback()
|
||
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/avatar/default', methods=['PUT'])
|
||
|
|
@login_required
|
||
|
|
def set_default_avatar():
|
||
|
|
"""
|
||
|
|
Set avatar to one of the default avatars
|
||
|
|
Security: Updates only current user's avatar
|
||
|
|
"""
|
||
|
|
data = request.get_json()
|
||
|
|
|
||
|
|
if not data or 'avatar' not in data:
|
||
|
|
return jsonify({'success': False, 'error': 'Avatar path required'}), 400
|
||
|
|
|
||
|
|
avatar_path = data['avatar']
|
||
|
|
|
||
|
|
# Validate it's a default avatar
|
||
|
|
if not avatar_path.startswith('icons/avatars/avatar-'):
|
||
|
|
return jsonify({'success': False, 'error': 'Invalid avatar selection'}), 400
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Delete old custom avatar if exists (not default avatars)
|
||
|
|
if current_user.avatar and not current_user.avatar.startswith('icons/avatars/'):
|
||
|
|
old_path = os.path.join(current_app.root_path, 'static', current_user.avatar)
|
||
|
|
if os.path.exists(old_path):
|
||
|
|
os.remove(old_path)
|
||
|
|
|
||
|
|
current_user.avatar = avatar_path
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'message': 'Avatar updated successfully',
|
||
|
|
'avatar': current_user.avatar
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
db.session.rollback()
|
||
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/password', methods=['PUT'])
|
||
|
|
@login_required
|
||
|
|
def change_password():
|
||
|
|
"""
|
||
|
|
Change user password
|
||
|
|
Security: Requires current password verification
|
||
|
|
"""
|
||
|
|
data = request.get_json()
|
||
|
|
|
||
|
|
if not data:
|
||
|
|
return jsonify({'success': False, 'error': 'No data provided'}), 400
|
||
|
|
|
||
|
|
current_password = data.get('current_password')
|
||
|
|
new_password = data.get('new_password')
|
||
|
|
|
||
|
|
if not current_password or not new_password:
|
||
|
|
return jsonify({'success': False, 'error': 'Current and new password required'}), 400
|
||
|
|
|
||
|
|
# Verify current password
|
||
|
|
if not bcrypt.check_password_hash(current_user.password_hash, current_password):
|
||
|
|
return jsonify({'success': False, 'error': 'Current password is incorrect'}), 400
|
||
|
|
|
||
|
|
if len(new_password) < 6:
|
||
|
|
return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400
|
||
|
|
|
||
|
|
try:
|
||
|
|
current_user.password_hash = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'message': 'Password changed successfully'
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
db.session.rollback()
|
||
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|