from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, jsonify from flask_login import login_required, current_user from app import db from app.models.user import User, Tag from app.models.category import Category, Expense from werkzeug.security import generate_password_hash import csv import io from datetime import datetime import json bp = Blueprint('settings', __name__, url_prefix='/settings') def admin_required(f): from functools import wraps @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_admin: flash('Admin access required', 'error') return redirect(url_for('main.dashboard')) return f(*args, **kwargs) return decorated_function @bp.route('/') @login_required def index(): users = User.query.all() if current_user.is_admin else [] tags = Tag.query.filter_by(user_id=current_user.id).all() return render_template('settings/index.html', users=users, tags=tags) # USER MANAGEMENT @bp.route('/profile', methods=['GET', 'POST']) @login_required def edit_profile(): if request.method == 'POST': current_user.username = request.form.get('username') current_user.email = request.form.get('email') current_user.currency = request.form.get('currency', 'USD') current_user.language = request.form.get('language', 'en') # Budget alert preferences current_user.budget_alerts_enabled = request.form.get('budget_alerts_enabled') == 'on' alert_email = request.form.get('alert_email', '').strip() current_user.alert_email = alert_email if alert_email else None new_password = request.form.get('new_password') if new_password: current_user.set_password(new_password) db.session.commit() flash('Profile updated successfully!', 'success') return redirect(url_for('settings.index')) from app.translations import get_available_languages languages = get_available_languages() return render_template('settings/edit_profile.html', languages=languages) @bp.route('/users/create', methods=['GET', 'POST']) @login_required @admin_required def create_user(): if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') is_admin = request.form.get('is_admin') == 'on' if User.query.filter_by(username=username).first(): flash('Username already exists', 'error') return redirect(url_for('settings.create_user')) if User.query.filter_by(email=email).first(): flash('Email already exists', 'error') return redirect(url_for('settings.create_user')) user = User(username=username, email=email, is_admin=is_admin) user.set_password(password) db.session.add(user) db.session.commit() flash(f'User {username} created successfully!', 'success') return redirect(url_for('settings.index')) return render_template('settings/create_user.html') @bp.route('/users//edit', methods=['GET', 'POST']) @login_required @admin_required def edit_user(user_id): user = User.query.get_or_404(user_id) if request.method == 'POST': user.username = request.form.get('username') user.email = request.form.get('email') user.is_admin = request.form.get('is_admin') == 'on' new_password = request.form.get('new_password') if new_password: user.set_password(new_password) db.session.commit() flash(f'User {user.username} updated!', 'success') return redirect(url_for('settings.index')) return render_template('settings/edit_user.html', user=user) @bp.route('/users//delete', methods=['POST']) @login_required @admin_required def delete_user(user_id): if user_id == current_user.id: flash('Cannot delete your own account', 'error') return redirect(url_for('settings.index')) user = User.query.get_or_404(user_id) db.session.delete(user) db.session.commit() flash(f'User {user.username} deleted', 'success') return redirect(url_for('settings.index')) # TAG MANAGEMENT @bp.route('/tags/create', methods=['GET', 'POST']) @login_required def create_tag(): if request.method == 'POST': name = request.form.get('name') color = request.form.get('color', '#6366f1') tag = Tag(name=name, color=color, user_id=current_user.id) db.session.add(tag) db.session.commit() flash(f'Tag "{name}" created!', 'success') return redirect(url_for('settings.index')) return render_template('settings/create_tag.html') @bp.route('/tags//delete', methods=['POST']) @login_required def delete_tag(tag_id): tag = Tag.query.filter_by(id=tag_id, user_id=current_user.id).first_or_404() db.session.delete(tag) db.session.commit() flash(f'Tag "{tag.name}" deleted', 'success') return redirect(url_for('settings.index')) # IMPORT/EXPORT @bp.route('/export') @login_required def export_data(): output = io.StringIO() writer = csv.writer(output) writer.writerow(['Category', 'Description', 'Amount', 'Date', 'Paid By', 'Tags']) expenses = Expense.query.filter_by(user_id=current_user.id).all() for expense in expenses: writer.writerow([ expense.category.name, expense.description, expense.amount, expense.date.strftime('%Y-%m-%d'), expense.paid_by or '', expense.tags or '' ]) output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8')), mimetype='text/csv', as_attachment=True, download_name=f'expenses_{datetime.now().strftime("%Y%m%d")}.csv' ) @bp.route('/import', methods=['GET', 'POST']) @login_required def import_data(): if request.method == 'POST': if 'file' not in request.files: flash('No file uploaded', 'error') return redirect(url_for('settings.import_data')) file = request.files['file'] if file.filename == '': flash('No file selected', 'error') return redirect(url_for('settings.import_data')) if not file.filename.endswith('.csv'): flash('Only CSV files are supported', 'error') return redirect(url_for('settings.import_data')) try: stream = io.StringIO(file.stream.read().decode('UTF8'), newline=None) csv_reader = csv.DictReader(stream) imported = 0 for row in csv_reader: category_name = row.get('Category') category = Category.query.filter_by(name=category_name, user_id=current_user.id).first() if not category: category = Category(name=category_name, user_id=current_user.id) db.session.add(category) db.session.flush() expense = Expense( description=row.get('Description'), amount=float(row.get('Amount', 0)), date=datetime.strptime(row.get('Date'), '%Y-%m-%d'), paid_by=row.get('Paid By'), tags=row.get('Tags'), category_id=category.id, user_id=current_user.id ) db.session.add(expense) imported += 1 db.session.commit() flash(f'Successfully imported {imported} expenses!', 'success') return redirect(url_for('main.dashboard')) except Exception as e: db.session.rollback() flash(f'Import failed: {str(e)}', 'error') return redirect(url_for('settings.import_data')) return render_template('settings/import.html') # 2FA Management @bp.route('/2fa/setup', methods=['GET', 'POST']) @login_required def setup_2fa(): if request.method == 'POST': token = request.form.get('token') if not current_user.totp_secret: flash('2FA setup not initiated', 'error') return redirect(url_for('settings.setup_2fa')) if current_user.verify_totp(token): current_user.is_2fa_enabled = True db.session.commit() flash('2FA enabled successfully!', 'success') return redirect(url_for('settings.index')) else: flash('Invalid code. Please try again.', 'error') # Generate QR code if not current_user.totp_secret: current_user.generate_totp_secret() db.session.commit() import qrcode import io import base64 uri = current_user.get_totp_uri() qr = qrcode.QRCode(version=1, box_size=10, border=5) qr.add_data(uri) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffer = io.BytesIO() img.save(buffer, format='PNG') buffer.seek(0) qr_base64 = base64.b64encode(buffer.getvalue()).decode() return render_template('settings/setup_2fa.html', qr_code=qr_base64, secret=current_user.totp_secret) @bp.route('/2fa/disable', methods=['POST']) @login_required def disable_2fa(): current_user.is_2fa_enabled = False current_user.totp_secret = None db.session.commit() flash('2FA disabled successfully', 'success') return redirect(url_for('settings.index'))