diff --git a/app.log b/app.log deleted file mode 100644 index 5d40469..0000000 --- a/app.log +++ /dev/null @@ -1,11 +0,0 @@ -Admin already exists. - * Serving Flask app 'app' - * Debug mode: on -WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on all addresses (0.0.0.0) - * Running on http://127.0.0.1:5001 - * Running on http://192.168.1.115:5001 -Press CTRL+C to quit - * Restarting with stat - * Debugger is active! - * Debugger PIN: 837-836-472 diff --git a/app/models.py b/app/models.py index f0e888e..45529db 100644 --- a/app/models.py +++ b/app/models.py @@ -26,6 +26,15 @@ class PrintFile(db.Model): status = db.Column(db.String(50), default='waiting') # waiting, slicing, sliced, failed transform_matrix = db.Column(db.Text, nullable=True) # json format of 16-element array +class UserSession(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + session_token = db.Column(db.String(100), unique=True, nullable=False) + ip_address = db.Column(db.String(50)) + user_agent = db.Column(db.String(255)) + last_active = db.Column(db.DateTime, default=datetime.utcnow) + is_active = db.Column(db.Boolean, default=True) + class SystemConfig(db.Model): id = db.Column(db.Integer, primary_key=True) key = db.Column(db.String(50), unique=True, nullable=False) diff --git a/app/routes/auth_routes.py b/app/routes/auth_routes.py index 7d413a5..3c04bfb 100644 --- a/app/routes/auth_routes.py +++ b/app/routes/auth_routes.py @@ -32,9 +32,28 @@ def login(): remember = bool(request.form.get('remember')) merge_data = bool(request.form.get('merge_data')) + + if user and check_password_hash(user.password_hash, password): login_user(user, remember=remember) + from app.models import UserSession + session_token = str(uuid.uuid4()) + # 尝试获取反向代理传递的真实 IP + client_ip = request.headers.get('X-Real-IP') + if not client_ip: + client_ip = request.remote_addr + + user_session = UserSession( + user_id=user.id, + session_token=session_token, + ip_address=client_ip, + user_agent=request.user_agent.string + ) + db.session.add(user_session) + db.session.commit() + session['user_session_token'] = session_token + if merge_data: guest_id = request.cookies.get('guest_id') if guest_id: @@ -110,10 +129,20 @@ def login(): flash('Invalid username or password', 'danger') return render_template('auth/login.html') + @auth_bp.route('/logout') @login_required def logout(): + session_token = session.get('user_session_token') + if session_token: + from app.models import UserSession + user_session = UserSession.query.filter_by(session_token=session_token).first() + if user_session: + user_session.is_active = False + db.session.commit() logout_user() + session.pop('user_session_token', None) + response = make_response(redirect(url_for('main.index'))) response.delete_cookie('guest_id') # Optionally clear guest cookie return response diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index 7f50b16..ed683ce 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -18,6 +18,24 @@ from app.routes.admin_routes import get_gcode_dir main_bp = Blueprint('main', __name__) +@main_bp.before_app_request +def check_user_session(): + if current_user.is_authenticated and not current_user.is_guest: + from app.models import UserSession + session_token = session.get('user_session_token') + if session_token: + user_session = UserSession.query.filter_by(session_token=session_token).first() + if not user_session or not user_session.is_active: + from flask_login import logout_user + logout_user() + session.pop('user_session_token', None) + flash('Your session has been terminated.', 'warning') + return redirect(url_for('auth.login')) + else: + user_session.last_active = datetime.utcnow() + db.session.commit() + + auth_bp = Blueprint('auth', __name__, url_prefix='/auth') admin_bp = Blueprint('admin', __name__, url_prefix='/admin') @@ -562,3 +580,52 @@ def engine_options(engine_name): patterns = engine.get_support_patterns(current_app) materials = engine.get_materials(current_app) if hasattr(engine, 'get_materials') else [] return jsonify({'presets': presets, 'support_patterns': patterns, 'materials': materials}) + +@main_bp.route('/account', methods=['GET', 'POST']) +@login_required +def account(): + if current_user.is_guest: + flash('Guests cannot manage accounts.', 'danger') + return redirect(url_for('main.index')) + + from app.models import UserSession + + if request.method == 'POST': + action = request.form.get('action') + + if action == 'change_password': + current_pass = request.form.get('current_password') + new_pass = request.form.get('new_password') + confirm_pass = request.form.get('confirm_password') + + if not check_password_hash(current_user.password_hash, current_pass): + flash('Current password is incorrect.', 'danger') + elif new_pass != confirm_pass: + flash('New passwords do not match.', 'danger') + elif len(new_pass) < 6: + flash('New password must be at least 6 characters.', 'danger') + else: + current_user.password_hash = generate_password_hash(new_pass) + db.session.commit() + flash('Password updated successfully.', 'success') + + elif action == 'terminate_session': + session_id = request.form.get('session_id') + token_to_terminate = request.form.get('session_token') + + my_session_token = session.get('user_session_token') + if token_to_terminate == my_session_token: + flash('You cannot terminate your current session from here. Please logout instead.', 'warning') + else: + us = UserSession.query.filter_by(id=session_id, user_id=current_user.id).first() + if us: + us.is_active = False + db.session.commit() + flash('Session terminated.', 'success') + + return redirect(url_for('main.account')) + + sessions = UserSession.query.filter_by(user_id=current_user.id, is_active=True).order_by(UserSession.last_active.desc()).all() + current_token = session.get('user_session_token') + + return render_template('slice/account.html', sessions=sessions, current_token=current_token) diff --git a/app/templates/base.html b/app/templates/base.html index 2a3198e..2a335d6 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -107,6 +107,13 @@ {{ _('Control') }} + {% if current_user.is_authenticated and not current_user.is_guest %} + + {% endif %} {% if current_user.is_authenticated and current_user.is_admin %} @@ -147,6 +154,13 @@ {{ _('Plater') }} + {% if current_user.is_authenticated and not current_user.is_guest %} + + {% endif %} {% if current_user.is_authenticated and current_user.is_admin %} diff --git a/app/templates/slice/account.html b/app/templates/slice/account.html new file mode 100644 index 0000000..2bdabaa --- /dev/null +++ b/app/templates/slice/account.html @@ -0,0 +1,99 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

