From 7dc679567f00c1aec008cc098edcf3d04edf1f8a Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Tue, 14 Apr 2026 20:48:27 +0200 Subject: [PATCH 01/13] fix: skip login on /viewplan for MachAI iframe users Extract is_machai_user() into database_api/ so both frontend and worker share the same check. /viewplan now looks up the task's user_id: MachAI users (non-UUID, not in UserAccount) can view without login; regular users still require authentication and ownership. Co-Authored-By: Claude Opus 4.6 (1M context) --- database_api/is_machai_user.py | 39 ++++++++++++++++++++++++++ frontend_multi_user/src/plan_routes.py | 11 ++++++-- worker_plan_database/app.py | 25 ++--------------- 3 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 database_api/is_machai_user.py diff --git a/database_api/is_machai_user.py b/database_api/is_machai_user.py new file mode 100644 index 00000000..487a3e50 --- /dev/null +++ b/database_api/is_machai_user.py @@ -0,0 +1,39 @@ +"""Check whether a user_id belongs to a MachAI iframe user.""" +import logging +import os +import uuid + +from database_api.planexe_db_singleton import db +from database_api.model_user_account import UserAccount + +logger = logging.getLogger(__name__) + + +def is_machai_user(user_id: str) -> bool: + """Return True if *user_id* belongs to a MachAI iframe user. + + Registered users (home.planexe.org sign-ups, docker admin) use UUID + identifiers and exist in the UserAccount table. MachAI iframe users + use opaque, non-UUID strings and are *not* in the table. + + Must be called inside a Flask app context. + """ + # Registered users and admins use UUIDs as their user_id. + try: + user_uuid = uuid.UUID(str(user_id)) + user = db.session.get(UserAccount, user_uuid) + if user is not None: + logger.debug("is_machai_user: user_id %r found in database — not a MachAI user.", user_id) + return False + except (ValueError, AttributeError): + pass + + # Fallback admin username (non-UUID string like "admin"). + admin_username = os.environ.get("PLANEXE_FRONTEND_MULTIUSER_ADMIN_USERNAME", "") + if admin_username and user_id == admin_username: + logger.debug("is_machai_user: user_id %r matches admin username — not a MachAI user.", user_id) + return False + + # Unknown user — likely a MachAI iframe user. + logger.debug("is_machai_user: user_id %r is unknown — treating as MachAI user.", user_id) + return True diff --git a/frontend_multi_user/src/plan_routes.py b/frontend_multi_user/src/plan_routes.py index 11d421f6..8be9bcba 100644 --- a/frontend_multi_user/src/plan_routes.py +++ b/frontend_multi_user/src/plan_routes.py @@ -877,7 +877,6 @@ def get_progress(): @plan_routes_bp.route("/viewplan") -@login_required def viewplan(): run_id = request.args.get("run_id", "") logger.info("ViewPlan endpoint requested for run_id: %r", run_id) @@ -885,7 +884,15 @@ def viewplan(): if task is None: logger.error("Task not found for run_id: %r", run_id) return jsonify({"error": "Task not found"}), 400 - if not current_user.is_admin and str(task.user_id) != str(current_user.id): + + from database_api.is_machai_user import is_machai_user + if is_machai_user(str(task.user_id)): + # MachAI iframe users can view their plan without login. + logger.info("ViewPlan: MachAI user, skipping auth for run_id=%s", run_id) + elif not current_user.is_authenticated: + # Non-MachAI plan requires login. + return current_app.login_manager.unauthorized() + elif not current_user.is_admin and str(task.user_id) != str(current_user.id): logger.warning("Unauthorized report access attempt. run_id=%s user_id=%s", run_id, current_user.id) return jsonify({"error": "Forbidden"}), 403 diff --git a/worker_plan_database/app.py b/worker_plan_database/app.py index 36b32819..dfebcb50 100644 --- a/worker_plan_database/app.py +++ b/worker_plan_database/app.py @@ -829,31 +829,10 @@ def _credits_for_usd(usd_amount: float) -> Decimal: def _should_send_to_machai(user_id: str) -> bool: """Return True only for MachAI iframe users (not registered in the database). - Registered users (home.planexe.org sign-ups, docker admin) and admin - accounts should NOT have their plan data sent to MachAI. - MachAI iframe users use non-UUID identifiers and are not in the UserAccount table. - Must be called inside a Flask app context. """ - # Registered users and admins use UUIDs as their user_id. - try: - user_uuid = uuid.UUID(str(user_id)) - user = db.session.get(UserAccount, user_uuid) - if user is not None: - logger.debug("_should_send_to_machai: user_id %r found in database, skipping MachAI.", user_id) - return False - except (ValueError, AttributeError): - pass - - # Fallback admin username (non-UUID string like "admin"). - admin_username = os.environ.get("PLANEXE_FRONTEND_MULTIUSER_ADMIN_USERNAME", "") - if admin_username and user_id == admin_username: - logger.debug("_should_send_to_machai: user_id %r matches admin username, skipping MachAI.", user_id) - return False - - # Unknown user — likely a MachAI iframe user. - logger.debug("_should_send_to_machai: user_id %r is unknown, will send to MachAI.", user_id) - return True + from database_api.is_machai_user import is_machai_user + return is_machai_user(user_id) def _resolve_user_for_billing(task_user_id: str) -> Optional[UserAccount]: From 4701e4b1641aef02560ad82c25576f8b7085d60a Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Tue, 14 Apr 2026 20:51:48 +0200 Subject: [PATCH 02/13] refactor: rename /viewplan parameter from run_id to plan_id Accept plan_id as the primary parameter, fall back to run_id for backwards compatibility. Update all internal links. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_multi_user/src/plan_routes.py | 8 ++++---- frontend_multi_user/src/planexe_modelviews.py | 2 +- frontend_multi_user/templates/plan_iframe.html | 2 +- frontend_multi_user/templates/run_via_database.html | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend_multi_user/src/plan_routes.py b/frontend_multi_user/src/plan_routes.py index 8be9bcba..7f3b8721 100644 --- a/frontend_multi_user/src/plan_routes.py +++ b/frontend_multi_user/src/plan_routes.py @@ -878,11 +878,11 @@ def get_progress(): @plan_routes_bp.route("/viewplan") def viewplan(): - run_id = request.args.get("run_id", "") - logger.info("ViewPlan endpoint requested for run_id: %r", run_id) - task = db.session.get(PlanItem, run_id) + plan_id = request.args.get("plan_id") or request.args.get("run_id", "") + logger.info("ViewPlan endpoint requested for plan_id: %r", plan_id) + task = db.session.get(PlanItem, plan_id) if task is None: - logger.error("Task not found for run_id: %r", run_id) + logger.error("Task not found for plan_id: %r", plan_id) return jsonify({"error": "Task not found"}), 400 from database_api.is_machai_user import is_machai_user diff --git a/frontend_multi_user/src/planexe_modelviews.py b/frontend_multi_user/src/planexe_modelviews.py index 6071ca14..a995d416 100644 --- a/frontend_multi_user/src/planexe_modelviews.py +++ b/frontend_multi_user/src/planexe_modelviews.py @@ -143,7 +143,7 @@ class PlanItemView(AdminOnlyModelView): ), 'prompt': lambda v, c, m, p: m.prompt[:100] + '...' if m.prompt and len(m.prompt) > 100 else m.prompt, 'view_plan': lambda v, c, m, p: Markup( - f'View' + f'View' ) if m.has_generated_report_html else '—', 'generated_report_html': lambda v, c, m, p: Markup( f'Download' diff --git a/frontend_multi_user/templates/plan_iframe.html b/frontend_multi_user/templates/plan_iframe.html index c868069e..54c6d89b 100644 --- a/frontend_multi_user/templates/plan_iframe.html +++ b/frontend_multi_user/templates/plan_iframe.html @@ -724,7 +724,7 @@