{{ _('Account Management') }}

+
+ + +
+
+
+
{{ _('Change Password') }}
+
+
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +
+
+
+
{{ _('Active Sessions') }}
+ {{ sessions|length }} +
+
+
+ + + + + + + + + + + {% for s in sessions %} + + + + + + + {% else %} + + + + {% endfor %} + +
{{ _('Device') }}{{ _('IP Address') }}{{ _('Last Active') }}{{ _('Action') }}
+
+ +
+
{{ s.user_agent.split(' ')[0] if s.user_agent else _('Unknown Device') }}
+ {% if s.session_token == current_token %} + {{ _('This Device') }} + {% endif %} +
+
+
+ {{ s.ip_address }} + + {{ s.last_active.strftime('%Y-%m-%d %H:%M:%S') }} + + {% if s.session_token != current_token %} +
+ + + + +
+ {% else %} + + {% endif %} +
{{ _('No active sessions found.') }}
+
+
+
+
+
+{% endblock %} diff --git a/llm_semantic_fix.py b/llm_semantic_fix.py deleted file mode 100644 index 80691e1..0000000 --- a/llm_semantic_fix.py +++ /dev/null @@ -1,93 +0,0 @@ -import os - -# 大模型语义映射表 (Creality/Bambu -> PrusaSlicer) -SEMANTIC_MAP = { - "outer_wall_line_width": "external_perimeter_extrusion_width", - "outer_wall_speed": "external_perimeter_speed", - "line_width": "extrusion_width", - "infill_direction": "fill_angle", - "sparse_infill_density": "fill_density", - "sparse_infill_pattern": "fill_pattern", - "initial_layer_line_width": "first_layer_extrusion_width", - "initial_layer_print_height": "first_layer_height", - "initial_layer_speed": "first_layer_speed", - "gap_infill_speed": "gap_fill_speed", - "infill_wall_overlap": "infill_overlap", - "sparse_infill_speed": "infill_speed", - "initial_layer_acceleration": "first_layer_acceleration", - "travel_speed": "travel_speed", - "bottom_shell_layers": "bottom_solid_layers", - "top_shell_layers": "top_solid_layers", - "top_surface_speed": "top_solid_infill_speed", - "layer_height": "layer_height", - "wall_loops": "perimeters", - "inner_wall_speed": "perimeter_speed", - "raft_layers": "raft_layers", - "brim_width": "brim_width", - "print_sequence": "complete_objects", - "elefant_foot_compensation": "elefant_foot_compensation", - "nozzle_temperature": "temperature", - "first_layer_bed_temperature": "first_layer_bed_temperature", - "bed_temperature": "bed_temperature", - "filament_diameter": "filament_diameter", - "support_material": "support_material", - "support_material_style": "support_material_style", - "retract_length": "retract_length", - "retract_speed": "retract_speed", - "z_hop": "retract_lift" -} - -# 从 prusa_new_cli.txt (或 Prusa 官方默认配置) 中允许通过的原生参数白名单 -NATIVE_ALLOWED = { - "bridge_speed", "bridge_flow", "default_acceleration", "brim_object_gap", - "ironing_type", "filament_cost", "filament_density", "filament_type", - "gcode_flavor", "nozzle_diameter", "start_gcode", "end_gcode", - "before_layer_gcode", "printer_model", "z_offset" -} - -def process_file(filepath): - with open(filepath, 'r') as f: - lines = f.readlines() - - new_lines = [] - changed = False - - for line in lines: - stripped = line.strip() - if not stripped or stripped.startswith('[') or stripped.startswith(';'): - new_lines.append(line) - continue - - if '=' in line: - parts = line.split('=', 1) - key = parts[0].strip() - val = parts[1].strip() - - # 处理特殊语义转换值 - if key == "print_sequence" and val == "by layer": - val = "0" - elif key == "print_sequence" and val == "by object": - val = "1" - - if key in SEMANTIC_MAP: - new_key = SEMANTIC_MAP[key] - new_lines.append(f"{new_key} = {val}\n") - changed = True - elif key in NATIVE_ALLOWED: - new_lines.append(f"{key} = {val}\n") - else: - new_lines.append(f";;;{line}") - changed = True - else: - new_lines.append(line) - - if changed: - with open(filepath, 'w') as f: - f.writelines(new_lines) - print(f"Applied semantic map to {filepath}") - -for root, dirs, files in os.walk('print_config/prusa_slicer'): - for file in files: - if file.endswith('.ini'): - process_file(os.path.join(root, file)) -