diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..c4047f8e8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# Keep ACR build context small and prevent local secrets/artifacts from being uploaded. +** + +!application/ +!application/single_app/ +!application/single_app/** +!docker-customization/ +!docker-customization/** + +.git/ +.github/ +.vscode/ +.azure/ +artifacts/ +deployers/ +docs/ +functional_tests/ +ui_tests/ +scripts/ + +**/.env +**/.env.* +**/.venv/ +**/venv/ +**/__pycache__/ +**/.pytest_cache/ +**/*.pyc +**/*.pyo +**/*.pyd + +application/single_app/flask_session/ +application/single_app/uploaded_openapi_files/ +application/single_app/instance/ \ No newline at end of file diff --git a/.github/instructions/location_of_functional_tests.instructions.md b/.github/instructions/location_of_functional_tests.instructions.md index 538a7cca9..77b88727b 100644 --- a/.github/instructions/location_of_functional_tests.instructions.md +++ b/.github/instructions/location_of_functional_tests.instructions.md @@ -6,12 +6,14 @@ applyTo: '**' ## šŸ“ **Location** All functional tests are stored in: `.\simplechat\functional_tests\` +Check local.instructions.md for any additional instructions on test creation, execution, and maintenance. ## šŸ“‚ **Directory Structure** The functional tests directory contains: - **Python test files** (`test_*.py`) - Executable test scripts - **JavaScript test files** (`test_*.js`) - Client-side/browser test scripts - **Documentation files** (`*.md`) - Test documentation and fix summaries +- **route_tests/** - Route registration, Blueprint policy, and unauthenticated access contract tests - **flask_session/** - Session data for tests requiring authenticated state ## šŸŽÆ **When to Create Functional Tests** @@ -25,6 +27,12 @@ The functional tests directory contains: āœ… **UI/UX Changes** - Test display logic, user interactions, data flow āœ… **Authentication/Security** - Verify access controls and data isolation +### **Always Update Route Tests For:** +āœ… **New Flask Routes** - Add the route to `functional_tests/route_tests/` policy coverage +āœ… **Route Moves or Renames** - Update Blueprint policy and endpoint-name expectations +āœ… **Auth Policy Changes** - Update unauthenticated access expectations and role-policy checks +āœ… **Public or External Routes** - Add explicit public/bearer-token policy entries and justify them in the tests + ### **Test Categories:** - **Integration Tests** - End-to-end functionality across multiple components - **Regression Tests** - Prevent previously fixed bugs from returning @@ -184,6 +192,7 @@ test_config = { - **Plugin Integration Tests** → `functional_tests/` (follow `test_openapi_*.py` patterns) - **Migration Tests** → `functional_tests/` (follow `test_migration.py` pattern) - **UI/Display Tests** → `functional_tests/` (follow `test_*_display.py` patterns) +- **Route Policy Tests** → `functional_tests/route_tests/` for Blueprint registration, route security policy inventory, and unauthenticated access contracts ### **Documentation Requirements:** - **For Bug Fixes**: Create accompanying `.md` file describing the issue and solution @@ -205,6 +214,11 @@ for test in test_*.py; do python $test; done # Run specific test pattern python test_openapi_*.py + +# Run route policy coverage tests +python route_tests/test_route_blueprint_policy_inventory.py +python route_tests/test_route_unauthenticated_policy_contract.py +python route_tests/test_route_policy_test_coverage.py ``` ### **Integration with Development Workflow:** @@ -230,4 +244,4 @@ python test_openapi_*.py ### **Maintenance:** āœ… **Regular Review** - Periodically review and update tests for relevance āœ… **Refactoring** - Extract common patterns into reusable utilities -āœ… **Documentation Updates** - Keep test documentation current with code changes \ No newline at end of file +āœ… **Documentation Updates** - Keep test documentation current with code changes diff --git a/.github/instructions/python-lang.instructions.md b/.github/instructions/python-lang.instructions.md index 213c27be7..0489d3d8c 100644 --- a/.github/instructions/python-lang.instructions.md +++ b/.github/instructions/python-lang.instructions.md @@ -31,10 +31,16 @@ applyTo: '**/*.py' - Files with routes MUST import `from swagger_wrapper import swagger_route, get_auth_security` and use the `@swagger_route(security=get_auth_security())` decorator for all route functions. +- New Flask routes MUST be registered on a `Blueprint`, not directly on `app`. Use `Blueprint(...).route(...)` or pass a `Blueprint` into the route module registrar. Do not add new `@app.route(...)` routes except for a reviewed framework/bootstrap exception. + +- Each route-owning Blueprint MUST have an explicit `before_request` security policy using the shared helpers in `functions_authentication.py`, such as `login_required_blueprint()`, `user_required_blueprint()`, `admin_required_blueprint()`, or `external_api_required_blueprint()`. Mixed-policy Blueprints may use a login-only Blueprint guard and keep stricter route-specific decorators such as `@user_required`, `@admin_required`, `@control_center_required(...)`, `@feedback_admin_required`, or `@safety_violation_admin_required` on individual routes. + +- Every route change MUST update or verify the route coverage tests in `functional_tests/route_tests/`. At minimum, run `python functional_tests/route_tests/test_route_blueprint_policy_inventory.py`, `python functional_tests/route_tests/test_route_unauthenticated_policy_contract.py`, and `python functional_tests/route_tests/test_route_policy_test_coverage.py` after adding, moving, or changing routes. + - When editing group workspace content, always use the `assert_group_role(user_id, group_id, allowed_roles=("Owner", "Admin", "DocumentManager", "User"))` function to verify the user's current membership and role in the group before allowing access to group-scoped resources or operations. This ensures that users cannot access or modify group content based solely on a potentially stale or tampered `activeGroupOid` reference. Roles should vary based on the level of access required for the operation, but should always include "Owner" and "Admin" as allowed roles. -- Unless otherwise indicated, all routes MUST include `@login_required` decorator. +- Unless otherwise indicated, all protected routes MUST be covered by a Blueprint-level login policy and/or an explicit route-level `@login_required` decorator. Public and external bearer-token routes must be listed in the route policy tests with their expected unauthenticated behavior. - Always use f-strings for string interpolation. Ex: `f"User ID: {user_id}"` instead of `"User ID: {}".format(user_id)"` -- Never use `except:` without specifying the exception type. Always catch specific exceptions or use `except Exception as ex:` to capture the exception details. This also avoids accidentally catching system-exiting exceptions like `KeyboardInterrupt` or `SystemExit`. \ No newline at end of file +- Never use `except:` without specifying the exception type. Always catch specific exceptions or use `except Exception as ex:` to capture the exception details. This also avoids accidentally catching system-exiting exceptions like `KeyboardInterrupt` or `SystemExit`. diff --git a/.github/prompts/prepare-for-pull-request.prompt.md b/.github/prompts/prepare-for-pull-request.prompt.md index 22a9fc3a1..305b0b2e5 100644 --- a/.github/prompts/prepare-for-pull-request.prompt.md +++ b/.github/prompts/prepare-for-pull-request.prompt.md @@ -86,7 +86,11 @@ Always run: When Python route files changed: - Run `python scripts/check_swagger_routes.py `. -- Verify every Flask route has `@swagger_route(security=get_auth_security())` immediately after `@app.route(...)`. +- Verify every Flask route is registered on a Blueprint, every route-owning Blueprint has an explicit `before_request` auth policy, and every route has `@swagger_route(security=get_auth_security())` immediately after the route decorator. +- Run the route policy coverage tests: + `python functional_tests/route_tests/test_route_blueprint_policy_inventory.py` + `python functional_tests/route_tests/test_route_unauthenticated_policy_contract.py` + `python functional_tests/route_tests/test_route_policy_test_coverage.py` When JavaScript, HTML, or Python browser-rendering surfaces changed: @@ -110,6 +114,7 @@ When HTML, CSS, or JavaScript changes affect UI behavior: When bug fixes, new features, route changes, API changes, security changes, or UI workflow changes are present: - Check for matching functional tests under `functional_tests/`. +- For route changes, check for matching route policy coverage under `functional_tests/route_tests/`. - Run relevant functional tests with pytest or directly with Python, matching existing test style. - If no relevant test exists, call this out and recommend the missing coverage. @@ -158,4 +163,4 @@ If the user confirms PR creation: - Include a PR body with summary, validation, tests, release notes status, and known risks. - Return the PR URL. -If GitHub CLI is unavailable or unauthenticated, report the exact blocker and provide the PR title/body text for manual creation. \ No newline at end of file +If GitHub CLI is unavailable or unauthenticated, report the exact blocker and provide the PR title/body text for manual creation. diff --git a/.github/prompts/route-authentication-audit.prompt.md b/.github/prompts/route-authentication-audit.prompt.md index 990acf34c..8ddcb46a3 100644 --- a/.github/prompts/route-authentication-audit.prompt.md +++ b/.github/prompts/route-authentication-audit.prompt.md @@ -5,28 +5,30 @@ argument-hint: "Target route files, endpoint prefixes, changed files, or route n agent: "agent" --- -Audit the requested SimpleChat Flask routes for runtime authentication and role authorization decorator coverage. Treat `@swagger_route(security=get_auth_security())` as OpenAPI metadata only; it does not enforce authentication at runtime. +Audit the requested SimpleChat Flask routes for Blueprint-level runtime authentication, route-specific role authorization, and Swagger metadata coverage. Treat `@swagger_route(security=get_auth_security())` as OpenAPI metadata only; it does not enforce authentication at runtime. Use the repository route and access-control guardrails in [.github/copilot-instructions.md](../copilot-instructions.md), [.github/instructions/python-lang.instructions.md](../instructions/python-lang.instructions.md), and [.github/instructions/broken-access-control-prevention.instructions.md](../instructions/broken-access-control-prevention.instructions.md). Use [scripts/check_swagger_routes.py](../../scripts/check_swagger_routes.py) and [scripts/check_broken_access_control.py](../../scripts/check_broken_access_control.py) where useful, but do not stop at deterministic checker output. ## Expected Route Patterns +- Every Flask route must be registered on a `Blueprint`. New `@app.route(...)` routes are not allowed except for a reviewed framework/bootstrap exception. +- Every route-owning Blueprint must have an explicit `before_request` policy using helpers such as `login_required_blueprint()`, `user_required_blueprint()`, `admin_required_blueprint()`, or `external_api_required_blueprint()`. - Every Flask route must include `@swagger_route(security=get_auth_security())` immediately after the route decorator unless the route has a reviewed exception. -- Most non-admin application API routes should use `@login_required` followed by `@user_required` before the view function runs. -- Admin-namespaced or admin-only routes should use `@login_required` followed by `@admin_required`, or a more specific admin decorator such as `@feedback_admin_required` or `@safety_violation_admin_required` when the codebase already uses that role boundary. +- Most non-admin application API routes should be covered by `user_required_blueprint()` or by a login-only Blueprint guard plus route-level `@user_required` before the view function runs. +- Admin-namespaced or admin-only routes should be covered by `admin_required_blueprint()` or by a login-only Blueprint guard plus `@admin_required`, `@control_center_required(...)`, `@feedback_admin_required`, or `@safety_violation_admin_required` where the codebase already uses that role boundary. - Group, public workspace, conversation, document, user profile, and plugin/action routes still need object-level authorization at the sensitive read or mutation boundary. Role decorators are not enough for object access. - Feature flags such as `@enabled_required(...)`, Swagger metadata, frontend-only visibility, and route names are not substitutes for runtime auth decorators. -- Public routes should be rare and explicit. Validate likely public exceptions against nearby files and app behavior, such as health probes, static assets, login/auth callbacks, external API bearer-token routes, and documentation endpoints. +- Public routes should be rare and explicit. Validate likely public exceptions against nearby files and app behavior, such as health probes, static assets, login/auth callbacks, and external API bearer-token routes. Public, login-only, admin, and external bearer routes must be represented in `functional_tests/route_tests/`. ## Audit Workflow -1. Identify all Flask route functions in the target files, including routes registered on blueprints and routes inside `register_route_backend_*` functions. -2. Record each route path, HTTP method, endpoint function, and decorator stack in source order. -3. Classify each route as public, authenticated user, admin, specialized admin, external API, or unclear. -4. Compare the decorator stack against nearby routes in the same file and similar feature areas. Prefer local conventions over generic assumptions. -5. Flag routes that are missing runtime authentication, have only `@login_required` where the surrounding user-facing API pattern expects `@user_required`, or expose admin functionality without an admin decorator. -6. For each protected route, check that `@login_required` appears before role decorators in the source so unauthenticated API callers receive the login boundary before role checks. -7. For routes using object IDs from path, query, body, session settings, or plugin arguments, call out any missing ownership, membership, or relationship validation separately from decorator coverage. +1. Identify all Flask route functions in the target files, including routes registered on Blueprints and routes inside route registrar functions. +2. Record each route path, HTTP method, endpoint function, Blueprint name, Blueprint `before_request` policy, and route decorator stack in source order. +3. Classify each route as public, authenticated user, login-only, admin, specialized admin, external API, or unclear. +4. Compare the Blueprint policy and route decorator stack against nearby routes in the same file and similar feature areas. Prefer local conventions over generic assumptions. +5. Flag routes that are not on a Blueprint, Blueprints without a `before_request` auth policy, protected routes missing a login boundary, routes with only `login_required` where the surrounding user-facing API pattern expects `user_required`, or admin functionality without an admin/specialized admin boundary. +6. For routes using object IDs from path, query, body, session settings, or plugin arguments, call out any missing ownership, membership, or relationship validation separately from Blueprint/decorator coverage. +7. Update or verify `functional_tests/route_tests/test_route_blueprint_policy_inventory.py`, `functional_tests/route_tests/test_route_unauthenticated_policy_contract.py`, and `functional_tests/route_tests/test_route_policy_test_coverage.py` for any new, removed, moved, or policy-changed route. 8. Recommend the smallest remediation and the minimum regression test that would fail before the fix and pass after. ## Output Format @@ -36,10 +38,11 @@ Return findings first, ordered by severity. For each finding include: - `Severity`: Critical, Important, Moderate, or Low. - `Route`: HTTP method and path. - `Function`: file path and function name. +- `Blueprint Policy`: Blueprint name and `before_request` auth policy, or missing. - `Current Decorators`: runtime auth decorators currently present. -- `Expected Decorators`: the route pattern that should be used. +- `Expected Policy`: the Blueprint/route pattern that should be used. - `Impact`: what anonymous, non-user, or non-admin callers could do or learn. - `Remediation`: exact decorator or object-level check to add. -- `Regression Test`: focused test coverage to prevent recurrence. +- `Regression Test`: focused test coverage to prevent recurrence, including route policy tests under `functional_tests/route_tests/` when route coverage changes. -If no issues are found, say that clearly and list any routes intentionally left public or login-only with the reason they are acceptable. \ No newline at end of file +If no issues are found, say that clearly and list any routes intentionally left public or login-only with the reason they are acceptable. diff --git a/.github/workflows/swagger-route-check.yml b/.github/workflows/swagger-route-check.yml index 61e794150..6a9119edb 100644 --- a/.github/workflows/swagger-route-check.yml +++ b/.github/workflows/swagger-route-check.yml @@ -9,6 +9,11 @@ on: paths: - 'application/single_app/**/*.py' - 'scripts/check_swagger_routes.py' + - 'functional_tests/route_tests/**/*.py' + - '.github/instructions/python-lang.instructions.md' + - '.github/instructions/location_of_functional_tests.instructions.md' + - '.github/prompts/route-authentication-audit.prompt.md' + - '.github/prompts/prepare-for-pull-request.prompt.md' - '.github/workflows/swagger-route-check.yml' jobs: @@ -34,6 +39,14 @@ jobs: route_python: - 'application/single_app/**/*.py' - 'scripts/check_swagger_routes.py' + route_policy: + - 'application/single_app/**/*.py' + - 'functional_tests/route_tests/**/*.py' + - '.github/instructions/python-lang.instructions.md' + - '.github/instructions/location_of_functional_tests.instructions.md' + - '.github/prompts/route-authentication-audit.prompt.md' + - '.github/prompts/prepare-for-pull-request.prompt.md' + - '.github/workflows/swagger-route-check.yml' - name: Run swagger route validation env: @@ -47,4 +60,11 @@ jobs: echo "Changed Python files:" printf '%s\n' "$CHANGED_ROUTE_FILES" | tr ' ' '\n' - python scripts/check_swagger_routes.py $CHANGED_ROUTE_FILES \ No newline at end of file + python scripts/check_swagger_routes.py $CHANGED_ROUTE_FILES + + - name: Run route policy validation + if: steps.changed-files.outputs.route_policy_any_changed == 'true' + run: | + python functional_tests/route_tests/test_route_blueprint_policy_inventory.py + python functional_tests/route_tests/test_route_unauthenticated_policy_contract.py + python functional_tests/route_tests/test_route_policy_test_coverage.py diff --git a/.gitignore b/.gitignore index 6f7075a79..e8e2d544b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,8 @@ scripts/me.json deploy.zip +.azure/deployment-plan.md application/teams_app/manifest.json application/teams_app/*.zip -local_storage_state.json \ No newline at end of file +local_storage_state.json diff --git a/application/community_customizations/actions/azure_billing_retriever/agent.instructions.md b/application/community_customizations/actions/azure_billing_retriever/agent.instructions.md index a0eaf84b9..52b530396 100644 --- a/application/community_customizations/actions/azure_billing_retriever/agent.instructions.md +++ b/application/community_customizations/actions/azure_billing_retriever/agent.instructions.md @@ -10,7 +10,7 @@ Use list_resource_groups() for resource groups under a given subscription. Retrieve current and historical charges. Generate cost forecasts for future periods. Display budgets and cost alerts. -Produce Matplotlib (pyplot) visualizations for actual, forecast, or combined datasets, using only the dedicated graphing functions. +Produce SimpleChat dynamic inline charts for actual, forecast, or combined datasets, using only the dedicated graphing functions. Use run_data_query(...) exclusively for data retrieval. When a visualization is requested, in the same turn: Execute run_data_query(...) with the appropriate parameters. @@ -47,7 +47,7 @@ Output Rules Do not truncate data unless the user explicitly requests it. When displaying tables, render full Markdown tables with all rows/columns. When producing CSV output, return the full CSV without truncation. -Do not embed binary data or raw images. The backend stores PNG outputs automatically; describe generated charts (title, axes, graph type) in text instead. +Do not embed binary data or raw images. Use the `chart_markdown` returned by plot_chart so the chat UI renders a dynamic chart. For every visualization request: Call run_data_query(...) to obtain rows, csv, and plot_hints. Immediately call plot_chart(...) (or plot_custom_chart(...)) with: @@ -55,6 +55,7 @@ conversation_id data = the returned rows or csv x_keys/y_keys chosen from plot_hints An appropriate graph_type from the recommended options +Return or include the `chart_markdown` value from the plot result in the response. Do not ask the user to restate parameters already inferred or used. Error Handling and Recovery Classify errors using: MissingParameter, BadDateFormat, UnknownEnum, NotFound, Authz, Throttle, ServiceError. diff --git a/application/community_customizations/actions/azure_billing_retriever/azure_billing_plugin.py b/application/community_customizations/actions/azure_billing_retriever/azure_billing_plugin.py index f0377603f..4a011fbe0 100644 --- a/application/community_customizations/actions/azure_billing_retriever/azure_billing_plugin.py +++ b/application/community_customizations/actions/azure_billing_retriever/azure_billing_plugin.py @@ -3,26 +3,20 @@ Azure Billing Plugin for Semantic Kernel - Supports user (Entra ID) and service principal authentication - Uses Azure Cost Management REST API for billing, budgets, alerts, forecasting -- Renders graphs server-side as PNG (base64 for web, downloadable) +- Renders dynamic SimpleChat inline charts for Cost Management data - Returns tabular data as CSV for minimal token usage - Requires user_impersonation for user auth on 40a69793-8fe6-4db1-9591-dbc5c57b17d8 (Azure Service Management) """ import io -import base64 import requests import csv -import matplotlib.pyplot as plt import logging import time -import random import re -import numpy as np import datetime -import textwrap from typing import Dict, Any, List, Optional, Union import json -from collections import defaultdict from semantic_kernel_plugins.base_plugin import BasePlugin from semantic_kernel.functions import kernel_function from semantic_kernel_plugins.plugin_invocation_logger import plugin_function_logger @@ -30,7 +24,7 @@ from functions_appinsights import log_event from functions_debug import debug_print from azure.core.credentials import AccessToken, TokenCredential -from config import cosmos_messages_container, cosmos_conversations_container +from semantic_kernel_plugins.chart_plugin import ChartPlugin RESOURCE_ID_REGEX = r"^/subscriptions/(?P[a-fA-F0-9-]+)/?(?:resourceGroups/(?P[^/]+))?$" @@ -42,6 +36,13 @@ AGGREGATION_COLUMNS= ["Cost", "CostUSD", "PreTaxCost", "PreTaxCostUSD"] DEFAULT_GROUPING_DIMENSIONS = ["None", "BillingPeriod", "ChargeType", "Frequency", "MeterCategory", "MeterId", "MeterSubCategory", "Product", "ResourceGroupName", "ResourceLocation", "ResourceType", "ServiceFamily", "ServiceName", "SubscriptionId", "SubscriptionName", "Tag"] SUPPORTED_GRAPH_TYPES = ["pie", "column_stacked", "column_grouped", "line", "area"] +GRAPH_TYPE_TO_INLINE_CHART_KIND = { + "pie": "pie", + "column_stacked": "stacked_bar", + "column_grouped": "bar", + "line": "line", + "area": "area", +} AZURE_BILLING_AUTH_FAILURE_MESSAGE = ( "Azure Billing service principal authentication failed. " "Check Application Insights for [AzureBilling] details." @@ -201,23 +202,6 @@ def _flatten_dict(self, d: Dict[str, Any], parent_key: str = '', sep: str = '.') items[new_key] = v return items - def _fig_to_base64_dict(self, fig, filename: str = "chart.png") -> Dict[str, str]: - """Convert a matplotlib Figure to a structured base64 dict. - - Returns: {"mime": "image/png", "filename": filename, "base64": , "image_url": "data:image/png;base64,"} - """ - buf = io.BytesIO() - fig.savefig(buf, format='png', bbox_inches='tight') - fig.clf() - buf.seek(0) - img_b64 = base64.b64encode(buf.read()).decode('utf-8') - return { - "mime": "image/png", - "filename": filename, - "base64": img_b64, - "image_url": f"data:image/png;base64,{img_b64}" - } - def _parse_csv_to_rows(self, data_csv: Union[str, List[str]]) -> List[Dict[str, Any]]: """Parse CSV content (string or list-of-lines) into list[dict]. @@ -512,7 +496,7 @@ def metadata(self) -> Dict[str, Any]: return { "name": self.metadata_dict.get("name", "azure_billing_plugin"), "type": "azure_billing", - "description": "Azure Billing plugin for cost, budgets, alerts, forecasting, CSV export, and PNG graphing.", + "description": "Azure Billing plugin for cost, budgets, alerts, forecasting, CSV export, and dynamic inline charting.", "methods": self._collect_kernel_methods_for_metadata() } @@ -535,7 +519,7 @@ def suggest_plot_config(self, data, columns: Optional[List[Dict[str, Any]]] = No hints = self._build_plot_hints(rows, columns) return hints - @kernel_function(description="Plot a chart/graph from provided data. Supports pie, column_stacked, column_grouped, line, and area.",) + @kernel_function(description="Build a dynamic SimpleChat chart from provided data. Supports pie, column_stacked, column_grouped, line, and area.",) @plugin_function_logger("AzureBillingPlugin") def plot_chart(self, conversation_id: str, @@ -561,281 +545,137 @@ def plot_chart(self, figsize=figsize ) - def _estimate_legend_items( - self, - graph_type: str, - rows: List[Dict[str, Any]], - y_keys_list: List[str], - stack_col: Optional[str], - ) -> int: - """Return the number of legend entries expected for a plot.""" - if graph_type == "pie": - return len(rows) - if graph_type == "column_stacked": - if stack_col: - return len({r.get(stack_col) for r in rows if r.get(stack_col) is not None}) - return len(y_keys_list) - return len(y_keys_list) - - def _adjust_figsize(self, base_figsize: List[float], legend_items: int) -> List[float]: - """Scale the figsize heuristically based on legend size.""" - scaled = list(base_figsize) - if legend_items > 6: - extra_width = min(legend_items * 0.12, 5.0) - scaled[0] = base_figsize[0] + extra_width - elif legend_items > 3: - scaled[1] = base_figsize[1] + 0.8 - if legend_items > 10: - scaled[1] = max(scaled[1], base_figsize[1] + min((legend_items - 10) * 0.2, 3.0)) - return scaled - - def _wrap_title(self, title: str, width: int = 60) -> str: - """Return a wrapped title so long strings stay inside the figure.""" - - if not title: - return "" - try: - return textwrap.fill(title, width=max(20, width)) - except Exception: - return title - - def _pie_autopct_formatter(self, values: List[float]): - """Return an autopct formatter that prints absolute value and percentage for top slices only.""" - - total = sum(values) or 1.0 - # Show labels for the most meaningful slices to avoid visual clutter. - sorted_indices = sorted(range(len(values)), key=lambda i: values[i], reverse=True) - max_labels = 8 if len(values) >= 15 else 12 - pct_threshold = 2.0 if len(values) >= 12 else 0.5 - show_indices = set() - for idx in sorted_indices[:max_labels]: - pct = (values[idx] / total) * 100 - if pct >= pct_threshold: - show_indices.add(idx) - - call_count = {"idx": -1} - - def _format(pct: float) -> str: - call_count["idx"] += 1 - idx = call_count["idx"] - if idx not in show_indices: - return "" - value = values[idx] - value_str = f"{value:,.0f}" if abs(value) >= 1000 else f"{value:,.2f}" - return f"{value_str}\n({pct:.1f}%)" - - return _format - - def _annotate_column_totals(self, ax, positions: List[float], totals: List[float]) -> None: - """Annotate summed column totals above each bar cluster and extend axes if needed.""" - - if not totals or not positions: - return - safe_totals: List[float] = [] - for value in totals: - try: - safe_totals.append(float(value)) - except (TypeError, ValueError): - safe_totals.append(0.0) - if not safe_totals: - return - abs_max = max(max(safe_totals), abs(min(safe_totals)), 1.0) - offset = max(abs_max * 0.02, 0.5) - headroom = max(abs_max * 0.05, offset) - label_positions: List[float] = [] - for x, total in zip(positions, safe_totals): - y = total + offset if total >= 0 else total - offset - label_positions.append(y) - va = 'bottom' if total >= 0 else 'top' - ax.text( - x, - y, - f"{total:,.2f}", - ha='center', - va=va, - fontsize=8, - fontweight='bold' - ) + def _coerce_number_for_chart(self, value: Any, field_name: str) -> float: + """Normalize chart numeric values while keeping validation errors explicit.""" - if label_positions: - current_bottom, current_top = ax.get_ylim() - max_label = max(label_positions) - min_label = min(label_positions) - pad = headroom - top_needed = max_label + pad - bottom_needed = min_label - pad - new_bottom = current_bottom - new_top = current_top - if top_needed > current_top: - new_top = top_needed - if bottom_needed < current_bottom: - new_bottom = bottom_needed - if new_bottom != current_bottom or new_top != current_top: - ax.set_ylim(new_bottom, new_top) - - def _place_side_legend( - self, - ax, - handles: Optional[List[Any]] = None, - labels: Optional[List[Any]] = None, - title: Optional[str] = None, - ncol: int = 1, - ) -> bool: - """Place legend to the right of the axes and reserve horizontal space.""" - if handles is not None or labels is not None: - legend = ax.legend( - handles, - labels, - title=title, - loc="center left", - bbox_to_anchor=(1.02, 0.5), - borderaxespad=0.0, - ncol=ncol, - ) - else: - legend = ax.legend( - title=title, - loc="center left", - bbox_to_anchor=(1.02, 0.5), - borderaxespad=0.0, - ncol=ncol, - ) - if legend is not None: - ax.figure.subplots_adjust(right=0.78) - return True - return False + if value in (None, ""): + return 0.0 + if isinstance(value, bool): + raise ValueError(f"{field_name} must be numeric.") + if isinstance(value, (int, float)): + return float(value) - def _plot_pie_chart( + try: + return float(str(value).strip().replace(",", "")) + except (TypeError, ValueError) as exc: + raise ValueError(f"{field_name} must be numeric.") from exc + + def _aggregate_rows_for_series_chart( self, - ax, rows: List[Dict[str, Any]], x_key: str, - y_key: str, - title: str, - xlabel: str, - ylabel: str, - ) -> bool: - labels = [r.get(x_key) for r in rows] - labels_display = ["Unknown" if label in (None, "") else str(label) for label in labels] - values = [float(r.get(y_key) or 0) for r in rows] - total_value = sum(values) - autopct = self._pie_autopct_formatter(values) - wedges, _, autotexts = ax.pie(values, autopct=autopct, startangle=90) - for autotext in autotexts: - autotext.set_fontsize(8) - ax.set_title(self._wrap_title(title or "Cost distribution")) - ax.text(0, 0, f"Total\n{total_value:,.2f}", ha='center', va='center', fontsize=10, fontweight='bold') - legend_labels = [] - for label, value in zip(labels_display, values): - value_str = f"{value:,.2f}" if abs(value) < 1000 else f"{value:,.0f}" - pct = (value / total_value * 100) if total_value else 0 - legend_labels.append(f"{label} — {value_str} ({pct:.1f}%)") - legend_title = f"{x_key} (Total: {total_value:,.2f})" - ncol = min(4, max(1, len(labels_display) // 10 + 1)) - return self._place_side_legend(ax, wedges, legend_labels, title=legend_title, ncol=ncol) - - def _plot_line_or_area_chart( + series_key: str, + value_key: str, + ) -> List[Dict[str, Any]]: + """Sum duplicate x/series pairs before sending data to the inline chart renderer.""" + + ordered_keys: List[tuple[str, str]] = [] + totals: Dict[tuple[str, str], float] = {} + for row in rows: + label = str(row.get(x_key, "") or "Unknown") + series = str(row.get(series_key, "") or "Unknown") + aggregate_key = (label, series) + if aggregate_key not in totals: + ordered_keys.append(aggregate_key) + totals[aggregate_key] = 0.0 + totals[aggregate_key] += self._coerce_number_for_chart(row.get(value_key), value_key) + + return [ + { + x_key: label, + series_key: series, + value_key: totals[(label, series)], + } + for label, series in ordered_keys + ] + + def _build_inline_chart_data( self, - ax, rows: List[Dict[str, Any]], - x_vals: List[Any], - y_keys_list: List[str], graph_type: str, x_key: str, - xlabel: str, - ylabel: str, - title: str, - ) -> bool: - for yk in y_keys_list: - y_vals = [float(r.get(yk) or 0) for r in rows] - if graph_type == "line": - ax.plot(x_vals, y_vals, marker='o', label=yk) - else: - ax.fill_between(range(len(x_vals)), y_vals, alpha=0.5, label=yk) - ax.set_title(self._wrap_title(title or "Cost trend")) - ax.set_xlabel(xlabel or x_key) - ax.set_ylabel(ylabel or (y_keys_list[0] if y_keys_list else "Value")) - ax.grid(True, axis='y', alpha=0.3) - return self._place_side_legend(ax) - - def _plot_column_grouped_chart( - self, - ax, - rows: List[Dict[str, Any]], - x_vals: List[Any], y_keys_list: List[str], + stack_col: Optional[str], + ) -> Dict[str, Any]: + """Build the row payload expected by the shared SimpleChat chart action.""" + + if graph_type == "pie": + return { + "rows": rows, + "labelField": x_key, + "valueField": y_keys_list[0], + } + + if stack_col and y_keys_list: + value_key = y_keys_list[0] + return { + "rows": self._aggregate_rows_for_series_chart(rows, x_key, stack_col, value_key), + "xField": x_key, + "seriesField": stack_col, + "valueField": value_key, + } + + return { + "rows": rows, + "xField": x_key, + "yFields": y_keys_list, + } + + def _build_inline_chart_options( + self, + graph_type: str, x_key: str, + y_keys_list: List[str], xlabel: str, ylabel: str, - title: str, - ) -> bool: - n_groups = len(rows) - n_bars = len(y_keys_list) - index = np.arange(n_groups) - bar_width = 0.8 / max(1, n_bars) - group_totals = [0.0 for _ in rows] - for i, yk in enumerate(y_keys_list): - y_vals = [float(r.get(yk) or 0) for r in rows] - # accumulate totals for the annotation step below - group_totals = [total + value for total, value in zip(group_totals, y_vals)] - ax.bar(index + i * bar_width, y_vals, bar_width, label=yk) - ax.set_xticks(index + bar_width * (n_bars - 1) / 2) - ax.set_xticklabels([str(x) for x in x_vals], rotation=45, ha='right') - ax.set_title(self._wrap_title(title or "Cost comparison")) - ax.set_xlabel(xlabel or x_key) - ax.set_ylabel(ylabel or ("Values" if len(y_keys_list) > 1 else y_keys_list[0])) - centers = (index + bar_width * (n_bars - 1) / 2).tolist() - self._annotate_column_totals(ax, centers, group_totals) - return self._place_side_legend(ax) - - def _plot_column_stacked_chart( + ) -> Dict[str, Any]: + """Return display options understood by the SimpleChat inline chart renderer.""" + + return { + "beginAtZero": True, + "legendPosition": "top", + "showDataTable": True, + "showLegend": True, + "smooth": graph_type in {"line", "area"}, + "stacked": graph_type == "column_stacked", + "xAxisLabel": xlabel or x_key, + "yAxisLabel": ylabel or (y_keys_list[0] if len(y_keys_list) == 1 else "Values"), + } + + def _build_inline_chart_result( self, - ax, + graph_type: str, rows: List[Dict[str, Any]], x_key: str, y_keys_list: List[str], stack_col: Optional[str], + title: str, xlabel: str, ylabel: str, - title: str, - ) -> bool: - x_vals_unique: List[Any] = [] - seen_x = set() - for r in rows: - xval = r.get(x_key) - if xval not in seen_x: - seen_x.add(xval) - x_vals_unique.append(xval) - - pivot = defaultdict(lambda: defaultdict(float)) - if stack_col: - for r in rows: - xval = r.get(x_key) - sval = r.get(stack_col) - yval = float(r.get(y_keys_list[0]) or 0) - pivot[xval][sval] += yval - y_keys_plot = sorted({key for row in pivot.values() for key in row.keys()}) - else: - for r in rows: - xval = r.get(x_key) - for yk in y_keys_list: - pivot[xval][yk] += float(r.get(yk) or 0) - y_keys_plot = y_keys_list - - data_matrix = [[pivot[x_val].get(yk, 0.0) for x_val in x_vals_unique] for yk in y_keys_plot] - index = np.arange(len(x_vals_unique)) - bottoms = np.zeros(len(x_vals_unique)) - for i, yk in enumerate(y_keys_plot): - ax.bar(index, data_matrix[i], bottom=bottoms, label=str(yk)) - bottoms += np.array(data_matrix[i]) - ax.set_xticks(index) - ax.set_xticklabels([str(x) for x in x_vals_unique], rotation=45, ha='right') - ax.set_title(self._wrap_title(title or "Cost breakdown")) - ax.set_xlabel(xlabel or x_key) - ax.set_ylabel(ylabel or (y_keys_list[0] if y_keys_list else "Values")) - legend_title = stack_col or "Segments" - self._annotate_column_totals(ax, index.tolist(), bottoms.tolist()) - return self._place_side_legend(ax, title=legend_title) + ) -> Dict[str, Any]: + """Use the shared chart plugin to produce validated simplechart markdown.""" + + chart_kind = GRAPH_TYPE_TO_INLINE_CHART_KIND[graph_type] + chart_data = self._build_inline_chart_data(rows, graph_type, x_key, y_keys_list, stack_col) + chart_options = self._build_inline_chart_options(graph_type, x_key, y_keys_list, xlabel, ylabel) + chart_plugin = ChartPlugin({"chart_capabilities": {chart_kind: True}}) + chart_result = chart_plugin.create_chart( + chart_type=chart_kind, + chart_data_json=json.dumps(chart_data, separators=(",", ":")), + title=title or "Azure cost chart", + x_axis_label=chart_options["xAxisLabel"], + y_axis_label=chart_options["yAxisLabel"], + options_json=json.dumps(chart_options, separators=(",", ":")), + ) + + if not chart_result.get("success"): + return { + "status": "error", + "error": chart_result.get("error") or "Failed to build inline chart payload.", + "error_type": chart_result.get("error_type", "validation"), + } + + return chart_result def plot_custom_chart(self, conversation_id: str, @@ -855,27 +695,9 @@ def plot_custom_chart(self, - x_keys: list of keys to use for x axis (required for non-pie charts); first key is primary x-axis, additional keys are used for stacking/grouping - y_keys: list of keys to plot on y axis (if None and graph_type is not pie, autodetect numeric columns) - graph_type: one of ['pie', 'column_stacked', 'column_grouped', 'line', 'area'] - - returns structured dict with mime, filename, base64, image_url and metadata + - returns a structured dict with chart_payload and chart_markdown for SimpleChat inline rendering """ - try: - #print(f"[AzureBillingPlugin] plot_custom_chart called with conversation_id={conversation_id}, graph_type={graph_type},\n x_keys={x_keys},\n y_keys={y_keys},\n title={title},\n xlabel={xlabel},\n ylabel={ylabel},\n figsize={figsize},\n data:{data}") - graph_type = graph_type.lower() if isinstance(graph_type, str) else str(graph_type) - # Validate figsize: must be a list/tuple of two numbers if provided - if figsize is None: - figsize = [7.0, 5.0] - elif isinstance(figsize, (list, tuple)): - if len(figsize) != 2: - return {"status": "error", "error": "figsize must be a list of two numbers: [width, height]"} - try: - figsize = [float(figsize[0]), float(figsize[1])] - except Exception: - return {"status": "error", "error": "figsize elements must be numeric"} - else: - return {"status": "error", "error": "figsize must be a list of two numbers or null"} - - except Exception as ex: - logging.exception("Unexpected error in plot_custom_chart parameter validation") - return {"status": "error", "error": str(ex)} + graph_type = graph_type.lower() if isinstance(graph_type, str) else str(graph_type) if graph_type not in SUPPORTED_GRAPH_TYPES: raise ValueError(f"Unsupported graph_type '{graph_type}'. Supported: {SUPPORTED_GRAPH_TYPES}") try: @@ -932,7 +754,6 @@ def ensure_list(value) -> List[Any]: x_key = x_keys_list[0] stack_col = None - x_vals = None else: if not y_keys_list: y_keys_list = ensure_list(recommended_defaults.get("y_keys")) @@ -957,99 +778,42 @@ def ensure_list(value) -> List[Any]: x_key = x_keys_list[0] stack_col = x_keys_list[1] if len(x_keys_list) > 1 else None - x_vals = [r.get(x_key) for r in rows] - fig = None try: - legend_items = self._estimate_legend_items(graph_type, rows, y_keys_list, stack_col) - scaled_figsize = self._adjust_figsize(figsize, legend_items) - - fig, ax = plt.subplots(figsize=tuple(scaled_figsize)) - - legend_outside = False - if graph_type == "pie": - legend_outside = self._plot_pie_chart( - ax, - rows, - x_keys_list[0], - y_keys_list[0], - title, - xlabel, - ylabel, - ) - elif graph_type in ("line", "area"): - legend_outside = self._plot_line_or_area_chart( - ax, - rows, - x_vals, - y_keys_list, - graph_type, - x_key, - xlabel, - ylabel, - title, - ) - elif graph_type == "column_grouped": - legend_outside = self._plot_column_grouped_chart( - ax, - rows, - x_vals, - y_keys_list, - x_key, - xlabel, - ylabel, - title, - ) - elif graph_type == "column_stacked": - legend_outside = self._plot_column_stacked_chart( - ax, - rows, - x_key, - y_keys_list, - stack_col, - xlabel, - ylabel, - title, - ) + chart_result = self._build_inline_chart_result( + graph_type=graph_type, + rows=rows, + x_key=x_key, + y_keys_list=y_keys_list, + stack_col=stack_col, + title=title, + xlabel=xlabel, + ylabel=ylabel, + ) + if chart_result.get("status") == "error": + return chart_result - if legend_outside: - plt.tight_layout(rect=[0, 0, 0.78, 1]) - else: - plt.tight_layout() - img_b64 = self._fig_to_base64_dict(fig, filename=filename) - payload = { + return { "status": "ok", - "type": "image_url", - "image_url": {"url": str(img_b64.get("image_url", ""))}, + "type": "inline_chart", + "chart_type": chart_result.get("chart_type"), + "chart_payload": chart_result.get("chart_payload"), + "chart_markdown": chart_result.get("chart_markdown"), + "summary": chart_result.get("summary", ""), "metadata": { "graph_type": graph_type, + "chart_type": chart_result.get("chart_type"), + "conversation_id": conversation_id, "x_keys": x_keys_list, "y_keys": y_keys_list, "stack_key": stack_col, - "figure_size": scaled_figsize, - "recommendations": hints.get("recommended", {}) + "recommendations": hints.get("recommended", {}), + "renderer": "simplechart", } } - - if conversation_id: - try: - self.upload_cosmos_message(conversation_id, str(img_b64.get("image_url", ""))) - payload["image_url"] = f"Stored chart image for conversation {conversation_id}" - payload["requires_message_reload"] = True - except Exception: - logging.exception("Failed to upload chart image to Cosmos DB") - payload.setdefault("warnings", []).append("Chart rendered but storing to conversation failed.") - else: - payload.setdefault("warnings", []).append("Chart rendered but conversation_id was not provided; image not persisted.") - - #time.sleep(5) # give time for image to upload before returning - return payload except Exception as ex: - logging.exception("Error while generating chart") - return {"status": "error", "error": f"Error while generating chart: {str(ex)}"} - finally: - if fig is not None: - plt.close(fig) + logging.exception("Error while generating inline chart") + return {"status": "error", "error": f"Error while generating inline chart: {str(ex)}"} @plugin_function_logger("AzureBillingPlugin") @@ -1518,7 +1282,7 @@ def get_run_data_query_format(self) -> Dict[str, Any]: Includes required/optional fields, types, valid values, and reflects the latest method signature. """ return { - "conversation_id": "", + "conversation_id": "", "subscription_id": "", "resource_group_name": "", "query_type": f"", @@ -1586,12 +1350,12 @@ def get_run_data_query_format(self) -> Dict[str, Any]: "Always supply at least one aggregation entry; the plugin no longer infers defaults when none are provided.", "Include at least one grouping (Dimension + name) so the query can bucket the data.", "Inspect plot_hints['recommended'] for suggested x_keys, y_keys, and chart types.", - "Pass rows (or the csv string) plus the chosen keys into plot_chart to render and persist a graph." + "Pass rows (or the csv string) plus the chosen keys into plot_chart to return chart_markdown for inline rendering." ] } # Returns the expected input data format for plot_custom_chart - @kernel_function(description="Get the expected input data format for plot_custom_chart (graphing) as JSON.") + @kernel_function(description="Get the expected input data format for plot_custom_chart dynamic inline charting as JSON.") @plugin_function_logger("AzureBillingPlugin") def get_plot_chart_format(self) -> Dict[str, Any]: """ @@ -1607,130 +1371,17 @@ def get_plot_chart_format(self) -> Dict[str, Any]: "title": "Cost share by resource type", "xlabel": "Resource Type", "ylabel": "Cost (USD)", - "filename": "chart.png", - "figsize": [7.0, 5.0], + "filename": "", + "figsize": "", + "output": { + "type": "inline_chart", + "chart_markdown": "```simplechart\\n{...}\\n```", + "chart_payload": "" + }, "notes": [ "Feed the list returned in run_data_query['rows'] directly, or supply the CSV from run_data_query['csv'].", "Pick x_keys/y_keys from run_data_query['plot_hints']['recommended'] to ensure compatible chart input.", - "Pie charts require exactly one numeric y_key; stacked/grouped charts accept multiple." + "Pie charts require exactly one numeric y_key; stacked/grouped charts accept multiple.", + "plot_chart returns chart_markdown for SimpleChat inline rendering; filename and figsize are retained only for backward-compatible callers and are ignored." ] } - - def upload_cosmos_message(self, - conversation_id: str, - content: str) -> Dict[str, Any]: - """ - Upload a message to Azure Cosmos DB. - """ - try: - image_message_id = f"{conversation_id}_image_{int(time.time())}_{random.randint(1000,9999)}" - # Check if image data is too large for a single Cosmos document (2MB limit) - # Account for JSON overhead by using 1.5MB as the safe limit for base64 content - max_content_size = 1500000 # 1.5MB in bytes - - if len(content) > max_content_size: - debug_print(f"Large image detected ({len(content)} bytes), splitting across multiple documents") - - # Split the data URL into manageable chunks - if content.startswith('data:image/png;base64,'): - # Extract just the base64 part for splitting - data_url_prefix = 'data:image/png;base64,' - base64_content = content[len(data_url_prefix):] - debug_print(f"Extracted base64 content length: {len(base64_content)} bytes") - else: - # For regular URLs, store as-is (shouldn't happen with large content) - data_url_prefix = '' - base64_content = content - - # Calculate chunk size and number of chunks - chunk_size = max_content_size - len(data_url_prefix) - 200 # More room for JSON overhead - chunks = [base64_content[i:i+chunk_size] for i in range(0, len(base64_content), chunk_size)] - total_chunks = len(chunks) - - debug_print(f"Splitting into {total_chunks} chunks of max {chunk_size} bytes each") - for i, chunk in enumerate(chunks): - debug_print(f"Chunk {i} length: {len(chunk)} bytes") - - # Verify we can reassemble before storing - reassembled_test = data_url_prefix + ''.join(chunks) - if len(reassembled_test) == len(content): - debug_print(f"āœ… Chunking verification passed - can reassemble to original size") - else: - debug_print(f"āŒ Chunking verification failed - {len(reassembled_test)} vs {len(content)}") - - - # Create main image document with metadata - main_image_doc = { - 'id': image_message_id, - 'conversation_id': conversation_id, - 'role': 'image', - 'content': f"{data_url_prefix}{chunks[0]}", # First chunk with data URL prefix - 'prompt': '', - 'created_at': datetime.datetime.utcnow().isoformat(), - 'timestamp': datetime.datetime.utcnow().isoformat(), - 'model_deployment_name': 'azurebillingplugin', - 'metadata': { - 'is_chunked': True, - 'total_chunks': total_chunks, - 'chunk_index': 0, - 'original_size': len(content) - } - } - - # Create additional chunk documents - chunk_docs = [] - for i in range(1, total_chunks): - chunk_doc = { - 'id': f"{image_message_id}_chunk_{i}", - 'conversation_id': conversation_id, - 'role': 'image_chunk', - 'content': chunks[i], - 'parent_message_id': image_message_id, - 'created_at': datetime.datetime.utcnow().isoformat(), - 'timestamp': datetime.datetime.utcnow().isoformat(), - 'metadata': { - 'is_chunk': True, - 'chunk_index': i, - 'total_chunks': total_chunks, - 'parent_message_id': image_message_id - } - } - chunk_docs.append(chunk_doc) - - # Store all documents - debug_print(f"Storing main document with content length: {len(main_image_doc['content'])} bytes") - cosmos_messages_container.upsert_item(main_image_doc) - - for i, chunk_doc in enumerate(chunk_docs): - debug_print(f"Storing chunk {i+1} with content length: {len(chunk_doc['content'])} bytes") - cosmos_messages_container.upsert_item(chunk_doc) - - debug_print(f"Successfully stored image in {total_chunks} documents") - debug_print(f"Main doc content starts with: {main_image_doc['content'][:50]}...") - debug_print(f"Main doc content ends with: ...{main_image_doc['content'][-50:]}") - else: - # Small image - store normally in single document - debug_print(f"Small image ({len(content)} bytes), storing in single document") - - image_doc = { - 'id': image_message_id, - 'conversation_id': conversation_id, - 'role': 'image', - 'content': content, - 'prompt': "", - 'created_at': datetime.datetime.utcnow().isoformat(), - 'timestamp': datetime.datetime.utcnow().isoformat(), - 'model_deployment_name': "azurebillingplugin", - 'metadata': { - 'is_chunked': False, - 'original_size': len(content) - } - } - cosmos_messages_container.upsert_item(image_doc) - conversation_item = cosmos_conversations_container.read_item(item=conversation_id, partition_key=conversation_id) - conversation_item['last_updated'] = datetime.datetime.utcnow().isoformat() - cosmos_conversations_container.upsert_item(conversation_item) - #time.sleep(5) # sleep to allow the message to propogate and the front end to pick it up when receiving the agent response - except Exception as e: - print(f"[ABP] Error uploading image message to Cosmos DB: {str(e)}") - logging.error(f"[ABP] Error uploading image message to Cosmos DB: {str(e)}") diff --git a/application/community_customizations/actions/azure_billing_retriever/readme.md b/application/community_customizations/actions/azure_billing_retriever/readme.md index 982ec8abb..365a2a4b5 100644 --- a/application/community_customizations/actions/azure_billing_retriever/readme.md +++ b/application/community_customizations/actions/azure_billing_retriever/readme.md @@ -3,20 +3,20 @@ # Azure Billing Action Instructions ## Overview -The Azure Billing action is an experimental Semantic Kernel plugin that helps agents explore Azure Cost Management data, generate CSV outputs, and render server-side charts for conversational reporting. It stitches together Azure REST APIs, matplotlib rendering, and Cosmos DB persistence so prototype agents can investigate subscriptions, budgets, alerts, and forecasts without touching the production portal. It leverages message injection (direct cosmos_messages_container access) to store chart images as conversation artifacts in lieu of embedding binary data in chat responses. You will need to move the ```azure_billing_plugin.py``` to the [semantic-kernel-plugins](../../../single_app/semantic_kernel_plugins/) folder, and move the ```schema.json``` and ```definition.json``` to the [schemas](../../../single_app/static/json/schemas) folder. +The Azure Billing action is an experimental Semantic Kernel plugin that helps agents explore Azure Cost Management data, generate CSV outputs, and render dynamic SimpleChat charts for conversational reporting. It stitches together Azure REST APIs and the shared SimpleChat `simplechart` renderer so prototype agents can investigate subscriptions, budgets, alerts, and forecasts without touching the production portal. You will need to move the ```azure_billing_plugin.py``` to the [semantic-kernel-plugins](../../../single_app/semantic_kernel_plugins/) folder, and move the ```schema.json``` and ```definition.json``` to the [schemas](../../../single_app/static/json/schemas) folder. ## Core capabilities - Enumerate subscriptions and resource groups via `list_subscriptions*` helpers for quick scope discovery. - Query budgets, alerts, and forecast data with Cost Management APIs, returning flattened CSV for low-token conversations. - Execute fully custom `run_data_query(...)` calls that enforce ISO-8601 time windows, aggregations, and groupings while emitting plot hints. -- Generate Matplotlib charts (`pie`, `column_stacked`, `column_grouped`, `line`, `area`) through `plot_chart` / `plot_custom_chart`, storing PNGs in Cosmos DB per conversation. +- Generate dynamic inline charts (`pie`, `column_stacked`, `column_grouped`, `line`, `area`) through `plot_chart` / `plot_custom_chart`, returning `chart_markdown` for the chat UI. - Offer helper endpoints (`get_query_configuration_options`, `get_query_columns`, `get_aggregatable_columns`, `get_run_data_query_format`, `get_plot_chart_format`) so agents can self-discover valid parameters. ## Architecture highlights - **Plugin class**: `AzureBillingPlugin` (see `azure_billing_plugin.py`) inherits from `BasePlugin`, exposing annotated `@kernel_function`s for the agent runtime. - **Authentication**: supports user impersonation (via `get_valid_access_token_for_plugins`) and service principals defined in the plugin manifest; automatically selects the right AAD authority per cloud. -- **Data rendering**: CSV assembly uses in-memory writers, while charts are produced with matplotlib, encoded as base64 data URLs, and persisted to Cosmos DB for later retrieval. -- **Sample assets**: `sample_pie.csv`, `sample_stacked_column.csv`, and `my_chart.png` demonstrate expected data formats and outputs for local experimentation. +- **Data rendering**: CSV assembly uses in-memory writers, while charts are returned as validated `simplechart` Markdown blocks rendered by the SimpleChat Chart.js frontend. +- **Sample assets**: `sample_pie.csv` and `sample_stacked_column.csv` demonstrate expected data formats for local experimentation. ## Authentication & configuration 1. Provide a plugin manifest with `endpoint`, `auth` (user or service principal), and optional `metadata/additionalFields` such as `apiVersion` (defaults to `2023-03-01`). @@ -27,26 +27,25 @@ The Azure Billing action is an experimental Semantic Kernel plugin that helps ag 1. **Discover scope**: call `list_subscriptions_and_resourcegroups()` or `list_subscriptions()` followed by `list_resource_groups(subscription_id)`. 2. **Inspect available dimensions**: use `get_query_configuration_options()` plus `get_grouping_dimensions()` to learn valid aggregations and groupings. 3. **Fetch data**: invoke `run_data_query(...)` with explicit `start_datetime`, `end_datetime`, at least one aggregation, and one grouping. The response includes `csv`, column metadata, and `plot_hints`. -4. **Visualize**: immediately pass the returned rows or CSV into `plot_chart(...)`, selecting `x_keys`, `y_keys`, and `graph_type` from `plot_hints`. Include the same `conversation_id` so the base64 PNG is attached to the chat transcript in Cosmos DB. +4. **Visualize**: immediately pass the returned rows or CSV into `plot_chart(...)`, selecting `x_keys`, `y_keys`, and `graph_type` from `plot_hints`. Include the returned `chart_markdown` in the response so the chat UI renders the dynamic chart. 5. **Iterate**: explore budgets with `get_budgets`, monitor alerts via `get_alerts` / `get_specific_alert`, or generate multi-month forecasts through `get_forecast`. ## Charting guidance - Supported graph types: `pie`, `column_stacked`, `column_grouped`, `line`, `area`. -- `plot_chart` is a convenience wrapper that forwards to `plot_custom_chart`; both sanitize figure sizes, wrap long titles, and annotate stacked totals. +- `plot_chart` is a convenience wrapper that forwards to `plot_custom_chart`; both return a validated `chart_payload`, `chart_markdown`, summary, and metadata for inline rendering. - `suggest_plot_config` can analyze arbitrary CSV/rows to recommend labels and numeric fields when the Cost Management query did not originate from this plugin. -## Outputs & persistence +## Outputs & rendering - Tabular results are returned as CSV strings to minimize token usage while keeping schemas explicit. -- Chart payloads include metadata (axes, graph type, figure size) plus a `data:image/png;base64` URL; when `conversation_id` is supplied the image is chunked/stored inside `cosmos_messages_container` with retry-friendly metadata. -- The agent should describe generated charts textually to users; binary content is delivered through the persisted conversation artifacts. +- Chart payloads include metadata (axes, graph type, renderer) plus `chart_markdown` containing a fenced `simplechart` block for SimpleChat's local Chart.js renderer. +- The agent should include generated `chart_markdown` in responses so SimpleChat can replace the fenced block with an interactive chart. ## Limitations & cautions - No throttling, retry, or quota management has been hardened—expect occasional failures from Cost Management when running multiple heavy queries. - Error handling is best-effort: the plugin attempts to normalize enums, dates, and aggregations but may still raise when inputs are malformed. -- Cosmos DB storage assumes the surrounding SimpleChat environment; using the plugin outside that context requires replacing the persistence hooks. +- The dynamic chart renderer assumes the surrounding SimpleChat frontend; using the plugin outside that context requires rendering or exporting the returned `simplechart` block separately. - Security hardening (secret rotation, granular RBAC validation, zero-trust networking) has **not** been completed; do not expose this plugin to production tenants or sensitive billing data without additional review. ## Additional resources - Review `instructions.md` in the same directory for the autonomous agent persona tailored to this action. - Leverage the sample CSV files to validate plotting offline before wiring the plugin into a notebook or agent loop. - diff --git a/application/single_app/app.py b/application/single_app/app.py index f6c993e7e..8e0836d0c 100644 --- a/application/single_app/app.py +++ b/application/single_app/app.py @@ -37,6 +37,7 @@ import threading import time from datetime import datetime +from flask import Blueprint, g from urllib.parse import urlparse from route_frontend_authentication import * @@ -91,7 +92,7 @@ from route_backend_data_management import register_route_backend_data_management from route_backend_msgraph_pending_actions import register_route_backend_msgraph_pending_actions from route_enhanced_citations import register_enhanced_citations_routes -from plugin_validation_endpoint import plugin_validation_bp +from plugin_validation_endpoint import plugin_validation_admin_bp, plugin_validation_bp from route_openapi import register_openapi_routes from route_migration import bp_migration from route_plugin_logging import bpl as plugin_logging_bp @@ -128,31 +129,42 @@ Session(app) + +def register_route_blueprint(name, registrar, auth_guard=None): + """Register a route module through a Blueprint and optional auth policy guard.""" + bp = Blueprint(name, __name__) + if auth_guard: + bp.before_request(auth_guard()) + registrar(bp) + app.register_blueprint(bp) + return bp + + app.register_blueprint(admin_plugins_bp) app.register_blueprint(dynamic_plugins_bp) app.register_blueprint(admin_agents_bp) app.register_blueprint(bp_agent_templates) app.register_blueprint(plugin_validation_bp) +app.register_blueprint(plugin_validation_admin_bp) app.register_blueprint(bp_migration) app.register_blueprint(plugin_logging_bp) # Register OpenAPI routes -register_openapi_routes(app) +register_route_blueprint('openapi', register_openapi_routes, user_required_blueprint) # Register Enhanced Citations routes -register_enhanced_citations_routes(app) +register_route_blueprint('enhanced_citations', register_enhanced_citations_routes, user_required_blueprint) # Register Speech routes -register_route_backend_speech(app) +register_route_blueprint('backend_speech', register_route_backend_speech, user_required_blueprint) # Register TTS routes -register_route_backend_tts(app) +register_route_blueprint('backend_tts', register_route_backend_tts, user_required_blueprint) # Register Swagger documentation routes from swagger_wrapper import register_swagger_routes register_swagger_routes(app) -from flask import g from flask_session import Session from redis import Redis from functions_settings import get_settings @@ -199,17 +211,9 @@ def configure_sessions(settings): try: if redis_auth_type == 'managed_identity': log_event("Redis enabled using Managed Identity", level=logging.INFO) - from config import get_redis_cache_infrastructure_endpoint - credential = DefaultAzureCredential() - redis_hostname = redis_url.split('.')[0] - cache_endpoint = get_redis_cache_infrastructure_endpoint(redis_hostname) - token = credential.get_token(cache_endpoint) - redis_client = Redis( - host=redis_url, - port=6380, - db=0, - password=token.token, - ssl=True, + redis_client = app_settings_cache.create_redis_managed_identity_client( + redis_url, + settings=settings, socket_connect_timeout=5, socket_timeout=5 ) @@ -909,7 +913,7 @@ def enforce_idle_session_timeout(): 'requires_reauth': True }), 401 - return redirect(url_for('local_logout')) + return redirect(url_for('frontend_authentication.local_logout')) except Exception as e: log_event(f"Idle timeout evaluation failed: {e}", level=logging.WARNING) @@ -973,8 +977,11 @@ def nl2br_filter(value): app.jinja_env.filters['nl2br'] = nl2br_filter +public_app_bp = Blueprint('public_app', __name__) + + # =================== Default Routes ===================== -@app.route('/') +@public_app_bp.route('/') @swagger_route(security=get_auth_security()) def index(): settings = get_settings() @@ -988,17 +995,17 @@ def index(): return render_template('index.html', app_settings=public_settings, landing_html=landing_html) -@app.route('/robots933456.txt') +@public_app_bp.route('/robots933456.txt') @swagger_route(security=get_auth_security()) def robots(): return send_from_directory('static', 'robots.txt') -@app.route('/favicon.ico') +@public_app_bp.route('/favicon.ico') @swagger_route(security=get_auth_security()) def favicon(): return send_from_directory('static', 'favicon.ico') -@app.route('/static/js/') +@public_app_bp.route('/static/js/') @swagger_route(security=get_auth_security()) def serve_js_modules(filename): """Serve JavaScript modules with correct MIME type.""" @@ -1011,12 +1018,16 @@ def serve_js_modules(filename): else: return send_from_directory('static/js', filename) -@app.route('/acceptable_use_policy.html') +@public_app_bp.route('/acceptable_use_policy.html') @swagger_route(security=get_auth_security()) def acceptable_use_policy(): return render_template('acceptable_use_policy.html') -@app.route('/api/session/heartbeat', methods=['POST']) +session_api_bp = Blueprint('session_api', __name__) +session_api_bp.before_request(login_required_blueprint()) + + +@session_api_bp.route('/api/session/heartbeat', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required def session_heartbeat(): @@ -1040,8 +1051,14 @@ def session_heartbeat(): 'idle_timeout_minutes': idle_timeout_minutes }), 200 -@app.route('/api/semantic-kernel/plugins') +debug_admin_bp = Blueprint('debug_admin', __name__) +debug_admin_bp.before_request(admin_required_blueprint()) + + +@debug_admin_bp.route('/api/semantic-kernel/plugins') @swagger_route(security=get_auth_security()) +@login_required +@admin_required def list_semantic_kernel_plugins(): """Test endpoint: List loaded Semantic Kernel plugins and their functions.""" global kernel @@ -1053,143 +1070,148 @@ def list_semantic_kernel_plugins(): return {"plugins": plugins} +app.register_blueprint(public_app_bp) +app.register_blueprint(session_api_bp) +app.register_blueprint(debug_admin_bp) + + # =================== Front End Routes =================== # ------------------- User Authentication Routes --------- -register_route_frontend_authentication(app) +register_route_blueprint('frontend_authentication', register_route_frontend_authentication) # ------------------- User Profile Routes ---------------- -register_route_frontend_profile(app) +register_route_blueprint('frontend_profile', register_route_frontend_profile, login_required_blueprint) # ------------------- Admin Settings Routes -------------- -register_route_frontend_admin_settings(app) +register_route_blueprint('frontend_admin_settings', register_route_frontend_admin_settings, admin_required_blueprint) # ------------------- Control Center Routes -------------- -register_route_frontend_control_center(app) +register_route_blueprint('frontend_control_center', register_route_frontend_control_center, login_required_blueprint) # ------------------- Chats Routes ----------------------- -register_route_frontend_chats(app) +register_route_blueprint('frontend_chats', register_route_frontend_chats, user_required_blueprint) # ------------------- Agents Catalog Routes -------------- -register_route_frontend_agents(app) +register_route_blueprint('frontend_agents', register_route_frontend_agents, user_required_blueprint) # ------------------- Conversations Routes --------------- -register_route_frontend_conversations(app) +register_route_blueprint('frontend_conversations', register_route_frontend_conversations, user_required_blueprint) # ------------------- Documents Routes ------------------- -register_route_frontend_workspace(app) +register_route_blueprint('frontend_workspace', register_route_frontend_workspace, user_required_blueprint) # ------------------- Groups Routes ---------------------- -register_route_frontend_groups(app) +register_route_blueprint('frontend_groups', register_route_frontend_groups, user_required_blueprint) # ------------------- Group Documents Routes ------------- -register_route_frontend_group_workspaces(app) -register_route_frontend_public_workspaces(app) +register_route_blueprint('frontend_group_workspaces', register_route_frontend_group_workspaces, user_required_blueprint) +register_route_blueprint('frontend_public_workspaces', register_route_frontend_public_workspaces, user_required_blueprint) # ------------------- Safety Routes ---------------------- -register_route_frontend_safety(app) +register_route_blueprint('frontend_safety', register_route_frontend_safety, login_required_blueprint) # ------------------- Feedback Routes ------------------- -register_route_frontend_feedback(app) +register_route_blueprint('frontend_feedback', register_route_frontend_feedback, login_required_blueprint) # ------------------- Support Routes -------------------- -register_route_frontend_support(app) +register_route_blueprint('frontend_support', register_route_frontend_support, user_required_blueprint) # ------------------- Notifications Routes -------------- -register_route_frontend_notifications(app) +register_route_blueprint('frontend_notifications', register_route_frontend_notifications, user_required_blueprint) # ------------------- Custom Pages Routes --------------- -register_route_custom_pages(app) +register_route_blueprint('custom_pages', register_route_custom_pages, login_required_blueprint) # ------------------- API Chat Routes -------------------- -register_route_backend_chats(app) +register_route_blueprint('backend_chats', register_route_backend_chats, user_required_blueprint) # ------------------- API Search Routes ------------------ -register_route_backend_search(app) +register_route_blueprint('backend_search', register_route_backend_search, user_required_blueprint) # ------------------- API Conversation Routes ------------ -register_route_backend_conversations(app) +register_route_blueprint('backend_conversations', register_route_backend_conversations, user_required_blueprint) # ------------------- API Collaboration Routes ----------- -register_route_backend_collaboration(app) +register_route_blueprint('backend_collaboration', register_route_backend_collaboration, user_required_blueprint) # ------------------- API MS Graph Pending Action Routes - -register_route_backend_msgraph_pending_actions(app) +register_route_blueprint('backend_msgraph_pending_actions', register_route_backend_msgraph_pending_actions, user_required_blueprint) # ------------------- API Documents Routes --------------- -register_route_backend_documents(app) +register_route_blueprint('backend_documents', register_route_backend_documents, user_required_blueprint) # ------------------- API Groups Routes ------------------ -register_route_backend_groups(app) +register_route_blueprint('backend_groups', register_route_backend_groups, user_required_blueprint) # ------------------- API User Routes -------------------- -register_route_backend_users(app) +register_route_blueprint('backend_users', register_route_backend_users, user_required_blueprint) # ------------------- API Group Documents Routes --------- -register_route_backend_group_documents(app) +register_route_blueprint('backend_group_documents', register_route_backend_group_documents, user_required_blueprint) # ------------------- API Model Routes ------------------- -register_route_backend_models(app) +register_route_blueprint('backend_models', register_route_backend_models, user_required_blueprint) # ------------------- API Workflow Routes ---------------- -register_route_backend_workflows(app) +register_route_blueprint('backend_workflows', register_route_backend_workflows, user_required_blueprint) # ------------------- API Safety Logs Routes ------------- -register_route_backend_safety(app) +register_route_blueprint('backend_safety', register_route_backend_safety, user_required_blueprint) # ------------------- API Feedback Routes --------------- -register_route_backend_feedback(app) +register_route_blueprint('backend_feedback', register_route_backend_feedback, user_required_blueprint) # ------------------- API Settings Routes --------------- -register_route_backend_settings(app) +register_route_blueprint('backend_settings', register_route_backend_settings, login_required_blueprint) # ------------------- API Data Management Routes --------- -register_route_backend_data_management(app) +register_route_blueprint('backend_data_management', register_route_backend_data_management, admin_required_blueprint) # ------------------- API Prompts Routes ---------------- -register_route_backend_prompts(app) +register_route_blueprint('backend_prompts', register_route_backend_prompts, user_required_blueprint) # ------------------- API Group Prompts Routes ---------- -register_route_backend_group_prompts(app) +register_route_blueprint('backend_group_prompts', register_route_backend_group_prompts, user_required_blueprint) # ------------------- API Control Center Routes --------- -register_route_backend_control_center(app) +register_route_blueprint('backend_control_center', register_route_backend_control_center, login_required_blueprint) # ------------------- API Notifications Routes ---------- -register_route_backend_notifications(app) +register_route_blueprint('backend_notifications', register_route_backend_notifications, user_required_blueprint) # ------------------- API Retention Policy Routes -------- -register_route_backend_retention_policy(app) +register_route_blueprint('backend_retention_policy', register_route_backend_retention_policy, login_required_blueprint) # ------------------- API Governance Routes -------------- -register_route_backend_governance(app) +register_route_blueprint('backend_governance', register_route_backend_governance, admin_required_blueprint) # ------------------- API Public Workspaces Routes ------- -register_route_backend_public_workspaces(app) +register_route_blueprint('backend_public_workspaces', register_route_backend_public_workspaces, user_required_blueprint) # ------------------- API Conversation Export Routes ----- -register_route_backend_conversation_export(app) +register_route_blueprint('backend_conversation_export', register_route_backend_conversation_export, user_required_blueprint) # ------------------- API Public Documents Routes -------- -register_route_backend_public_documents(app) +register_route_blueprint('backend_public_documents', register_route_backend_public_documents, user_required_blueprint) # ------------------- API Public Prompts Routes ---------- -register_route_backend_public_prompts(app) +register_route_blueprint('backend_public_prompts', register_route_backend_public_prompts, user_required_blueprint) # ------------------- API File Sync Routes --------------- -register_route_backend_file_sync(app) +register_route_blueprint('backend_file_sync', register_route_backend_file_sync, login_required_blueprint) # ------------------- API Workspace Identity Routes ------ -register_route_backend_workspace_identities(app) +register_route_blueprint('backend_workspace_identities', register_route_backend_workspace_identities, login_required_blueprint) # ------------------- API User Agreement Routes ---------- -register_route_backend_user_agreement(app) +register_route_blueprint('backend_user_agreement', register_route_backend_user_agreement, user_required_blueprint) # ------------------- API Thoughts Routes ---------------- -register_route_backend_thoughts(app) +register_route_blueprint('backend_thoughts', register_route_backend_thoughts, user_required_blueprint) # ------------------- External Health Routes ---------- -register_route_external_health(app) -register_no_auth_health(app) +register_route_blueprint('external_health', register_route_external_health) +register_route_blueprint('external_no_auth_health', register_no_auth_health) if __name__ == '__main__': debug_mode = os.environ.get("FLASK_DEBUG", "0") == "1" diff --git a/application/single_app/app_settings_cache.py b/application/single_app/app_settings_cache.py index 5f3f24e4e..90dbc4a62 100644 --- a/application/single_app/app_settings_cache.py +++ b/application/single_app/app_settings_cache.py @@ -7,10 +7,13 @@ import json import logging import copy +import base64 +import os import threading import time from datetime import datetime from redis import Redis +from redis.credentials import CredentialProvider from azure.identity import DefaultAzureCredential # NOTE: functions_keyvault is imported locally inside configure_app_cache to avoid a circular @@ -19,6 +22,8 @@ _settings = None _logger = logging.getLogger(__name__) +REDIS_ENTRA_TOKEN_SCOPE = 'https://redis.azure.com/.default' +REDIS_TOKEN_REFRESH_BUFFER_SECONDS = 300 APP_SETTINGS_CACHE = {} APP_USER_UI_SETTINGS_CACHE = {} APP_STREAM_SESSION_METADATA = {} @@ -55,6 +60,65 @@ _app_cache_lock = threading.Lock() +def _get_redis_entra_token_scope(settings=None): + configured_scope = (settings or {}).get('redis_entra_token_scope') or os.getenv('REDIS_ENTRA_TOKEN_SCOPE') + return (configured_scope or REDIS_ENTRA_TOKEN_SCOPE).strip() + + +def _decode_token_claims(access_token): + parts = access_token.split('.') + if len(parts) < 2: + raise ValueError('Redis Microsoft Entra token did not contain JWT claims.') + + payload = parts[1] + payload += '=' * (-len(payload) % 4) + decoded_payload = base64.urlsafe_b64decode(payload.encode('utf-8')).decode('utf-8') + return json.loads(decoded_payload) + + +def _get_redis_username_from_claims(access_token): + claims = _decode_token_claims(access_token) + username = claims.get('oid') or claims.get('appid') + if not username: + raise ValueError('Redis Microsoft Entra token did not include an object ID claim.') + return username + + +class RedisManagedIdentityCredentialProvider(CredentialProvider): + """Provides Redis ACL username and Microsoft Entra token credentials.""" + + def __init__(self, credential=None, scope=None): + self.credential = credential or DefaultAzureCredential() + self.scope = scope or REDIS_ENTRA_TOKEN_SCOPE + self._cached_credentials = None + self._expires_on = 0 + + def get_credentials(self): + now = time.time() + if self._cached_credentials and now < self._expires_on - REDIS_TOKEN_REFRESH_BUFFER_SECONDS: + return self._cached_credentials + + token = self.credential.get_token(self.scope) + username = _get_redis_username_from_claims(token.token) + self._cached_credentials = (username, token.token) + self._expires_on = token.expires_on + return self._cached_credentials + + +def create_redis_managed_identity_client(redis_url, settings=None, **redis_kwargs): + credential_provider = RedisManagedIdentityCredentialProvider( + scope=_get_redis_entra_token_scope(settings) + ) + return Redis( + host=redis_url, + port=6380, + db=0, + credential_provider=credential_provider, + ssl=True, + **redis_kwargs + ) + + def _get_expiration_timestamp(ttl_seconds=None): if ttl_seconds is None: return None @@ -147,15 +211,9 @@ def configure_app_cache(settings, redis_cache_endpoint=None): redis_auth_type = settings.get('redis_auth_type', 'key').strip().lower() if redis_auth_type == 'managed_identity': log_event("[ASC] Redis enabled using Managed Identity", level=logging.INFO) - credential = DefaultAzureCredential() - cache_endpoint = redis_cache_endpoint - token = credential.get_token(cache_endpoint) - redis_client = Redis( - host=redis_url, - port=6380, - db=0, - password=token.token, - ssl=True + redis_client = create_redis_managed_identity_client( + redis_url, + settings=settings ) elif redis_auth_type == 'key_vault': log_event("[ASC] Redis enabled using Key Vault Secret", level=logging.INFO) diff --git a/application/single_app/config.py b/application/single_app/config.py index d12dae5f0..b242f9321 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -95,7 +95,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.250.001" +VERSION = "0.250.004" SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY', 'true').lower() != 'false' @@ -104,6 +104,7 @@ 'CSRF_ENFORCE_ORIGIN_FOR_UNSAFE_METHODS', 'true' ).lower() != 'false' + def _split_origin_list(raw_value): """Return trimmed origins from comma, space, or JSON-list environment values.""" if not raw_value: @@ -313,6 +314,7 @@ def get_allowed_extensions(enable_video=False, enable_audio=False): if TEAMS_FRAME_ANCESTORS: FRAME_ANCESTORS_DIRECTIVE = f"{FRAME_ANCESTORS_DIRECTIVE} {' '.join(TEAMS_FRAME_ANCESTORS)}" + # Security Headers Configuration SECURITY_HEADERS = { 'X-Content-Type-Options': 'nosniff', diff --git a/application/single_app/functions_authentication.py b/application/single_app/functions_authentication.py index 86abdc12c..e79ce7337 100644 --- a/application/single_app/functions_authentication.py +++ b/application/single_app/functions_authentication.py @@ -13,6 +13,51 @@ REQUESTED_SCOPES_SESSION_KEY = "requested_oauth_scopes" #REDIRECT_PATH = getattr(globals(), 'REDIRECT_PATH', '/.auth/login/aad/callback') + +def apply_blueprint_auth(*decorators): + """Return a Blueprint before_request guard composed from existing auth decorators.""" + decorator_names = tuple( + decorator if isinstance(decorator, str) else getattr(decorator, '__name__', str(decorator)) + for decorator in decorators + ) + + def guard(): + def authorized_request(): + return None + + protected_request = authorized_request + resolved_decorators = [ + globals()[decorator] if isinstance(decorator, str) else decorator + for decorator in decorators + ] + for decorator in reversed(resolved_decorators): + protected_request = decorator(protected_request) + + return protected_request() + + guard._simplechat_auth_policy = decorator_names + return guard + + +def login_required_blueprint(): + """Return a Blueprint before_request guard that requires an authenticated session.""" + return apply_blueprint_auth('login_required') + + +def user_required_blueprint(): + """Return a Blueprint before_request guard that requires authenticated User/Admin access.""" + return apply_blueprint_auth('login_required', 'user_required') + + +def admin_required_blueprint(): + """Return a Blueprint before_request guard that requires authenticated Admin access.""" + return apply_blueprint_auth('login_required', 'admin_required') + + +def external_api_required_blueprint(): + """Return a Blueprint before_request guard that requires an ExternalApi bearer token.""" + return apply_blueprint_auth('accesstoken_required') + def build_front_door_urls(front_door_url): """ Build home and login redirect URLs from a Front Door base URL. @@ -689,7 +734,7 @@ def decorated_function(*args, **kwargs): # Fall back to environment variable if Front Door is enabled but no URL is set return redirect(LOGIN_REDIRECT_URL) - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) return f(*args, **kwargs) return decorated_function diff --git a/application/single_app/functions_custom_pages.py b/application/single_app/functions_custom_pages.py index fad7e96f3..7dc94af2f 100644 --- a/application/single_app/functions_custom_pages.py +++ b/application/single_app/functions_custom_pages.py @@ -293,7 +293,7 @@ def get_custom_pages_nav(settings: Dict[str, Any]) -> List[Dict[str, Any]]: if not is_custom_page_authorized(page, roles): continue if has_app_context(): - href = url_for("custom_page", slug=page["slug"]) + href = url_for("custom_pages.custom_page", slug=page["slug"]) else: href = f"/custom/{page['slug']}" nav_items.append({ diff --git a/application/single_app/functions_model_endpoint_runtime.py b/application/single_app/functions_model_endpoint_runtime.py index 9660d2683..b1cf5449f 100644 --- a/application/single_app/functions_model_endpoint_runtime.py +++ b/application/single_app/functions_model_endpoint_runtime.py @@ -7,6 +7,7 @@ from config import cognitive_services_scope from foundry_agent_runtime import resolve_authority +from functions_settings import resolve_model_endpoint_foundry_scope from model_endpoint_clients import ( MODEL_ENDPOINT_PROTOCOL_ANTHROPIC, MODEL_ENDPOINT_PROTOCOL_AZURE_OPENAI, @@ -88,28 +89,7 @@ def build_model_endpoint_context( def resolve_foundry_scope_for_endpoint_auth(auth_settings, endpoint=None): """Resolve the correct scope for Foundry-backed inference authentication.""" - auth_settings = auth_settings or {} - custom_scope = str(auth_settings.get('foundry_scope') or '').strip() - if custom_scope: - return custom_scope - - management_cloud = str(auth_settings.get('management_cloud') or 'public').lower() - if management_cloud in ('government', 'usgovernment', 'usgov'): - return 'https://ai.azure.us/.default' - if management_cloud == 'china': - return 'https://ai.azure.cn/.default' - if management_cloud == 'germany': - return 'https://ai.azure.de/.default' - - endpoint_value = str(endpoint or '').lower() - if 'azure.us' in endpoint_value: - return 'https://ai.azure.us/.default' - if 'azure.cn' in endpoint_value: - return 'https://ai.azure.cn/.default' - if 'azure.de' in endpoint_value: - return 'https://ai.azure.de/.default' - - return 'https://ai.azure.com/.default' + return resolve_model_endpoint_foundry_scope(auth_settings, endpoint=endpoint) def resolve_credential_for_model_endpoint_auth(auth_settings): diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index 31c78aa9a..59a73c33a 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -1704,6 +1704,116 @@ def apply_custom_endpoint_setting_migration(settings_item): return updated + +def normalize_model_endpoint_management_cloud(value): + """Return the canonical management cloud value for model endpoint auth.""" + normalized_value = str(value or "").strip().lower() + if normalized_value in {"government", "usgovernment", "usgov", "gov", "gcc"}: + return "government" + if normalized_value == "custom": + return "custom" + return "public" + + +def get_model_endpoint_management_cloud_for_environment(environment=None): + """Resolve the app environment into the model endpoint management cloud.""" + environment_value = AZURE_ENVIRONMENT if environment is None else environment + return normalize_model_endpoint_management_cloud(environment_value) + + +def is_model_endpoint_management_cloud_user_editable(endpoint_provider, endpoint_auth_type): + """Return True when the UI intentionally lets users choose endpoint cloud.""" + provider = str(endpoint_provider or "").strip().lower() + auth_type = str(endpoint_auth_type or "").strip().lower() + return provider in {"aifoundry", "new_foundry"} and auth_type == "service_principal" + + +def get_model_endpoint_default_custom_authority(): + """Return the app-level authority used when model endpoints inherit custom cloud.""" + if get_model_endpoint_management_cloud_for_environment() != "custom": + return "" + return str(authority or "").strip() + + +def get_model_endpoint_default_foundry_scope(): + """Return the app-level Foundry token scope for endpoint auth defaults.""" + management_cloud = get_model_endpoint_management_cloud_for_environment() + if management_cloud == "government": + return "https://ai.azure.us/.default" + if management_cloud == "custom": + return str(cognitive_services_scope or "").strip() + return "https://ai.azure.com/.default" + + +def resolve_model_endpoint_foundry_scope(auth_settings, endpoint=None): + """Resolve the Foundry token scope for a saved model endpoint auth payload.""" + auth_settings = auth_settings or {} + custom_scope = str(auth_settings.get("foundry_scope") or "").strip() + if custom_scope: + return custom_scope + + management_cloud = normalize_model_endpoint_management_cloud(auth_settings.get("management_cloud")) + if management_cloud == "government": + return "https://ai.azure.us/.default" + if management_cloud == "custom": + default_scope = get_model_endpoint_default_foundry_scope() + if default_scope: + return default_scope + raise ValueError("Foundry scope is required for custom cloud model endpoint authentication.") + + endpoint_value = str(endpoint or "").lower() + if "azure.us" in endpoint_value: + return "https://ai.azure.us/.default" + if "azure.cn" in endpoint_value: + return "https://ai.azure.cn/.default" + if "azure.de" in endpoint_value: + return "https://ai.azure.de/.default" + + return "https://ai.azure.com/.default" + + +def normalize_model_endpoint_auth_for_environment(endpoint_copy): + """Normalize endpoint auth cloud fields that are owned by app environment.""" + if not isinstance(endpoint_copy, dict): + return False + + changed = False + auth = endpoint_copy.get("auth") + if not isinstance(auth, dict): + auth = {} + endpoint_copy["auth"] = auth + changed = True + + provider = str(endpoint_copy.get("provider") or "").strip().lower() + auth_type = str(auth.get("type") or "").strip().lower() + current_cloud = normalize_model_endpoint_management_cloud(auth.get("management_cloud")) + default_cloud = get_model_endpoint_management_cloud_for_environment() + cloud_user_editable = is_model_endpoint_management_cloud_user_editable(provider, auth_type) + + if not cloud_user_editable or not auth.get("management_cloud"): + if auth.get("management_cloud") != default_cloud: + auth["management_cloud"] = default_cloud + changed = True + current_cloud = default_cloud + elif auth.get("management_cloud") != current_cloud: + auth["management_cloud"] = current_cloud + changed = True + + if current_cloud == "custom" and not cloud_user_editable: + custom_authority = get_model_endpoint_default_custom_authority() + if custom_authority and auth.get("custom_authority") != custom_authority: + auth["custom_authority"] = custom_authority + changed = True + + if provider in {"aifoundry", "new_foundry", "anthropic", "claude"}: + foundry_scope = get_model_endpoint_default_foundry_scope() + if foundry_scope and auth.get("foundry_scope") != foundry_scope: + auth["foundry_scope"] = foundry_scope + changed = True + + return changed + + def normalize_model_endpoints(endpoints): """Normalize model endpoints with stable IDs and enabled flags.""" if not isinstance(endpoints, list): @@ -1730,6 +1840,9 @@ def normalize_model_endpoints(endpoints): endpoint_copy["enabled"] = True changed = True + if normalize_model_endpoint_auth_for_environment(endpoint_copy): + changed = True + models = endpoint_copy.get("models") or [] normalized_models = [] for model in models: diff --git a/application/single_app/functions_workflow_runner.py b/application/single_app/functions_workflow_runner.py index e4c570c15..aff2f3848 100644 --- a/application/single_app/functions_workflow_runner.py +++ b/application/single_app/functions_workflow_runner.py @@ -88,7 +88,7 @@ from functions_search_service import resolve_document_context, search_documents from functions_search import normalize_search_id_list, normalize_search_scope, normalize_search_top_n from functions_simplechat_operations import upload_generated_analysis_artifact_for_current_user -from functions_settings import get_settings, get_user_settings, is_tabular_processing_enabled, normalize_model_endpoints +from functions_settings import get_settings, get_user_settings, is_tabular_processing_enabled, normalize_model_endpoints, resolve_model_endpoint_foundry_scope from functions_source_review import ( URL_ACCESS_CONTEXT_WORKFLOW, compact_source_review_result_for_metadata, @@ -3170,26 +3170,7 @@ def _resolve_authority(auth_settings): def _resolve_foundry_scope(auth_settings, endpoint=None): - custom_scope = (auth_settings.get('foundry_scope') or '').strip() - if custom_scope: - return custom_scope - - management_cloud = (auth_settings.get('management_cloud') or 'public').lower() - if management_cloud in ('government', 'usgovernment', 'usgov'): - return 'https://ai.azure.us/.default' - if management_cloud == 'china': - return 'https://ai.azure.cn/.default' - if management_cloud == 'germany': - return 'https://ai.azure.de/.default' - - endpoint_value = (endpoint or '').lower() - if 'azure.us' in endpoint_value: - return 'https://ai.azure.us/.default' - if 'azure.cn' in endpoint_value: - return 'https://ai.azure.cn/.default' - if 'azure.de' in endpoint_value: - return 'https://ai.azure.de/.default' - return 'https://ai.azure.com/.default' + return resolve_model_endpoint_foundry_scope(auth_settings, endpoint=endpoint) def _build_workflow_credential(auth_settings): diff --git a/application/single_app/plugin_validation_endpoint.py b/application/single_app/plugin_validation_endpoint.py index 10460fe58..92031b939 100644 --- a/application/single_app/plugin_validation_endpoint.py +++ b/application/single_app/plugin_validation_endpoint.py @@ -8,7 +8,7 @@ from flask import Blueprint, current_app, jsonify, request from functions_appinsights import log_event -from functions_authentication import admin_required, login_required, user_required +from functions_authentication import admin_required, admin_required_blueprint, login_required, user_required, user_required_blueprint from json_schema_validation import apply_plugin_validation_defaults from semantic_kernel_plugins.plugin_health_checker import PluginErrorRecovery, PluginHealthChecker from semantic_kernel_plugins.plugin_loader import discover_plugins @@ -16,6 +16,9 @@ plugin_validation_bp = Blueprint('plugin_validation', __name__) +plugin_validation_admin_bp = Blueprint('plugin_validation_admin', __name__) +plugin_validation_bp.before_request(user_required_blueprint()) +plugin_validation_admin_bp.before_request(admin_required_blueprint()) def _validate_plugin_manifest_request(): @@ -70,7 +73,7 @@ def validate_plugin_manifest(): return jsonify({'error': f'Validation failed: {str(e)}'}), 500 -@plugin_validation_bp.route('/api/admin/plugins/validate', methods=['POST']) +@plugin_validation_admin_bp.route('/api/admin/plugins/validate', methods=['POST']) @swagger_route( security=get_auth_security() ) @@ -87,7 +90,7 @@ def validate_plugin_manifest_admin(): return jsonify({'error': f'Validation failed: {str(e)}'}), 500 -@plugin_validation_bp.route('/api/admin/plugins/test-instantiation', methods=['POST']) +@plugin_validation_admin_bp.route('/api/admin/plugins/test-instantiation', methods=['POST']) @swagger_route( security=get_auth_security() ) @@ -160,7 +163,7 @@ def normalize(s): return jsonify({'error': f'Test failed: {str(e)}'}), 500 -@plugin_validation_bp.route('/api/admin/plugins/health-check/', methods=['GET']) +@plugin_validation_admin_bp.route('/api/admin/plugins/health-check/', methods=['GET']) @swagger_route( security=get_auth_security() ) @@ -238,7 +241,7 @@ def normalize(s): }), 500 -@plugin_validation_bp.route('/api/admin/plugins/repair/', methods=['POST']) +@plugin_validation_admin_bp.route('/api/admin/plugins/repair/', methods=['POST']) @swagger_route( security=get_auth_security() ) diff --git a/application/single_app/requirements.txt b/application/single_app/requirements.txt index 7f955285a..e12214a03 100644 --- a/application/single_app/requirements.txt +++ b/application/single_app/requirements.txt @@ -47,7 +47,7 @@ pillow==12.2.0 ffmpeg-binaries-compat==1.0.1 ffmpeg-python==0.2.0 semantic-kernel==1.39.4 -snowflake-connector-python[pandas]==3.14.0 +snowflake-connector-python[pandas]==3.18.0 tableauserverclient==0.40 protobuf==6.33.5 redis==5.3.1 diff --git a/application/single_app/route_backend_agent_templates.py b/application/single_app/route_backend_agent_templates.py index 0800eb113..6e622c73c 100644 --- a/application/single_app/route_backend_agent_templates.py +++ b/application/single_app/route_backend_agent_templates.py @@ -6,6 +6,7 @@ from functions_authentication import ( admin_required, + login_required_blueprint, login_required, user_required, get_current_user_info, @@ -24,6 +25,7 @@ from functions_settings import get_settings bp_agent_templates = Blueprint('agent_templates', __name__) +bp_agent_templates.before_request(login_required_blueprint()) def _feature_flags(): diff --git a/application/single_app/route_backend_agents.py b/application/single_app/route_backend_agents.py index 3b35c46cd..a8f7bdf93 100644 --- a/application/single_app/route_backend_agents.py +++ b/application/single_app/route_backend_agents.py @@ -59,6 +59,7 @@ from functions_governance import ensure_governance_access, upsert_item_policy bpa = Blueprint('admin_agents', __name__) +bpa.before_request(login_required_blueprint()) AGENT_INSTRUCTION_FIELD_LIMIT = 6000 AGENT_INSTRUCTION_OUTPUT_TOKEN_LIMIT = 1400 @@ -2000,4 +2001,4 @@ def get_global_agent_settings(include_admin_extras=False, user_id=None, group_id "enable_multi_model_endpoints": effective_multi_flag, "model_endpoints": combined_endpoints, }) - \ No newline at end of file + diff --git a/application/single_app/route_backend_chats.py b/application/single_app/route_backend_chats.py index e41a41951..eecc57632 100644 --- a/application/single_app/route_backend_chats.py +++ b/application/single_app/route_backend_chats.py @@ -10583,28 +10583,7 @@ def record_and_publish_tabular_progress_thought(thought_payload): def resolve_foundry_scope_for_auth(auth_settings, endpoint=None): """Resolve the correct scope for Foundry-backed inference authentication.""" - auth_settings = auth_settings or {} - custom_scope = str(auth_settings.get('foundry_scope') or '').strip() - if custom_scope: - return custom_scope - - management_cloud = str(auth_settings.get('management_cloud') or 'public').lower() - if management_cloud in ('government', 'usgovernment', 'usgov'): - return 'https://ai.azure.us/.default' - if management_cloud == 'china': - return 'https://ai.azure.cn/.default' - if management_cloud == 'germany': - return 'https://ai.azure.de/.default' - - endpoint_value = str(endpoint or '').lower() - if 'azure.us' in endpoint_value: - return 'https://ai.azure.us/.default' - if 'azure.cn' in endpoint_value: - return 'https://ai.azure.cn/.default' - if 'azure.de' in endpoint_value: - return 'https://ai.azure.de/.default' - - return 'https://ai.azure.com/.default' + return resolve_model_endpoint_foundry_scope(auth_settings, endpoint=endpoint) def get_foundry_api_version_candidates(primary_version, settings): @@ -10947,7 +10926,7 @@ def restore_agent_stream_retry_state(agent, retry_state): settings.function_choice_behavior = original_behavior -def register_route_backend_chats(app): +def register_route_backend_chats(bp): def build_background_stream_response(event_generator_factory, stream_session=None): """Run SSE generation in background execution so it survives disconnects.""" stream_bridge = BackgroundStreamBridge(stream_session=stream_session) @@ -12259,7 +12238,7 @@ def execute_analyze_chat_request(data=None, publish_background_event=None): forced_action_type=DOCUMENT_ACTION_TYPE_ANALYZE, ) - @app.route('/api/chat/document-action', methods=['POST']) + @bp.route('/api/chat/document-action', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -12267,7 +12246,7 @@ def chat_document_action_api(): payload, status_code = execute_document_action_chat_request() return jsonify(payload), status_code - @app.route('/api/chat/document-action/stream', methods=['POST']) + @bp.route('/api/chat/document-action/stream', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -12320,7 +12299,7 @@ def generate_document_action_response(publish_background_event=None): return build_background_stream_response(generate_document_action_response, stream_session=stream_session) - @app.route('/api/chat/analyze', methods=['POST']) + @bp.route('/api/chat/analyze', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -12328,7 +12307,7 @@ def chat_analyze_api(): payload, status_code = execute_analyze_chat_request() return jsonify(payload), status_code - @app.route('/api/chat/analyze/stream', methods=['POST']) + @bp.route('/api/chat/analyze/stream', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -12381,7 +12360,7 @@ def generate_analyze_response(publish_background_event=None): return build_background_stream_response(generate_analyze_response, stream_session=stream_session) - @app.route('/api/chat/image-proposals/generate', methods=['POST']) + @bp.route('/api/chat/image-proposals/generate', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -12485,7 +12464,7 @@ def generate_image_from_proposal(): ) return jsonify({'error': error_message}), status_code - @app.route('/api/chat', methods=['POST']) + @bp.route('/api/chat', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -15942,10 +15921,10 @@ def gpt_error(e): ) return jsonify({ 'error': f'Internal server error: {str(e)}', - 'details': error_traceback if app.debug else None + 'details': error_traceback if current_app.debug else None }), 500 - @app.route('/api/chat/stream', methods=['POST']) + @bp.route('/api/chat/stream', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19090,7 +19069,7 @@ def finalize_cancelled_agent_stream_response(): return build_background_stream_response(generate, stream_session=stream_session) - @app.route('/api/chat/stream/cancel/', methods=['POST']) + @bp.route('/api/chat/stream/cancel/', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19113,7 +19092,7 @@ def chat_stream_cancel_api(conversation_id): **stream_status, }) - @app.route('/api/chat/stream/status/', methods=['GET']) + @bp.route('/api/chat/stream/status/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19128,7 +19107,7 @@ def chat_stream_status_api(conversation_id): stream_status['conversation_id'] = conversation_id return jsonify(stream_status) - @app.route('/api/tabular/generated-output/runs/', methods=['GET']) + @bp.route('/api/tabular/generated-output/runs/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19143,7 +19122,7 @@ def tabular_generated_output_run_status_api(run_id): return jsonify({'error': 'Tabular generated-output run not found'}), 404 return jsonify({'success': True, 'run': run_status}) - @app.route('/api/tabular/generated-output/runs//resume', methods=['POST']) + @bp.route('/api/tabular/generated-output/runs//resume', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19160,7 +19139,7 @@ def tabular_generated_output_run_resume_api(run_id): return jsonify(resume_result), 409 return jsonify(resume_result) - @app.route('/api/chat/stream/reattach/', methods=['GET']) + @bp.route('/api/chat/stream/reattach/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19225,7 +19204,7 @@ def consume_reattach_stream(): } ) - @app.route('/api/chat/stream/client-event', methods=['POST']) + @bp.route('/api/chat/stream/client-event', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19271,7 +19250,7 @@ def chat_stream_client_event_api(): return jsonify({'success': True, 'event_type': event_type}) - @app.route('/api/message//mask', methods=['POST']) + @bp.route('/api/message//mask', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -19385,7 +19364,7 @@ def mask_message_api(message_id): debug_print(f"[MASK API ERROR] Full traceback:\n{error_traceback}") return jsonify({ 'error': f'Internal server error: {str(e)}', - 'details': error_traceback if app.debug else None + 'details': error_traceback if current_app.debug else None }), 500 diff --git a/application/single_app/route_backend_collaboration.py b/application/single_app/route_backend_collaboration.py index 20e703742..78a09a48a 100644 --- a/application/single_app/route_backend_collaboration.py +++ b/application/single_app/route_backend_collaboration.py @@ -385,8 +385,8 @@ def _sync_collaboration_mask_metadata_to_source(message_doc): source_container.upsert_item(source_message_doc) -def register_route_backend_collaboration(app): - @app.route('/api/collaboration/conversations', methods=['GET']) +def register_route_backend_collaboration(bp): + @bp.route('/api/collaboration/conversations', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -436,7 +436,7 @@ def list_collaboration_conversations_api(): ) return jsonify({'error': 'Failed to load collaborative conversations'}), 500 - @app.route('/api/collaboration/conversations', methods=['POST']) + @bp.route('/api/collaboration/conversations', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -544,7 +544,7 @@ def create_collaboration_conversation_api(): ) return jsonify({'error': 'Failed to create collaborative conversation'}), 500 - @app.route('/api/collaboration/conversations/', methods=['GET']) + @bp.route('/api/collaboration/conversations/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -579,7 +579,7 @@ def get_collaboration_conversation_api(conversation_id): ) return jsonify({'error': 'Failed to load collaborative conversation'}), 500 - @app.route('/api/collaboration/conversations//invite-response', methods=['POST']) + @bp.route('/api/collaboration/conversations//invite-response', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -630,7 +630,7 @@ def respond_to_collaboration_invite_api(conversation_id): ) return jsonify({'error': 'Failed to update invite response'}), 500 - @app.route('/api/collaboration/conversations//members', methods=['POST']) + @bp.route('/api/collaboration/conversations//members', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -692,7 +692,7 @@ def invite_collaboration_members_api(conversation_id): ) return jsonify({'error': 'Failed to invite collaborative conversation members'}), 500 - @app.route('/api/collaboration/conversations/from-personal//members', methods=['POST']) + @bp.route('/api/collaboration/conversations/from-personal//members', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -771,7 +771,7 @@ def convert_personal_conversation_to_collaboration_api(conversation_id): ) return jsonify({'error': 'Failed to convert conversation to collaborative conversation'}), 500 - @app.route('/api/collaboration/conversations/from-group//members', methods=['POST']) + @bp.route('/api/collaboration/conversations/from-group//members', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -853,7 +853,7 @@ def convert_group_conversation_to_collaboration_api(conversation_id): ) return jsonify({'error': 'Failed to convert group conversation to collaborative conversation'}), 500 - @app.route('/api/collaboration/conversations//members/', methods=['DELETE']) + @bp.route('/api/collaboration/conversations//members/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -899,7 +899,7 @@ def remove_collaboration_member_api(conversation_id, member_user_id): ) return jsonify({'error': 'Failed to remove collaborative conversation member'}), 500 - @app.route('/api/collaboration/conversations//members//role', methods=['PUT']) + @bp.route('/api/collaboration/conversations//members//role', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -952,7 +952,7 @@ def update_collaboration_member_role_api(conversation_id, member_user_id): ) return jsonify({'error': 'Failed to update collaborative conversation role'}), 500 - @app.route('/api/collaboration/conversations/', methods=['PUT']) + @bp.route('/api/collaboration/conversations/', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1001,7 +1001,7 @@ def update_collaboration_conversation_title_api(conversation_id): ) return jsonify({'error': 'Failed to update collaborative conversation'}), 500 - @app.route('/api/collaboration/conversations//pin', methods=['POST']) + @bp.route('/api/collaboration/conversations//pin', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1026,7 +1026,7 @@ def toggle_collaboration_conversation_pin_api(conversation_id): ) return jsonify({'error': 'Failed to toggle collaborative pin status'}), 500 - @app.route('/api/collaboration/conversations//hide', methods=['POST']) + @bp.route('/api/collaboration/conversations//hide', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1051,7 +1051,7 @@ def toggle_collaboration_conversation_hide_api(conversation_id): ) return jsonify({'error': 'Failed to toggle collaborative hide status'}), 500 - @app.route('/api/collaboration/conversations//delete-action', methods=['POST']) + @bp.route('/api/collaboration/conversations//delete-action', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1133,7 +1133,7 @@ def collaboration_delete_action_api(conversation_id): ) return jsonify({'error': 'Failed to update collaborative conversation membership'}), 500 - @app.route('/api/collaboration/conversations//messages', methods=['GET']) + @bp.route('/api/collaboration/conversations//messages', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1164,7 +1164,7 @@ def get_collaboration_messages_api(conversation_id): ) return jsonify({'error': 'Failed to load collaborative conversation messages'}), 500 - @app.route('/api/collaboration/conversations//messages//mask', methods=['POST']) + @bp.route('/api/collaboration/conversations//messages//mask', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1243,7 +1243,7 @@ def mask_collaboration_message_api(conversation_id, message_id): ) return jsonify({'error': 'Failed to update shared message mask state'}), 500 - @app.route('/api/collaboration/conversations//images/', methods=['GET']) + @bp.route('/api/collaboration/conversations//images/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1306,7 +1306,7 @@ def get_collaboration_image_api(conversation_id, message_id): ) return jsonify({'error': 'Failed to load collaborative image'}), 500 - @app.route('/api/collaboration/conversations//messages', methods=['POST']) + @bp.route('/api/collaboration/conversations//messages', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1366,7 +1366,7 @@ def post_collaboration_message_api(conversation_id): ) return jsonify({'error': 'Failed to post collaborative conversation message'}), 500 - @app.route('/api/collaboration/conversations//stream', methods=['POST']) + @bp.route('/api/collaboration/conversations//stream', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1676,7 +1676,7 @@ def transform_event_block(event_block): ) return jsonify({'error': 'Failed to start collaborative AI workflow'}), 500 - @app.route('/api/collaboration/conversations//stream/cancel', methods=['POST']) + @bp.route('/api/collaboration/conversations//stream/cancel', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1726,7 +1726,7 @@ def cancel_collaboration_message_stream_api(conversation_id): ) return jsonify({'error': 'Failed to cancel collaborative AI stream'}), 500 - @app.route('/api/collaboration/conversations//messages/', methods=['DELETE']) + @bp.route('/api/collaboration/conversations//messages/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1785,7 +1785,7 @@ def delete_collaboration_message_api(conversation_id, message_id): ) return jsonify({'error': 'Failed to delete collaborative conversation message'}), 500 - @app.route('/api/collaboration/conversations//mark-read', methods=['POST']) + @bp.route('/api/collaboration/conversations//mark-read', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1824,7 +1824,7 @@ def mark_collaboration_conversation_read_api(conversation_id): ) return jsonify({'error': 'Failed to mark collaborative conversation read'}), 500 - @app.route('/api/collaboration/conversations//typing', methods=['POST']) + @bp.route('/api/collaboration/conversations//typing', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1866,7 +1866,7 @@ def collaboration_typing_api(conversation_id): ) return jsonify({'error': 'Failed to publish typing event'}), 500 - @app.route('/api/collaboration/conversations//events', methods=['GET']) + @bp.route('/api/collaboration/conversations//events', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1905,4 +1905,4 @@ def collaboration_events_api(conversation_id): level=logging.ERROR, exceptionTraceback=True, ) - return jsonify({'error': 'Failed to attach collaborative event stream'}), 500 \ No newline at end of file + return jsonify({'error': 'Failed to attach collaborative event stream'}), 500 diff --git a/application/single_app/route_backend_control_center.py b/application/single_app/route_backend_control_center.py index 931308d9b..40dba76b6 100644 --- a/application/single_app/route_backend_control_center.py +++ b/application/single_app/route_backend_control_center.py @@ -2709,10 +2709,10 @@ def get_document_storage_size(doc, cosmos_container, container_name, folder_pref return {} -def register_route_backend_control_center(app): +def register_route_backend_control_center(bp): # User Management APIs - @app.route('/api/admin/control-center/users', methods=['GET']) + @bp.route('/api/admin/control-center/users', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -2820,7 +2820,7 @@ def api_get_all_users(): debug_print(f"Error getting users: {e}") return jsonify({'error': 'Failed to retrieve users'}), 500 - @app.route('/api/admin/control-center/users//access', methods=['PATCH']) + @bp.route('/api/admin/control-center/users//access', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -2876,7 +2876,7 @@ def api_update_user_access(user_id): debug_print(f"Error updating user access: {e}") return jsonify({'error': 'Failed to update user access'}), 500 - @app.route('/api/admin/control-center/users//file-uploads', methods=['PATCH']) + @bp.route('/api/admin/control-center/users//file-uploads', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -2932,7 +2932,7 @@ def api_update_user_file_uploads(user_id): debug_print(f"Error updating user file uploads: {e}") return jsonify({'error': 'Failed to update user file upload permissions'}), 500 - @app.route('/api/admin/control-center/users//delete-documents', methods=['POST']) + @bp.route('/api/admin/control-center/users//delete-documents', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3006,7 +3006,7 @@ def api_delete_user_documents_admin(user_id): }) return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/users/bulk-action', methods=['POST']) + @bp.route('/api/admin/control-center/users/bulk-action', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3092,7 +3092,7 @@ def api_bulk_user_action(): return jsonify({'error': 'Failed to perform bulk action'}), 500 # Group Management APIs - @app.route('/api/admin/control-center/groups', methods=['GET']) + @bp.route('/api/admin/control-center/groups', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3205,7 +3205,7 @@ def api_get_all_groups(): debug_print(f"Error getting groups: {e}") return jsonify({'error': 'Failed to retrieve groups'}), 500 - @app.route('/api/admin/control-center/groups//status', methods=['PUT']) + @bp.route('/api/admin/control-center/groups//status', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3304,7 +3304,7 @@ def api_update_group_status(group_id): debug_print(f"Error updating group status: {e}") return jsonify({'error': 'Failed to update group status'}), 500 - @app.route('/api/admin/control-center/groups/', methods=['GET']) + @bp.route('/api/admin/control-center/groups/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3329,7 +3329,7 @@ def api_get_group_details_admin(group_id): debug_print(f"Error getting group details: {e}") return jsonify({'error': 'Failed to retrieve group details'}), 500 - @app.route('/api/admin/control-center/groups/', methods=['DELETE']) + @bp.route('/api/admin/control-center/groups/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3394,7 +3394,7 @@ def api_delete_group_admin(group_id): debug_print(f"Error creating group deletion request: {e}") return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/groups//delete-documents', methods=['POST']) + @bp.route('/api/admin/control-center/groups//delete-documents', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3457,7 +3457,7 @@ def api_delete_group_documents_admin(group_id): debug_print(f"Error creating document deletion request: {e}") return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/groups//members', methods=['GET']) + @bp.route('/api/admin/control-center/groups//members', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3491,7 +3491,7 @@ def api_get_group_members_admin(group_id): debug_print(f"Error getting group members: {e}") return jsonify({'error': 'Failed to retrieve group members'}), 500 - @app.route('/api/admin/control-center/groups//take-ownership', methods=['POST']) + @bp.route('/api/admin/control-center/groups//take-ownership', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3559,7 +3559,7 @@ def api_admin_take_group_ownership(group_id): debug_print(f"Error creating take ownership request: {e}") return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/groups//transfer-ownership', methods=['POST']) + @bp.route('/api/admin/control-center/groups//transfer-ownership', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3642,7 +3642,7 @@ def api_admin_transfer_group_ownership(group_id): debug_print(f"Error creating transfer ownership request: {e}") return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/groups//add-member', methods=['POST']) + @bp.route('/api/admin/control-center/groups//add-member', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3749,7 +3749,7 @@ def api_admin_add_group_member(group_id): debug_print(f"Error adding group member: {e}") return jsonify({'error': 'Failed to add member'}), 500 - @app.route('/api/admin/control-center/groups//activity', methods=['GET']) + @bp.route('/api/admin/control-center/groups//activity', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -3974,7 +3974,7 @@ def api_admin_get_group_activity(group_id): return jsonify({'error': f'Failed to fetch group activity: {str(e)}'}), 500 # Public Workspaces API - @app.route('/api/admin/control-center/public-workspaces', methods=['GET']) + @bp.route('/api/admin/control-center/public-workspaces', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4079,7 +4079,7 @@ def api_control_center_public_workspaces(): debug_print(f"Error getting public workspaces for control center: {e}") return jsonify({'error': 'Failed to retrieve public workspaces'}), 500 - @app.route('/api/admin/control-center/public-workspaces//status', methods=['PUT']) + @bp.route('/api/admin/control-center/public-workspaces//status', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4178,7 +4178,7 @@ def api_update_public_workspace_status(workspace_id): debug_print(f"Error updating public workspace status: {e}") return jsonify({'error': 'Failed to update public workspace status'}), 500 - @app.route('/api/admin/control-center/public-workspaces/bulk-action', methods=['POST']) + @bp.route('/api/admin/control-center/public-workspaces/bulk-action', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4340,7 +4340,7 @@ def api_bulk_public_workspace_action(): debug_print(f"Error performing bulk public workspace action: {e}") return jsonify({'error': 'Failed to perform bulk action'}), 500 - @app.route('/api/admin/control-center/public-workspaces/', methods=['GET']) + @bp.route('/api/admin/control-center/public-workspaces/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4365,7 +4365,7 @@ def api_get_public_workspace_details(workspace_id): return jsonify({'error': 'Failed to retrieve workspace details'}), 500 - @app.route('/api/admin/control-center/public-workspaces//members', methods=['GET']) + @bp.route('/api/admin/control-center/public-workspaces//members', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4459,7 +4459,7 @@ def api_get_public_workspace_members(workspace_id): return jsonify({'error': 'Failed to retrieve workspace members'}), 500 - @app.route('/api/admin/control-center/public-workspaces//add-member', methods=['POST']) + @bp.route('/api/admin/control-center/public-workspaces//add-member', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4572,7 +4572,7 @@ def api_admin_add_workspace_member(workspace_id): return jsonify({'error': 'Failed to add workspace member'}), 500 - @app.route('/api/admin/control-center/public-workspaces//add-member-single', methods=['POST']) + @bp.route('/api/admin/control-center/public-workspaces//add-member-single', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4677,7 +4677,7 @@ def api_admin_add_workspace_member_single(workspace_id): return jsonify({'error': 'Failed to add workspace member'}), 500 - @app.route('/api/admin/control-center/public-workspaces//activity', methods=['GET']) + @bp.route('/api/admin/control-center/public-workspaces//activity', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4868,7 +4868,7 @@ def api_get_public_workspace_activity(workspace_id): return jsonify({'error': 'Failed to retrieve workspace activity'}), 500 - @app.route('/api/admin/control-center/public-workspaces//take-ownership', methods=['POST']) + @bp.route('/api/admin/control-center/public-workspaces//take-ownership', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -4949,7 +4949,7 @@ def api_admin_take_workspace_ownership(workspace_id): traceback.print_exc() return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/public-workspaces//ownership', methods=['PUT']) + @bp.route('/api/admin/control-center/public-workspaces//ownership', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -5073,7 +5073,7 @@ def api_update_public_workspace_ownership(workspace_id): return jsonify({'error': 'Failed to create ownership transfer request'}), 500 - @app.route('/api/admin/control-center/public-workspaces//documents', methods=['DELETE']) + @bp.route('/api/admin/control-center/public-workspaces//documents', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -5138,7 +5138,7 @@ def api_delete_public_workspace_documents_admin(workspace_id): return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/public-workspaces/', methods=['DELETE']) + @bp.route('/api/admin/control-center/public-workspaces/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -5206,7 +5206,7 @@ def api_delete_public_workspace_admin(workspace_id): return jsonify({'error': str(e)}), 500 # Activity Trends API - @app.route('/api/admin/control-center/activity-trends', methods=['GET']) + @bp.route('/api/admin/control-center/activity-trends', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('dashboard') @@ -5259,7 +5259,7 @@ def api_get_activity_trends(): return jsonify({'error': 'Failed to retrieve activity trends'}), 500 - @app.route('/api/admin/control-center/token-filters', methods=['GET']) + @bp.route('/api/admin/control-center/token-filters', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('dashboard') @@ -5357,7 +5357,7 @@ def api_get_token_filter_options(): - @app.route('/api/admin/control-center/activity-trends/export', methods=['POST']) + @bp.route('/api/admin/control-center/activity-trends/export', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('dashboard') @@ -5554,7 +5554,7 @@ def api_export_activity_trends(): debug_print(f"Error exporting activity trends: {e}") return jsonify({'error': 'Failed to export data'}), 500 - @app.route('/api/admin/control-center/activity-trends/chat', methods=['POST']) + @bp.route('/api/admin/control-center/activity-trends/chat', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('dashboard') @@ -5712,7 +5712,7 @@ def api_chat_activity_trends(): return jsonify({'error': 'Failed to create chat conversation'}), 500 # Data Refresh API - @app.route('/api/admin/control-center/refresh', methods=['POST']) + @bp.route('/api/admin/control-center/refresh', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -5857,7 +5857,7 @@ def api_refresh_control_center_data(): return jsonify({'error': 'Failed to refresh data'}), 500 # Get refresh status API - @app.route('/api/admin/control-center/refresh-status', methods=['GET']) + @bp.route('/api/admin/control-center/refresh-status', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -5881,7 +5881,7 @@ def api_get_refresh_status(): return jsonify({'error': 'Failed to get refresh status'}), 500 # Activity Log Migration APIs - @app.route('/api/admin/control-center/migrate/status', methods=['GET']) + @bp.route('/api/admin/control-center/migrate/status', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -5981,7 +5981,7 @@ def api_get_migration_status(): debug_print(f"Error getting migration status: {e}") return jsonify({'error': 'Failed to get migration status'}), 500 - @app.route('/api/admin/control-center/migrate/all', methods=['POST']) + @bp.route('/api/admin/control-center/migrate/all', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6291,7 +6291,7 @@ def api_migrate_to_activity_logs(): traceback.print_exc() return jsonify({'error': f'Migration failed: {str(e)}'}), 500 - @app.route('/api/admin/control-center/activity-logs', methods=['GET']) + @bp.route('/api/admin/control-center/activity-logs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6416,7 +6416,7 @@ def api_get_activity_logs(): ) return jsonify({'error': 'Failed to fetch activity logs'}), 500 - @app.route('/api/admin/control-center/activity-logs/export', methods=['GET']) + @bp.route('/api/admin/control-center/activity-logs/export', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6512,7 +6512,7 @@ def api_export_activity_logs(): # APPROVAL WORKFLOW ENDPOINTS # ============================================================================ - @app.route('/api/admin/control-center/approvals', methods=['GET']) + @bp.route('/api/admin/control-center/approvals', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6603,7 +6603,7 @@ def _get_authorized_route_approval( ) return approval, user_id, user_roles, user_email, user_name - @app.route('/api/admin/control-center/approvals/', methods=['GET']) + @bp.route('/api/admin/control-center/approvals/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6640,7 +6640,7 @@ def api_admin_get_approval_by_id(approval_id): debug_print(traceback.format_exc()) return jsonify({'error': 'Failed to fetch approval', 'details': str(e)}), 500 - @app.route('/api/admin/control-center/approvals//approve', methods=['POST']) + @bp.route('/api/admin/control-center/approvals//approve', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6695,7 +6695,7 @@ def api_admin_approve_request(approval_id): debug_print(f"Error approving request: {e}") return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/control-center/approvals//deny', methods=['POST']) + @bp.route('/api/admin/control-center/approvals//deny', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('admin') @@ -6751,7 +6751,7 @@ def api_admin_deny_request(approval_id): return jsonify({'error': str(e)}), 500 # New standalone approvals API endpoints (accessible to all users with permissions) - @app.route('/api/approvals', methods=['GET']) + @bp.route('/api/approvals', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required def api_get_approvals(): @@ -6822,7 +6822,7 @@ def api_get_approvals(): debug_print(traceback.format_exc()) return jsonify({'error': 'Failed to fetch approvals', 'details': str(e)}), 500 - @app.route('/api/approvals/', methods=['GET']) + @bp.route('/api/approvals/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required def api_get_approval_by_id(approval_id): @@ -6858,7 +6858,7 @@ def api_get_approval_by_id(approval_id): debug_print(traceback.format_exc()) return jsonify({'error': 'Failed to fetch approval', 'details': str(e)}), 500 - @app.route('/api/approvals//approve', methods=['POST']) + @bp.route('/api/approvals//approve', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required def api_approve_request(approval_id): @@ -6912,7 +6912,7 @@ def api_approve_request(approval_id): debug_print(f"Error approving request: {e}") return jsonify({'error': str(e)}), 500 - @app.route('/api/approvals//deny', methods=['POST']) + @bp.route('/api/approvals//deny', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required def api_deny_request(approval_id): @@ -7966,4 +7966,4 @@ def _execute_delete_user_documents(approval, executor_id, executor_email, execut debug_print(f"Error deleting user documents: {e}") return {'success': False, 'message': f'Failed to delete user documents: {str(e)}'} - return jsonify({'error': 'Failed to retrieve activity logs'}), 500 \ No newline at end of file + return jsonify({'error': 'Failed to retrieve activity logs'}), 500 diff --git a/application/single_app/route_backend_conversation_export.py b/application/single_app/route_backend_conversation_export.py index 7ce50e2dd..025c2cad1 100644 --- a/application/single_app/route_backend_conversation_export.py +++ b/application/single_app/route_backend_conversation_export.py @@ -110,10 +110,10 @@ ) -def register_route_backend_conversation_export(app): +def register_route_backend_conversation_export(bp): """Register conversation export API routes.""" - @app.route('/api/conversations/export', methods=['POST']) + @bp.route('/api/conversations/export', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -229,7 +229,7 @@ def api_export_conversations(): log_event(f"Conversation export failed: {exc}", level="WARNING") return jsonify({'error': f'Export failed: {str(exc)}'}), 500 - @app.route('/api/message/export-word', methods=['POST']) + @bp.route('/api/message/export-word', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -287,7 +287,7 @@ def api_export_message_word(): log_event(f"Message export failed: {exc}", level="WARNING") return jsonify({'error': 'Export failed due to a server error. Please try again later.'}), 500 - @app.route('/api/message/export-powerpoint', methods=['POST']) + @bp.route('/api/message/export-powerpoint', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -360,7 +360,7 @@ def api_export_message_powerpoint(): log_event(f"Message PowerPoint export failed: {exc}", level="WARNING") return jsonify({'error': 'PowerPoint export failed due to a server error. Please try again later.'}), 500 - @app.route('/api/message/export-email-draft', methods=['POST']) + @bp.route('/api/message/export-email-draft', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_conversations.py b/application/single_app/route_backend_conversations.py index 6e1c570b0..cd6117268 100644 --- a/application/single_app/route_backend_conversations.py +++ b/application/single_app/route_backend_conversations.py @@ -810,9 +810,9 @@ def _persist_scope_lock_update(conversation_item, conversation_kind, user_id, ne return conversation_item -def register_route_backend_conversations(app): +def register_route_backend_conversations(bp): - @app.route('/api/get_messages', methods=['GET']) + @bp.route('/api/get_messages', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -882,7 +882,7 @@ def api_get_messages(): print(f"ERROR: Failed to get messages: {str(e)}") return jsonify({'error': 'Conversation not found'}), 404 - @app.route('/api/image/', methods=['GET']) + @bp.route('/api/image/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -940,7 +940,7 @@ def api_get_image(image_id): traceback.print_exc() return jsonify({'error': 'Failed to retrieve image'}), 500 - @app.route('/api/get_conversations', methods=['GET']) + @bp.route('/api/get_conversations', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -956,7 +956,7 @@ def get_conversations(): }), 200 - @app.route('/api/conversations/feed', methods=['GET']) + @bp.route('/api/conversations/feed', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -998,7 +998,7 @@ def get_conversations_feed(): return jsonify({'error': 'Failed to load conversations'}), 500 - @app.route('/api/create_conversation', methods=['POST']) + @bp.route('/api/create_conversation', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1018,7 +1018,7 @@ def create_conversation(): 'title': conversation_item.get('title', 'New Conversation') }), 200 - @app.route('/api/conversations/', methods=['PUT']) + @bp.route('/api/conversations/', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1065,7 +1065,7 @@ def update_conversation_title(conversation_id): print(e) return jsonify({'error': 'Failed to update conversation'}), 500 - @app.route('/api/conversations/', methods=['DELETE']) + @bp.route('/api/conversations/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1193,7 +1193,7 @@ def delete_conversation(conversation_id): "success": True }), 200 - @app.route('/api/delete_multiple_conversations', methods=['POST']) + @bp.route('/api/delete_multiple_conversations', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1296,7 +1296,7 @@ def delete_multiple_conversations(): "failed_ids": failed_ids }), 200 - @app.route('/api/conversations//pin', methods=['POST']) + @bp.route('/api/conversations//pin', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1338,7 +1338,7 @@ def toggle_conversation_pin(conversation_id): print(f"Error toggling conversation pin: {e}") return jsonify({'error': 'Failed to toggle pin status'}), 500 - @app.route('/api/conversations//hide', methods=['POST']) + @bp.route('/api/conversations//hide', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1380,7 +1380,7 @@ def toggle_conversation_hide(conversation_id): print(f"Error toggling conversation hide: {e}") return jsonify({'error': 'Failed to toggle hide status'}), 500 - @app.route('/api/conversations/bulk-pin', methods=['POST']) + @bp.route('/api/conversations/bulk-pin', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1438,7 +1438,7 @@ def bulk_pin_conversations(): "action": pin_action }), 200 - @app.route('/api/conversations/bulk-hide', methods=['POST']) + @bp.route('/api/conversations/bulk-hide', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1496,7 +1496,7 @@ def bulk_hide_conversations(): "action": hide_action }), 200 - @app.route('/api/conversations//metadata', methods=['GET']) + @bp.route('/api/conversations//metadata', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1566,7 +1566,7 @@ def get_conversation_metadata_api(conversation_id): print(f"Error retrieving conversation metadata: {e}") return jsonify({'error': 'Failed to retrieve conversation metadata'}), 500 - @app.route('/api/conversations//mark-read', methods=['POST']) + @bp.route('/api/conversations//mark-read', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1606,7 +1606,7 @@ def mark_conversation_read_api(conversation_id): debug_print(f"Error marking conversation {conversation_id} as read: {e}") return jsonify({'error': 'Failed to mark conversation as read'}), 500 - @app.route('/api/conversations//summary', methods=['POST']) + @bp.route('/api/conversations//summary', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1723,7 +1723,7 @@ def generate_conversation_summary_api(conversation_id): debug_print(f"Summary generation API error: {exc}") return jsonify({'error': 'Summary generation failed'}), 500 - @app.route('/api/conversations//scope_lock', methods=['PATCH']) + @bp.route('/api/conversations//scope_lock', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1772,7 +1772,7 @@ def patch_conversation_scope_lock(conversation_id): debug_print(f"Error updating scope lock: {e}") return jsonify({'error': 'Failed to update scope lock'}), 500 - @app.route('/api/conversations/classifications', methods=['GET']) + @bp.route('/api/conversations/classifications', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1813,7 +1813,7 @@ def get_user_classifications(): print(f"Error fetching classifications: {e}") return jsonify({'error': 'Failed to fetch classifications'}), 500 - @app.route('/api/search_conversations', methods=['POST']) + @bp.route('/api/search_conversations', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2017,7 +2017,7 @@ def search_conversations(): ) return jsonify({'error': 'Failed to search conversations'}), 500 - @app.route('/api/user-settings/search-history', methods=['GET']) + @bp.route('/api/user-settings/search-history', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2037,7 +2037,7 @@ def get_search_history(): print(f"Error retrieving search history: {e}") return jsonify({'error': 'Failed to retrieve search history'}), 500 - @app.route('/api/user-settings/search-history', methods=['POST']) + @bp.route('/api/user-settings/search-history', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2063,7 +2063,7 @@ def save_search_to_history(): print(f"Error saving search to history: {e}") return jsonify({'error': 'Failed to save search to history'}), 500 - @app.route('/api/user-settings/search-history', methods=['DELETE']) + @bp.route('/api/user-settings/search-history', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2086,7 +2086,7 @@ def clear_search_history(): print(f"Error clearing search history: {e}") return jsonify({'error': 'Failed to clear search history'}), 500 - @app.route('/api/message/', methods=['DELETE']) + @bp.route('/api/message/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2293,7 +2293,7 @@ def delete_message(message_id): import traceback traceback.print_exc() return jsonify({'error': 'Failed to delete message'}), 500 - @app.route('/api/message//retry', methods=['POST']) + @bp.route('/api/message//retry', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2507,7 +2507,7 @@ def retry_message(message_id): traceback.print_exc() return jsonify({'error': 'Failed to retry message'}), 500 - @app.route('/api/message//edit', methods=['POST']) + @bp.route('/api/message//edit', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2726,7 +2726,7 @@ def edit_message(message_id): traceback.print_exc() return jsonify({'error': 'Failed to edit message'}), 500 - @app.route('/api/message//switch-attempt', methods=['POST']) + @bp.route('/api/message//switch-attempt', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_data_management.py b/application/single_app/route_backend_data_management.py index 7ae89cde3..e41541324 100644 --- a/application/single_app/route_backend_data_management.py +++ b/application/single_app/route_backend_data_management.py @@ -58,8 +58,8 @@ def _log_data_management_admin_action(action, description, additional_context=No ) -def register_route_backend_data_management(app): - @app.route("/api/admin/data-management/settings", methods=["GET"]) +def register_route_backend_data_management(bp): + @bp.route("/api/admin/data-management/settings", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -70,7 +70,7 @@ def get_admin_data_management_settings(): "settings": sanitize_data_management_settings_for_admin(settings), }), 200 - @app.route("/api/admin/data-management/settings", methods=["PUT"]) + @bp.route("/api/admin/data-management/settings", methods=["PUT"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -99,7 +99,7 @@ def update_admin_data_management_settings(): "settings": sanitize_data_management_settings_for_admin(settings), }), 200 - @app.route("/api/admin/data-management/encryption-key", methods=["POST"]) + @bp.route("/api/admin/data-management/encryption-key", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -122,7 +122,7 @@ def generate_admin_data_management_encryption_key(): ) return jsonify({"success": True, "settings": settings}), 200 - @app.route("/api/admin/data-management/storage/test", methods=["POST"]) + @bp.route("/api/admin/data-management/storage/test", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -143,7 +143,7 @@ def test_admin_data_management_storage(): return jsonify({"success": False, "error": "Backup storage connection test failed."}), 400 return jsonify(result), 200 - @app.route("/api/admin/data-management/target/cosmos/test", methods=["POST"]) + @bp.route("/api/admin/data-management/target/cosmos/test", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -161,7 +161,7 @@ def test_admin_data_management_target_cosmos(): return jsonify({"success": False, "error": "Target Cosmos connection test failed."}), 400 return jsonify(result), 200 - @app.route("/api/admin/data-management/target/search/test", methods=["POST"]) + @bp.route("/api/admin/data-management/target/search/test", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -179,7 +179,7 @@ def test_admin_data_management_target_search(): return jsonify({"success": False, "error": "Target Search connection test failed."}), 400 return jsonify(result), 200 - @app.route("/api/admin/data-management/target/enhanced-citation-storage/test", methods=["POST"]) + @bp.route("/api/admin/data-management/target/enhanced-citation-storage/test", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -201,7 +201,7 @@ def test_admin_data_management_target_enhanced_citation_storage(): return jsonify({"success": False, "error": "Target Enhanced Citation Storage connection test failed."}), 400 return jsonify(result), 200 - @app.route("/api/admin/data-management/jobs", methods=["GET"]) + @bp.route("/api/admin/data-management/jobs", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -210,7 +210,7 @@ def list_admin_data_management_jobs(): jobs = [sanitize_data_management_job_for_admin(job) for job in get_data_management_jobs(limit=limit)] return jsonify({"success": True, "jobs": jobs}), 200 - @app.route("/api/admin/data-management/jobs/", methods=["GET"]) + @bp.route("/api/admin/data-management/jobs/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -220,7 +220,7 @@ def get_admin_data_management_job_detail(job_id): return jsonify({"success": False, "error": "Data Management job was not found."}), 404 return jsonify({"success": True, **detail}), 200 - @app.route("/api/admin/data-management/backups", methods=["GET"]) + @bp.route("/api/admin/data-management/backups", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -229,7 +229,7 @@ def list_admin_data_management_backups(): backup_summary = get_data_management_backup_summary(limit=limit) return jsonify({"success": True, **backup_summary}), 200 - @app.route("/api/admin/data-management/migration/catalog/", methods=["GET"]) + @bp.route("/api/admin/data-management/migration/catalog/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -242,7 +242,7 @@ def get_admin_data_management_migration_catalog(target_type): return jsonify({"success": False, "error": str(exc)}), 400 return jsonify({"success": True, **catalog}), 200 - @app.route("/api/admin/data-management/migration/summary", methods=["POST"]) + @bp.route("/api/admin/data-management/migration/summary", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -254,7 +254,7 @@ def summarize_admin_data_management_migration(): return jsonify({"success": False, "error": str(exc)}), 400 return jsonify({"success": True, "summary": summary}), 200 - @app.route("/api/admin/data-management/jobs", methods=["POST"]) + @bp.route("/api/admin/data-management/jobs", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -296,4 +296,4 @@ def create_admin_data_management_job(): ) public_job = sanitize_data_management_job_for_admin(job) public_job["submitted_to_executor"] = submitted - return jsonify({"success": True, "job": public_job}), 202 \ No newline at end of file + return jsonify({"success": True, "job": public_job}), 202 diff --git a/application/single_app/route_backend_documents.py b/application/single_app/route_backend_documents.py index 7b2ac0f3b..47bf29336 100644 --- a/application/single_app/route_backend_documents.py +++ b/application/single_app/route_backend_documents.py @@ -346,8 +346,8 @@ def _find_accessible_citation_document(user_id, document_id, scope_name): return None -def register_route_backend_documents(app): - @app.route('/api/get_file_content', methods=['POST']) +def register_route_backend_documents(bp): + @bp.route('/api/get_file_content', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -510,7 +510,7 @@ def get_file_content(): add_file_task_to_file_processing_log(document_id=file_id, user_id=user_id, content="Error retrieving file content: " + str(e)) return jsonify({'error': f'Error retrieving file content: {str(e)}'}), 500 - @app.route('/api/documents/upload', methods=['POST']) + @bp.route('/api/documents/upload', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -651,7 +651,7 @@ def api_user_upload_document(): }), response_status - @app.route('/api/documents', methods=['GET']) + @bp.route('/api/documents', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -832,7 +832,7 @@ def api_get_user_documents(): "needs_legacy_update_check": legacy_count > 0 }), 200 - @app.route('/api/documents/', methods=['GET']) + @bp.route('/api/documents/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -844,7 +844,7 @@ def api_get_user_document(document_id): return get_document(user_id, document_id) - @app.route('/api/documents//versions', methods=['GET']) + @bp.route('/api/documents//versions', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -864,7 +864,7 @@ def api_get_user_document_versions(document_id): 'versions': versions, }), 200 - @app.route('/api/documents//download', methods=['GET']) + @bp.route('/api/documents//download', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -892,7 +892,7 @@ def api_download_user_document(document_id): ) return jsonify({'error': 'Unable to download document'}), 500 - @app.route('/api/documents/download', methods=['POST']) + @bp.route('/api/documents/download', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -945,7 +945,7 @@ def api_download_user_documents(): ) return jsonify({'error': 'Unable to download selected documents'}), 500 - @app.route('/api/documents/', methods=['PATCH']) + @bp.route('/api/documents/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1091,7 +1091,7 @@ def api_patch_user_document(document_id): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/documents/', methods=['DELETE']) + @bp.route('/api/documents/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1162,7 +1162,7 @@ def api_delete_user_document(document_id): except Exception as e: return jsonify({'error': f'Error deleting document: {str(e)}'}), 500 - @app.route('/api/documents//extract_metadata', methods=['POST']) + @bp.route('/api/documents//extract_metadata', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1195,7 +1195,7 @@ def api_extract_user_metadata(document_id): 'document_id': document_id }), 200 - @app.route('/api/documents/reprocess_extraction', methods=['POST']) + @bp.route('/api/documents/reprocess_extraction', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1258,7 +1258,7 @@ def api_reprocess_document_extraction(): 'errors': errors, }), status_code - @app.route("/api/get_citation", methods=["POST"]) + @bp.route("/api/get_citation", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1323,7 +1323,7 @@ def get_citation_for_scope(search_client, scope_name): return jsonify({"error": "Citation not found in user, group, or public docs"}), 404 - @app.route('/api/documents/upgrade_legacy', methods=['POST']) + @bp.route('/api/documents/upgrade_legacy', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1338,7 +1338,7 @@ def api_upgrade_legacy_user_documents(): # ============= TAG MANAGEMENT API ENDPOINTS ============= - @app.route('/api/documents/tags', methods=['GET']) + @bp.route('/api/documents/tags', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1357,7 +1357,7 @@ def api_get_workspace_tags(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/documents/tags', methods=['POST']) + @bp.route('/api/documents/tags', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1442,7 +1442,7 @@ def api_create_tag(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/documents/bulk-tag', methods=['POST']) + @bp.route('/api/documents/bulk-tag', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1589,7 +1589,7 @@ def api_bulk_tag_documents(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/documents/tags/', methods=['PATCH']) + @bp.route('/api/documents/tags/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1775,7 +1775,7 @@ def api_update_tag(tag_name): traceback.print_exc() return jsonify({'error': str(e)}), 500 - @app.route('/api/documents/tags/', methods=['DELETE']) + @bp.route('/api/documents/tags/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1869,7 +1869,7 @@ def api_delete_tag(tag_name): return jsonify({'error': str(e)}), 500 # ============= DOCUMENT SHARING API ENDPOINTS ============= - @app.route('/api/documents//share', methods=['POST']) + @bp.route('/api/documents//share', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1923,7 +1923,7 @@ def api_share_document(document_id): except Exception as e: return jsonify({'error': f'Error sharing document: {str(e)}'}), 500 - @app.route('/api/documents//unshare', methods=['DELETE']) + @bp.route('/api/documents//unshare', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1963,7 +1963,7 @@ def api_unshare_document(document_id): except Exception as e: return jsonify({'error': f'Error unsharing document: {str(e)}'}), 500 - @app.route('/api/documents//shared-users', methods=['GET']) + @bp.route('/api/documents//shared-users', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2032,7 +2032,7 @@ def api_get_shared_users(document_id): except Exception as e: return jsonify({'error': f'Error getting shared users: {str(e)}'}), 500 - @app.route('/api/documents//remove-self', methods=['DELETE']) + @bp.route('/api/documents//remove-self', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2083,7 +2083,7 @@ def api_remove_self_from_document(document_id): debug_print(f"[ERROR] /api/documents/{document_id}/remove-self: {e}", flush=True) return jsonify({'error': f'Error removing from shared document: {str(e)}'}), 500 - @app.route('/api/documents//approve-share', methods=['POST']) + @bp.route('/api/documents//approve-share', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2146,4 +2146,4 @@ def api_approve_shared_document(document_id): except exceptions.CosmosResourceNotFoundError: return jsonify({'error': 'Document not found or access denied'}), 404 except Exception as e: - return jsonify({'error': f'Error approving shared document: {str(e)}'}), 500 \ No newline at end of file + return jsonify({'error': f'Error approving shared document: {str(e)}'}), 500 diff --git a/application/single_app/route_backend_feedback.py b/application/single_app/route_backend_feedback.py index 2d537ff1a..ffc0217fb 100644 --- a/application/single_app/route_backend_feedback.py +++ b/application/single_app/route_backend_feedback.py @@ -211,9 +211,9 @@ def _build_feedback_export_response(items, filename_prefix, include_user_id=Fals ) return response -def register_route_backend_feedback(app): +def register_route_backend_feedback(bp): - @app.route("/feedback/submit", methods=["POST"]) + @bp.route("/feedback/submit", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -345,7 +345,7 @@ def feedback_submit(): return jsonify({"error": "Failed to save feedback"}), 500 - @app.route("/feedback/review", methods=["GET"]) + @bp.route("/feedback/review", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -380,7 +380,7 @@ def feedback_review_get(): traceback.print_exc() return jsonify({"error": f"Failed to retrieve feedback: {str(e)}"}), 500 - @app.route("/feedback/review/stats", methods=["GET"]) + @bp.route("/feedback/review/stats", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -397,7 +397,7 @@ def feedback_review_stats(): except Exception as e: return jsonify({"error": f"Failed to retrieve feedback stats: {str(e)}"}), 500 - @app.route("/feedback/review/export", methods=["GET"]) + @bp.route("/feedback/review/export", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -414,7 +414,7 @@ def feedback_review_export(): except Exception as e: return jsonify({"error": f"Failed to export feedback: {str(e)}"}), 500 - @app.route("/feedback/review/", methods=["GET"]) + @bp.route("/feedback/review/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -450,7 +450,7 @@ def feedback_review_get_single(feedbackId): traceback.print_exc() return jsonify({"error": f"Failed to retrieve feedback item: {str(e)}"}), 500 - @app.route("/feedback/review/", methods=["PATCH"]) + @bp.route("/feedback/review/", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -495,7 +495,7 @@ def feedback_review_update(feedbackId): return jsonify({"error": "Failed to save changes"}), 500 - @app.route("/feedback/retest/", methods=["POST"]) + @bp.route("/feedback/retest/", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -516,7 +516,7 @@ def feedback_retest(feedbackId): except Exception as e: return jsonify({"error": str(e)}), 500 - @app.route("/feedback/my", methods=["GET"]) + @bp.route("/feedback/my", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -556,7 +556,7 @@ def feedback_my(): print(f"Error in feedback_my: {str(e)}") return jsonify({"error": f"An error occurred while fetching your feedback: {str(e)}"}), 500 - @app.route("/feedback/my/stats", methods=["GET"]) + @bp.route("/feedback/my/stats", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -578,7 +578,7 @@ def feedback_my_stats(): except Exception as e: return jsonify({"error": f"Failed to retrieve feedback stats: {str(e)}"}), 500 - @app.route("/feedback/my/export", methods=["GET"]) + @bp.route("/feedback/my/export", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -608,4 +608,4 @@ def run_prompt_against_gpt(prompt): # response = llm_client.invoke(prompt) # return response.content print(f"Retesting prompt (stub): {prompt}") - return f"[Retested with current model config] Mock AI response for: '{prompt}'" \ No newline at end of file + return f"[Retested with current model config] Mock AI response for: '{prompt}'" diff --git a/application/single_app/route_backend_file_sync.py b/application/single_app/route_backend_file_sync.py index 75b0beadc..baba504f8 100644 --- a/application/single_app/route_backend_file_sync.py +++ b/application/single_app/route_backend_file_sync.py @@ -35,7 +35,7 @@ from swagger_wrapper import get_auth_security, swagger_route -def register_route_backend_file_sync(app): +def register_route_backend_file_sync(bp): def _error(message, status=400): return jsonify({"error": message}), status @@ -184,7 +184,7 @@ def _browse_source(scope_type, scope_id, user_id, source_id=None): result = browse_file_sync_source_path(scope_type, scope_id, payload, user_id, source_id=source_id) return jsonify({"browse": result}), 200 - @app.route('/api/admin/file-sync/users/search', methods=['GET']) + @bp.route('/api/admin/file-sync/users/search', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -198,7 +198,7 @@ def api_admin_file_sync_user_search(): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/groups/search', methods=['GET']) + @bp.route('/api/admin/file-sync/groups/search', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -212,7 +212,7 @@ def api_admin_file_sync_group_search(): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public-workspaces/search', methods=['GET']) + @bp.route('/api/admin/file-sync/public-workspaces/search', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -226,7 +226,7 @@ def api_admin_file_sync_public_workspace_search(): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync///sources/browse', methods=['POST']) + @bp.route('/api/admin/file-sync///sources/browse', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -238,7 +238,7 @@ def api_admin_file_sync_source_browse_new(scope_type, scope_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync///sources//browse', methods=['POST']) + @bp.route('/api/admin/file-sync///sources//browse', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -250,7 +250,7 @@ def api_admin_file_sync_source_browse_existing(scope_type, scope_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources', methods=['GET']) + @bp.route('/api/admin/file-sync/personal//sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -262,7 +262,7 @@ def api_admin_file_sync_personal_sources_list(target_user_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources', methods=['POST']) + @bp.route('/api/admin/file-sync/personal//sources', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -274,7 +274,7 @@ def api_admin_file_sync_personal_sources_create(target_user_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources/test-connection', methods=['POST']) + @bp.route('/api/admin/file-sync/personal//sources/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -286,7 +286,7 @@ def api_admin_file_sync_personal_source_test_connection_new(target_user_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources/', methods=['PATCH']) + @bp.route('/api/admin/file-sync/personal//sources/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -298,7 +298,7 @@ def api_admin_file_sync_personal_source_update(target_user_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources//test-connection', methods=['POST']) + @bp.route('/api/admin/file-sync/personal//sources//test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -310,7 +310,7 @@ def api_admin_file_sync_personal_source_test_connection_existing(target_user_id, except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources/', methods=['DELETE']) + @bp.route('/api/admin/file-sync/personal//sources/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -322,7 +322,7 @@ def api_admin_file_sync_personal_source_delete(target_user_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources//sync', methods=['POST']) + @bp.route('/api/admin/file-sync/personal//sources//sync', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -334,7 +334,7 @@ def api_admin_file_sync_personal_source_sync(target_user_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/personal//sources//runs', methods=['GET']) + @bp.route('/api/admin/file-sync/personal//sources//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -346,7 +346,7 @@ def api_admin_file_sync_personal_source_runs(target_user_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources', methods=['GET']) + @bp.route('/api/admin/file-sync/group//sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -358,7 +358,7 @@ def api_admin_file_sync_group_sources_list(group_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources', methods=['POST']) + @bp.route('/api/admin/file-sync/group//sources', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -370,7 +370,7 @@ def api_admin_file_sync_group_sources_create(group_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources/test-connection', methods=['POST']) + @bp.route('/api/admin/file-sync/group//sources/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -382,7 +382,7 @@ def api_admin_file_sync_group_source_test_connection_new(group_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources/', methods=['PATCH']) + @bp.route('/api/admin/file-sync/group//sources/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -394,7 +394,7 @@ def api_admin_file_sync_group_source_update(group_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources//test-connection', methods=['POST']) + @bp.route('/api/admin/file-sync/group//sources//test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -406,7 +406,7 @@ def api_admin_file_sync_group_source_test_connection_existing(group_id, source_i except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources/', methods=['DELETE']) + @bp.route('/api/admin/file-sync/group//sources/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -418,7 +418,7 @@ def api_admin_file_sync_group_source_delete(group_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources//sync', methods=['POST']) + @bp.route('/api/admin/file-sync/group//sources//sync', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -430,7 +430,7 @@ def api_admin_file_sync_group_source_sync(group_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/group//sources//runs', methods=['GET']) + @bp.route('/api/admin/file-sync/group//sources//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -442,7 +442,7 @@ def api_admin_file_sync_group_source_runs(group_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources', methods=['GET']) + @bp.route('/api/admin/file-sync/public//sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -454,7 +454,7 @@ def api_admin_file_sync_public_sources_list(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources', methods=['POST']) + @bp.route('/api/admin/file-sync/public//sources', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -466,7 +466,7 @@ def api_admin_file_sync_public_sources_create(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources/test-connection', methods=['POST']) + @bp.route('/api/admin/file-sync/public//sources/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -478,7 +478,7 @@ def api_admin_file_sync_public_source_test_connection_new(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources/', methods=['PATCH']) + @bp.route('/api/admin/file-sync/public//sources/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -490,7 +490,7 @@ def api_admin_file_sync_public_source_update(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources//test-connection', methods=['POST']) + @bp.route('/api/admin/file-sync/public//sources//test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -502,7 +502,7 @@ def api_admin_file_sync_public_source_test_connection_existing(public_workspace_ except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources/', methods=['DELETE']) + @bp.route('/api/admin/file-sync/public//sources/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -514,7 +514,7 @@ def api_admin_file_sync_public_source_delete(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources//sync', methods=['POST']) + @bp.route('/api/admin/file-sync/public//sources//sync', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -526,7 +526,7 @@ def api_admin_file_sync_public_source_sync(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/file-sync/public//sources//runs', methods=['GET']) + @bp.route('/api/admin/file-sync/public//sources//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -538,7 +538,7 @@ def api_admin_file_sync_public_source_runs(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources', methods=['GET']) + @bp.route('/api/file-sync/personal/sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -550,7 +550,7 @@ def api_file_sync_personal_sources_list(): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync//sources/browse', methods=['POST']) + @bp.route('/api/file-sync//sources/browse', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -567,7 +567,7 @@ def api_file_sync_source_browse_new(scope_type): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync//sources//browse', methods=['POST']) + @bp.route('/api/file-sync//sources//browse', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -584,7 +584,7 @@ def api_file_sync_source_browse_existing(scope_type, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources/browse', methods=['POST']) + @bp.route('/api/file-sync/public//sources/browse', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -596,7 +596,7 @@ def api_file_sync_public_source_browse_new(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources//browse', methods=['POST']) + @bp.route('/api/file-sync/public//sources//browse', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -608,7 +608,7 @@ def api_file_sync_public_source_browse_existing(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources', methods=['POST']) + @bp.route('/api/file-sync/personal/sources', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -620,7 +620,7 @@ def api_file_sync_personal_sources_create(): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources/test-connection', methods=['POST']) + @bp.route('/api/file-sync/personal/sources/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -632,7 +632,7 @@ def api_file_sync_personal_source_test_connection_new(): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources/', methods=['PATCH']) + @bp.route('/api/file-sync/personal/sources/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -644,7 +644,7 @@ def api_file_sync_personal_source_update(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources//test-connection', methods=['POST']) + @bp.route('/api/file-sync/personal/sources//test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -656,7 +656,7 @@ def api_file_sync_personal_source_test_connection_existing(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources/', methods=['DELETE']) + @bp.route('/api/file-sync/personal/sources/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -668,7 +668,7 @@ def api_file_sync_personal_source_delete(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources//sync', methods=['POST']) + @bp.route('/api/file-sync/personal/sources//sync', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -680,7 +680,7 @@ def api_file_sync_personal_source_sync(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources//runs', methods=['GET']) + @bp.route('/api/file-sync/personal/sources//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -692,7 +692,7 @@ def api_file_sync_personal_source_runs(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/personal/sources//ignore-path', methods=['POST']) + @bp.route('/api/file-sync/personal/sources//ignore-path', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -704,7 +704,7 @@ def api_file_sync_personal_source_ignore_path(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources', methods=['GET']) + @bp.route('/api/file-sync/group/sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -716,7 +716,7 @@ def api_file_sync_group_sources_list(): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources', methods=['POST']) + @bp.route('/api/file-sync/group/sources', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -728,7 +728,7 @@ def api_file_sync_group_sources_create(): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources/test-connection', methods=['POST']) + @bp.route('/api/file-sync/group/sources/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -740,7 +740,7 @@ def api_file_sync_group_source_test_connection_new(): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources/', methods=['PATCH']) + @bp.route('/api/file-sync/group/sources/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -752,7 +752,7 @@ def api_file_sync_group_source_update(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources//test-connection', methods=['POST']) + @bp.route('/api/file-sync/group/sources//test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -764,7 +764,7 @@ def api_file_sync_group_source_test_connection_existing(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources/', methods=['DELETE']) + @bp.route('/api/file-sync/group/sources/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -776,7 +776,7 @@ def api_file_sync_group_source_delete(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources//sync', methods=['POST']) + @bp.route('/api/file-sync/group/sources//sync', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -788,7 +788,7 @@ def api_file_sync_group_source_sync(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources//runs', methods=['GET']) + @bp.route('/api/file-sync/group/sources//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -800,7 +800,7 @@ def api_file_sync_group_source_runs(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/group/sources//ignore-path', methods=['POST']) + @bp.route('/api/file-sync/group/sources//ignore-path', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -812,7 +812,7 @@ def api_file_sync_group_source_ignore_path(source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources', methods=['GET']) + @bp.route('/api/file-sync/public//sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -824,7 +824,7 @@ def api_file_sync_public_sources_list(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources', methods=['POST']) + @bp.route('/api/file-sync/public//sources', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -836,7 +836,7 @@ def api_file_sync_public_sources_create(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources/test-connection', methods=['POST']) + @bp.route('/api/file-sync/public//sources/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -848,7 +848,7 @@ def api_file_sync_public_source_test_connection_new(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources/', methods=['PATCH']) + @bp.route('/api/file-sync/public//sources/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -860,7 +860,7 @@ def api_file_sync_public_source_update(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources//test-connection', methods=['POST']) + @bp.route('/api/file-sync/public//sources//test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -872,7 +872,7 @@ def api_file_sync_public_source_test_connection_existing(public_workspace_id, so except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources/', methods=['DELETE']) + @bp.route('/api/file-sync/public//sources/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -884,7 +884,7 @@ def api_file_sync_public_source_delete(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources//sync', methods=['POST']) + @bp.route('/api/file-sync/public//sources//sync', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -896,7 +896,7 @@ def api_file_sync_public_source_sync(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources//runs', methods=['GET']) + @bp.route('/api/file-sync/public//sources//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -908,7 +908,7 @@ def api_file_sync_public_source_runs(public_workspace_id, source_id): except Exception as error: return _map_exception(error) - @app.route('/api/file-sync/public//sources//ignore-path', methods=['POST']) + @bp.route('/api/file-sync/public//sources//ignore-path', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -918,4 +918,4 @@ def api_file_sync_public_source_ignore_path(public_workspace_id, source_id): user_id, workspace_id = _require_public_context(public_workspace_id) return _ignore_path(FILE_SYNC_SCOPE_PUBLIC, workspace_id, source_id, user_id) except Exception as error: - return _map_exception(error) \ No newline at end of file + return _map_exception(error) diff --git a/application/single_app/route_backend_governance.py b/application/single_app/route_backend_governance.py index c72b42bbe..f56e78da0 100644 --- a/application/single_app/route_backend_governance.py +++ b/application/single_app/route_backend_governance.py @@ -84,8 +84,8 @@ def _build_item_policy_search_haystack(policy): ]).lower() -def register_route_backend_governance(app): - @app.route('/api/admin/governance/policies', methods=['GET']) +def register_route_backend_governance(bp): + @bp.route('/api/admin/governance/policies', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -96,7 +96,7 @@ def get_governance_policies_route(): 'feature_keys': list(DEFAULT_FEATURE_POLICIES.keys()), }), 200 - @app.route('/api/admin/governance/policies/', methods=['PUT']) + @bp.route('/api/admin/governance/policies/', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -139,21 +139,21 @@ def _delete_governance_item_policy(entity_type, item_id, policy_id=None): return jsonify({'deleted': deleted}), 200 - @app.route('/api/admin/governance/item-policies//', methods=['DELETE']) + @bp.route('/api/admin/governance/item-policies//', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required def delete_governance_item_policy_route(entity_type, item_id): return _delete_governance_item_policy(entity_type, item_id) - @app.route('/api/admin/governance/item-policies///', methods=['DELETE']) + @bp.route('/api/admin/governance/item-policies///', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required def delete_governance_named_item_policy_route(entity_type, item_id, policy_id): return _delete_governance_item_policy(entity_type, item_id, policy_id) - @app.route('/api/admin/governance/item-policies', methods=['GET']) + @bp.route('/api/admin/governance/item-policies', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -161,7 +161,7 @@ def get_governance_item_policies_route(): entity_type = str(request.args.get('entity_type') or '').strip() or None return jsonify({'item_policies': list_item_policies(entity_type=entity_type)}), 200 - @app.route('/api/admin/governance/item-policies/review', methods=['GET']) + @bp.route('/api/admin/governance/item-policies/review', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -197,7 +197,7 @@ def review_governance_item_policies_route(): 'entity_type': entity_type, }), 200 - @app.route('/api/admin/governance/item-policies//', methods=['PUT']) + @bp.route('/api/admin/governance/item-policies//', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @admin_required diff --git a/application/single_app/route_backend_group_documents.py b/application/single_app/route_backend_group_documents.py index 88e03dd8b..af6d69acb 100644 --- a/application/single_app/route_backend_group_documents.py +++ b/application/single_app/route_backend_group_documents.py @@ -271,7 +271,7 @@ def _create_group_document_share_decision_notification( }, ) -def register_route_backend_group_documents(app): +def register_route_backend_group_documents(bp): """ Provides backend routes for group-level document management: - GET /api/group_documents (list) @@ -279,7 +279,7 @@ def register_route_backend_group_documents(app): - DELETE /api/group_documents/ """ - @app.route('/api/group_documents/upload', methods=['POST']) + @bp.route('/api/group_documents/upload', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -397,7 +397,7 @@ def api_upload_group_document(): }), response_status - @app.route('/api/group_documents', methods=['GET']) + @bp.route('/api/group_documents', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -661,7 +661,7 @@ def api_get_group_documents(): "needs_legacy_update_check": legacy_count > 0 }), 200 - @app.route('/api/group_documents/', methods=['GET']) + @bp.route('/api/group_documents/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -689,7 +689,7 @@ def api_get_group_document(document_id): return get_document(user_id=user_id, document_id=document_id, group_id=active_group_id) - @app.route('/api/group_documents//versions', methods=['GET']) + @bp.route('/api/group_documents//versions', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -753,7 +753,7 @@ def _authorize_group_document_download(user_id, document_id): return None, None, (jsonify({'error': 'Document not found or access denied'}), 404) return active_group_id, document_record, None - @app.route('/api/group_documents//download', methods=['GET']) + @bp.route('/api/group_documents//download', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -779,7 +779,7 @@ def api_download_group_document(document_id): ) return jsonify({'error': 'Unable to download document'}), 500 - @app.route('/api/group_documents/download', methods=['POST']) + @bp.route('/api/group_documents/download', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -837,7 +837,7 @@ def api_download_group_documents(): ) return jsonify({'error': 'Unable to download selected documents'}), 500 - @app.route('/api/group_documents/', methods=['PATCH']) + @bp.route('/api/group_documents/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -980,7 +980,7 @@ def api_patch_group_document(document_id): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/group_documents/', methods=['DELETE']) + @bp.route('/api/group_documents/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1062,7 +1062,7 @@ def api_delete_group_document(document_id): except Exception as e: return jsonify({'error': f'Error deleting group document: {str(e)}'}), 500 - @app.route('/api/group_documents//extract_metadata', methods=['POST']) + @bp.route('/api/group_documents//extract_metadata', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1102,7 +1102,7 @@ def api_extract_group_metadata(document_id): 'document_id': document_id }), 200 - @app.route('/api/group_documents/reprocess_extraction', methods=['POST']) + @bp.route('/api/group_documents/reprocess_extraction', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1194,7 +1194,7 @@ def api_reprocess_group_document_extraction(): 'errors': errors, }), status_code - @app.route('/api/group_documents/upgrade_legacy', methods=['POST']) + @bp.route('/api/group_documents/upgrade_legacy', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1218,7 +1218,7 @@ def api_upgrade_legacy_group_documents(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/group_documents//shared-groups', methods=['GET']) + @bp.route('/api/group_documents//shared-groups', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1283,7 +1283,7 @@ def api_get_document_shared_groups(document_id): except Exception as e: return jsonify({'error': f'Error retrieving shared groups: {str(e)}'}), 500 - @app.route('/api/group_documents//approve-share-with-group', methods=['POST']) + @bp.route('/api/group_documents//approve-share-with-group', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1364,7 +1364,7 @@ def api_approve_shared_group_document(document_id): except Exception as e: return jsonify({'error': f'Error approving shared document: {str(e)}'}), 500 - @app.route('/api/group_documents//approve-generated-artifact', methods=['POST']) + @bp.route('/api/group_documents//approve-generated-artifact', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1478,7 +1478,7 @@ def api_approve_group_generated_artifact(document_id): except Exception as e: return jsonify({'error': f'Error approving generated artifact: {str(e)}'}), 500 - @app.route('/api/group_documents//deny-generated-artifact', methods=['POST']) + @bp.route('/api/group_documents//deny-generated-artifact', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1560,7 +1560,7 @@ def api_deny_group_generated_artifact(document_id): except Exception as e: return jsonify({'error': f'Error denying generated artifact: {str(e)}'}), 500 - @app.route('/api/group_documents//cancel-generated-artifact', methods=['POST']) + @bp.route('/api/group_documents//cancel-generated-artifact', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1620,7 +1620,7 @@ def api_cancel_group_generated_artifact(document_id): except Exception as e: return jsonify({'error': f'Error canceling generated artifact: {str(e)}'}), 500 - @app.route('/api/group_documents//share-with-group', methods=['POST']) + @bp.route('/api/group_documents//share-with-group', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1732,7 +1732,7 @@ def api_share_document_with_group(document_id): except Exception as e: return jsonify({'error': f'Error sharing document: {str(e)}'}), 500 - @app.route('/api/group_documents//unshare-with-group', methods=['DELETE']) + @bp.route('/api/group_documents//unshare-with-group', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1801,7 +1801,7 @@ def api_unshare_document_with_group(document_id): except Exception as e: return jsonify({'error': f'Error unsharing document: {str(e)}'}), 500 - @app.route('/api/group_documents//remove-self', methods=['DELETE']) + @bp.route('/api/group_documents//remove-self', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1878,7 +1878,7 @@ def api_remove_self_from_group_document(document_id): except Exception as e: return jsonify({'error': f'Error removing group from shared document: {str(e)}'}), 500 - @app.route('/api/group_documents/tags', methods=['GET']) + @bp.route('/api/group_documents/tags', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1925,7 +1925,7 @@ def api_get_group_document_tags(): merged = sorted(all_tags.values(), key=lambda t: t['name']) return jsonify({'tags': merged}), 200 - @app.route('/api/group_documents/tags', methods=['POST']) + @bp.route('/api/group_documents/tags', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1995,7 +1995,7 @@ def api_create_group_tag(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/group_documents/bulk-tag', methods=['POST']) + @bp.route('/api/group_documents/bulk-tag', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2118,7 +2118,7 @@ def api_bulk_tag_group_documents(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/group_documents/tags/', methods=['PATCH']) + @bp.route('/api/group_documents/tags/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -2241,7 +2241,7 @@ def api_update_group_tag(tag_name): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/group_documents/tags/', methods=['DELETE']) + @bp.route('/api/group_documents/tags/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_group_prompts.py b/application/single_app/route_backend_group_prompts.py index bd49da209..151aed979 100644 --- a/application/single_app/route_backend_group_prompts.py +++ b/application/single_app/route_backend_group_prompts.py @@ -1,6 +1,8 @@ # route_backend_group_prompts.py from config import * +from flask import current_app + from functions_authentication import * from functions_group import require_active_group from functions_settings import * @@ -22,8 +24,8 @@ def _get_active_group_or_error(user_id): return None, (jsonify({"error": "You are not a member of the active group"}), 403) -def register_route_backend_group_prompts(app): - @app.route('/api/group_prompts', methods=['GET']) +def register_route_backend_group_prompts(bp): + @bp.route('/api/group_prompts', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -48,10 +50,10 @@ def get_group_prompts(): "total_count": total }), 200 except Exception as e: - app.logger.error(f"Error fetching group prompts: {e}") + current_app.logger.error(f"Error fetching group prompts: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/group_prompts', methods=['POST']) + @bp.route('/api/group_prompts', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -78,10 +80,10 @@ def create_group_prompt(): ) return jsonify(result), 201 except Exception as e: - app.logger.error(f"Error creating group prompt: {e}") + current_app.logger.error(f"Error creating group prompt: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/group_prompts/', methods=['GET']) + @bp.route('/api/group_prompts/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -103,10 +105,10 @@ def get_group_prompt(prompt_id): return jsonify({"error":"Prompt not found or access denied"}), 404 return jsonify(item), 200 except Exception as e: - app.logger.error(f"Unexpected error getting group prompt {prompt_id}: {e}") + current_app.logger.error(f"Unexpected error getting group prompt {prompt_id}: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/group_prompts/', methods=['PATCH']) + @bp.route('/api/group_prompts/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -142,10 +144,10 @@ def update_group_prompt(prompt_id): return jsonify({"error":"Prompt not found or access denied"}), 404 return jsonify(result), 200 except Exception as e: - app.logger.error(f"Unexpected error updating group prompt {prompt_id}: {e}") + current_app.logger.error(f"Unexpected error updating group prompt {prompt_id}: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/group_prompts/', methods=['DELETE']) + @bp.route('/api/group_prompts/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -166,5 +168,5 @@ def delete_group_prompt(prompt_id): return jsonify({"error":"Prompt not found or access denied"}), 404 return jsonify({"message":"Prompt deleted successfully"}), 200 except Exception as e: - app.logger.error(f"Unexpected error deleting group prompt {prompt_id}: {e}") + current_app.logger.error(f"Unexpected error deleting group prompt {prompt_id}: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 diff --git a/application/single_app/route_backend_groups.py b/application/single_app/route_backend_groups.py index be8dadc93..627ebea11 100644 --- a/application/single_app/route_backend_groups.py +++ b/application/single_app/route_backend_groups.py @@ -30,12 +30,12 @@ ) from swagger_wrapper import swagger_route, get_auth_security -def register_route_backend_groups(app): +def register_route_backend_groups(bp): """ Register all group-related API endpoints under '/api/groups/...' """ - @app.route("/api/groups/discover", methods=["GET"]) + @bp.route("/api/groups/discover", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -84,7 +84,7 @@ def discover_groups(): return jsonify(results), 200 - @app.route("/api/groups", methods=["GET"]) + @bp.route("/api/groups", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -167,7 +167,7 @@ def api_list_groups(): return jsonify({"error": f"An error occurred while fetching your groups: {str(e)}"}), 500 - @app.route("/api/groups", methods=["POST"]) + @bp.route("/api/groups", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -192,7 +192,7 @@ def api_create_group(): except Exception as ex: return jsonify({"error": str(ex)}), 400 - @app.route("/api/groups/", methods=["GET"]) + @bp.route("/api/groups/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -233,7 +233,7 @@ def api_get_group_details(group_id): return jsonify(response_doc), 200 - @app.route("/api/groups//download-settings", methods=["PATCH"]) + @bp.route("/api/groups//download-settings", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -272,7 +272,7 @@ def api_update_group_download_settings(group_id): "disable_file_downloads": group_doc["disable_file_downloads"], }), 200 - @app.route("/api/groups/", methods=["DELETE"]) + @bp.route("/api/groups/", methods=["DELETE"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -297,7 +297,7 @@ def api_delete_group(group_id): delete_group(group_id) return jsonify({"message": "Group deleted successfully"}), 200 - @app.route("/api/groups/", methods=["PATCH", "PUT"]) + @bp.route("/api/groups/", methods=["PATCH", "PUT"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -339,7 +339,7 @@ def api_update_group(group_id): return jsonify({"message": "Group updated", "id": group_id}), 200 - @app.route("/api/groups//logo", methods=["GET"]) + @bp.route("/api/groups//logo", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -371,7 +371,7 @@ def api_get_group_logo(group_id): download_name="group-logo.png", ) - @app.route("/api/groups//logo", methods=["POST"]) + @bp.route("/api/groups//logo", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -417,7 +417,7 @@ def api_upload_group_logo(group_id): "logoVersion": group_doc["logoVersion"], }), 200 - @app.route("/api/groups/setActive", methods=["PATCH"]) + @bp.route("/api/groups/setActive", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -447,7 +447,7 @@ def api_set_active_group(): return jsonify({"message": f"Active group set to {group_id}"}), 200 - @app.route("/api/groups//requests", methods=["POST"]) + @bp.route("/api/groups//requests", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -485,7 +485,7 @@ def request_to_join(group_id): return jsonify({"message": "Membership request created"}), 201 - @app.route("/api/groups//requests", methods=["GET"]) + @bp.route("/api/groups//requests", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -509,7 +509,7 @@ def view_pending_requests(group_id): return jsonify(group_doc.get("pendingUsers", [])), 200 - @app.route("/api/groups//requests/", methods=["PATCH"]) + @bp.route("/api/groups//requests/", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -560,7 +560,7 @@ def approve_reject_request(group_id, request_id): return jsonify({"message": msg}), 200 - @app.route("/api/groups//members", methods=["POST"]) + @bp.route("/api/groups//members", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -588,7 +588,7 @@ def add_member_directly(group_id): except ValueError as exc: return jsonify({"error": str(exc)}), 400 - @app.route("/api/groups//members/", methods=["DELETE"]) + @bp.route("/api/groups//members/", methods=["DELETE"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -711,7 +711,7 @@ def remove_member(group_id, member_id): return jsonify({"error": "User not found in group"}), 404 - @app.route("/api/groups//members/", methods=["PATCH"]) + @bp.route("/api/groups//members/", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -813,7 +813,7 @@ def update_member_role(group_id, member_id): return jsonify({"message": f"User {member_id} updated to {new_role}"}), 200 - @app.route("/api/groups//members", methods=["GET"]) + @bp.route("/api/groups//members", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -865,7 +865,7 @@ def view_group_members(group_id): return jsonify(results), 200 - @app.route("/api/groups//transferOwnership", methods=["PATCH"]) + @bp.route("/api/groups//transferOwnership", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -938,7 +938,7 @@ def transfer_ownership(group_id): return jsonify({"message": "Ownership transferred successfully"}), 200 - @app.route("/api/groups//fileCount", methods=["GET"]) + @bp.route("/api/groups//fileCount", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -978,7 +978,7 @@ def get_group_file_count(group_id): return jsonify({ "fileCount": file_count }), 200 - @app.route("/api/groups//activity", methods=["GET"]) + @bp.route("/api/groups//activity", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1038,7 +1038,7 @@ def api_group_activity(group_id): return jsonify(activities), 200 - @app.route("/api/groups//stats", methods=["GET"]) + @bp.route("/api/groups//stats", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_models.py b/application/single_app/route_backend_models.py index 66533263c..6b7a7053f 100644 --- a/application/single_app/route_backend_models.py +++ b/application/single_app/route_backend_models.py @@ -33,7 +33,7 @@ def _is_foundry_project_endpoint(endpoint): return 'services.ai.azure.com' in (endpoint or '').lower() -def register_route_backend_models(app): +def register_route_backend_models(bp): """ Register backend routes for fetching Azure OpenAI models. """ @@ -207,15 +207,7 @@ def build_foundry_settings_from_endpoint(endpoint_cfg): } def resolve_foundry_scope(auth_settings): - management_cloud = (auth_settings.get("management_cloud") or "public").lower() - if management_cloud == "government": - return "https://ai.azure.us/.default" - if management_cloud == "custom": - custom_scope = (auth_settings.get("foundry_scope") or "").strip() - if not custom_scope: - raise ValueError("Foundry scope is required for custom cloud configurations.") - return custom_scope - return "https://ai.azure.com/.default" + return resolve_model_endpoint_foundry_scope(auth_settings) def build_foundry_token(auth_settings): management_cloud = (auth_settings.get("management_cloud") or "public").lower() @@ -264,17 +256,13 @@ def get_aoai_account_name(endpoint): return endpoint_host.split(".")[0].strip() def get_management_cloud_for_environment(): - if AZURE_ENVIRONMENT == "usgovernment": - return "government" - if AZURE_ENVIRONMENT == "custom": - return "custom" - return "public" + return get_model_endpoint_management_cloud_for_environment() def build_legacy_aoai_discovery_auth_settings(): return { "type": "service_principal", "management_cloud": get_management_cloud_for_environment(), - "custom_authority": authority if AZURE_ENVIRONMENT == "custom" else "", + "custom_authority": get_model_endpoint_default_custom_authority(), "tenant_id": TENANT_ID, "client_id": CLIENT_ID, "client_secret": MICROSOFT_PROVIDER_AUTHENTICATION_SECRET, @@ -537,7 +525,7 @@ def handle_test_model_connection(scope="global"): 400, ) - @app.route('/api/models/gpt', methods=['GET']) + @bp.route('/api/models/gpt', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -593,7 +581,7 @@ def get_gpt_models(): return jsonify({"models": models}) - @app.route('/api/models/embedding', methods=['GET']) + @bp.route('/api/models/embedding', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -647,7 +635,7 @@ def get_embedding_models(): return jsonify({"models": models}) - @app.route('/api/models/image', methods=['GET']) + @bp.route('/api/models/image', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -700,7 +688,7 @@ def get_image_models(): return jsonify({"models": models}) - @app.route('/api/models/test-connection', methods=['POST']) + @bp.route('/api/models/test-connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -783,7 +771,7 @@ def test_model_inference_connection(): ) - @app.route('/api/models/fetch', methods=['POST']) + @bp.route('/api/models/fetch', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -792,7 +780,7 @@ def fetch_model_list(): return handle_fetch_model_list(scope="global") - @app.route('/api/user/model-endpoints', methods=['GET']) + @bp.route('/api/user/model-endpoints', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -810,7 +798,7 @@ def get_user_model_endpoints(): }) - @app.route('/api/user/model-endpoints', methods=['POST']) + @bp.route('/api/user/model-endpoints', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -876,7 +864,7 @@ def save_user_model_endpoints(): return jsonify({"success": True}) - @app.route('/api/group/model-endpoints', methods=['GET']) + @bp.route('/api/group/model-endpoints', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -904,7 +892,7 @@ def get_group_model_endpoints_route(): }) - @app.route('/api/group/model-endpoints', methods=['POST']) + @bp.route('/api/group/model-endpoints', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -980,7 +968,7 @@ def save_group_model_endpoints(): return jsonify({"success": True}) - @app.route('/api/models/foundry/agents', methods=['POST']) + @bp.route('/api/models/foundry/agents', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1074,7 +1062,7 @@ def list_foundry_agents(): }) - @app.route('/api/models/test-model', methods=['POST']) + @bp.route('/api/models/test-model', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1083,7 +1071,7 @@ def test_model_connection(): return handle_test_model_connection(scope="global") - @app.route('/api/user/models/fetch', methods=['POST']) + @bp.route('/api/user/models/fetch', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1092,7 +1080,7 @@ def fetch_model_list_user(): return handle_fetch_model_list(scope="user") - @app.route('/api/user/models/test-model', methods=['POST']) + @bp.route('/api/user/models/test-model', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1101,7 +1089,7 @@ def test_model_connection_user(): return handle_test_model_connection(scope="user") - @app.route('/api/group/models/fetch', methods=['POST']) + @bp.route('/api/group/models/fetch', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1121,7 +1109,7 @@ def fetch_model_list_group(): return handle_fetch_model_list(scope="group") - @app.route('/api/group/models/test-model', methods=['POST']) + @bp.route('/api/group/models/test-model', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1138,4 +1126,4 @@ def test_model_connection_group(): return build_group_access_error_response(user_id, exc, "group model connection tests") except PermissionError as exc: return build_group_access_error_response(user_id, exc, "group model connection tests") - return handle_test_model_connection(scope="group") \ No newline at end of file + return handle_test_model_connection(scope="group") diff --git a/application/single_app/route_backend_msgraph_pending_actions.py b/application/single_app/route_backend_msgraph_pending_actions.py index 93a7d31b0..5032ec327 100644 --- a/application/single_app/route_backend_msgraph_pending_actions.py +++ b/application/single_app/route_backend_msgraph_pending_actions.py @@ -91,8 +91,8 @@ def _normalize_access_test_scopes(raw_scopes): return normalized_scopes, invalid_scopes -def register_route_backend_msgraph_pending_actions(app): - @app.route('/api/msgraph/test-access', methods=['POST']) +def register_route_backend_msgraph_pending_actions(bp): + @bp.route('/api/msgraph/test-access', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -132,7 +132,7 @@ def test_user_msgraph_access(): ) return _error_response(error_payload, default_status=401) - @app.route('/api/msgraph/pending-actions', methods=['GET']) + @bp.route('/api/msgraph/pending-actions', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -154,7 +154,7 @@ def list_user_msgraph_pending_actions(): }) - @app.route('/api/msgraph/pending-actions/', methods=['GET']) + @bp.route('/api/msgraph/pending-actions/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -166,7 +166,7 @@ def get_user_msgraph_pending_action(action_id): return jsonify(build_pending_action_response(action)) - @app.route('/api/msgraph/pending-actions//approve', methods=['POST']) + @bp.route('/api/msgraph/pending-actions//approve', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -188,7 +188,7 @@ def approve_user_msgraph_pending_action(action_id): return jsonify(build_pending_action_response(action)) - @app.route('/api/msgraph/pending-actions//send-now', methods=['POST']) + @bp.route('/api/msgraph/pending-actions//send-now', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -210,7 +210,7 @@ def send_now_user_msgraph_pending_action(action_id): return jsonify(build_pending_action_response(action)) - @app.route('/api/msgraph/pending-actions//cancel', methods=['POST']) + @bp.route('/api/msgraph/pending-actions//cancel', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -229,4 +229,4 @@ def cancel_user_msgraph_pending_action(action_id): if error: return _error_response(error) - return jsonify(build_pending_action_response(action)) \ No newline at end of file + return jsonify(build_pending_action_response(action)) diff --git a/application/single_app/route_backend_notifications.py b/application/single_app/route_backend_notifications.py index 5787ccb27..defd4305e 100644 --- a/application/single_app/route_backend_notifications.py +++ b/application/single_app/route_backend_notifications.py @@ -7,9 +7,9 @@ from swagger_wrapper import swagger_route, get_auth_security from functions_debug import debug_print -def register_route_backend_notifications(app): +def register_route_backend_notifications(bp): - @app.route("/api/notifications", methods=["GET"]) + @bp.route("/api/notifications", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -59,7 +59,7 @@ def api_get_notifications(): 'error': 'Failed to fetch notifications' }), 500 - @app.route("/api/notifications/count", methods=["GET"]) + @bp.route("/api/notifications/count", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -83,7 +83,7 @@ def api_get_notification_count(): 'count': 0 }), 500 - @app.route("/api/notifications/workflow-alerts", methods=["GET"]) + @bp.route("/api/notifications/workflow-alerts", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -107,7 +107,7 @@ def api_get_workflow_alert_notifications(): 'notifications': [], }), 500 - @app.route("/api/notifications//read", methods=["POST"]) + @bp.route("/api/notifications//read", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -137,7 +137,7 @@ def api_mark_notification_read(notification_id): 'error': 'Internal server error' }), 500 - @app.route("/api/notifications//dismiss", methods=["DELETE"]) + @bp.route("/api/notifications//dismiss", methods=["DELETE"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -167,7 +167,7 @@ def api_dismiss_notification(notification_id): 'error': 'Internal server error' }), 500 - @app.route("/api/notifications/mark-all-read", methods=["POST"]) + @bp.route("/api/notifications/mark-all-read", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -192,7 +192,7 @@ def api_mark_all_read(): 'error': 'Internal server error' }), 500 - @app.route("/api/notifications/settings", methods=["POST"]) + @bp.route("/api/notifications/settings", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_plugins.py b/application/single_app/route_backend_plugins.py index c8eaa0943..4e3d983e6 100644 --- a/application/single_app/route_backend_plugins.py +++ b/application/single_app/route_backend_plugins.py @@ -504,6 +504,7 @@ def get_plugin_types(allowed_type_filter=None): return jsonify(types) bpap = Blueprint('admin_plugins', __name__) +bpap.before_request(login_required_blueprint()) def _redact_plugin_for_logging(plugin): @@ -1625,6 +1626,7 @@ def discover_mcp_tools(): # Dynamic Plugin Metadata Endpoint bpdp = Blueprint('dynamic_plugins', __name__) +bpdp.before_request(admin_required_blueprint()) @bpdp.route('/api/admin/plugins/dynamic', methods=['GET']) @swagger_route(security=get_auth_security()) diff --git a/application/single_app/route_backend_prompts.py b/application/single_app/route_backend_prompts.py index 9bbe254e1..aae661ebb 100644 --- a/application/single_app/route_backend_prompts.py +++ b/application/single_app/route_backend_prompts.py @@ -1,13 +1,15 @@ # route_backend_prompts.py from config import * +from flask import current_app + from functions_authentication import * from functions_settings import * from functions_prompts import * from swagger_wrapper import swagger_route, get_auth_security -def register_route_backend_prompts(app): - @app.route('/api/prompts', methods=['GET']) +def register_route_backend_prompts(bp): + @bp.route('/api/prompts', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -27,10 +29,10 @@ def get_prompts(): "total_count": total }), 200 except Exception as e: - app.logger.error(f"Error fetching prompts: {e}") + current_app.logger.error(f"Error fetching prompts: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/prompts', methods=['POST']) + @bp.route('/api/prompts', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -54,10 +56,10 @@ def create_prompt(): ) return jsonify(result), 201 except Exception as e: - app.logger.error(f"Error creating prompt: {e}") + current_app.logger.error(f"Error creating prompt: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/prompts/', methods=['GET']) + @bp.route('/api/prompts/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -74,10 +76,10 @@ def get_prompt(prompt_id): return jsonify({"error":"Prompt not found or access denied"}), 404 return jsonify(item), 200 except Exception as e: - app.logger.error(f"Unexpected error getting prompt {prompt_id}: {e}") + current_app.logger.error(f"Unexpected error getting prompt {prompt_id}: {e}") return jsonify({"error": "An unexpected error occurred"}), 500 - @app.route('/api/prompts/', methods=['PATCH']) + @bp.route('/api/prompts/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -108,10 +110,10 @@ def update_prompt(prompt_id): return jsonify({"error":"Prompt not found or access denied"}), 404 return jsonify(result), 200 except Exception as e: - app.logger.error(f"Unexpected error updating prompt {prompt_id}: {e}") + current_app.logger.error(f"Unexpected error updating prompt {prompt_id}: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 - @app.route('/api/prompts/', methods=['DELETE']) + @bp.route('/api/prompts/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -127,5 +129,5 @@ def delete_prompt(prompt_id): return jsonify({"error":"Prompt not found or access denied"}), 404 return jsonify({"message":"Prompt deleted successfully"}), 200 except Exception as e: - app.logger.error(f"Unexpected error deleting prompt {prompt_id}: {e}") + current_app.logger.error(f"Unexpected error deleting prompt {prompt_id}: {e}") return jsonify({"error":"An unexpected error occurred"}), 500 diff --git a/application/single_app/route_backend_public_documents.py b/application/single_app/route_backend_public_documents.py index bd2b23a0b..a39ba286b 100644 --- a/application/single_app/route_backend_public_documents.py +++ b/application/single_app/route_backend_public_documents.py @@ -65,12 +65,12 @@ def _require_active_public_workspace_response(user_id, allowed_roles=PUBLIC_WORK return active_ws, ws_doc, role, None -def register_route_backend_public_documents(app): +def register_route_backend_public_documents(bp): """ Provides backend routes for public-workspace–scoped document management """ - @app.route('/api/public_documents/upload', methods=['POST']) + @bp.route('/api/public_documents/upload', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -158,7 +158,7 @@ def api_upload_public_document(): 'errors': errors }), status - @app.route('/api/public_documents', methods=['GET']) + @bp.route('/api/public_documents', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -280,7 +280,7 @@ def api_list_public_documents(): 'needs_legacy_update': legacy_count > 0 }), 200 - @app.route('/api/public_workspace_documents', methods=['GET']) + @bp.route('/api/public_workspace_documents', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -334,7 +334,7 @@ def api_list_public_workspace_documents(): 'workspace_name': 'All Public Workspaces' }), 200 - @app.route('/api/public_documents/', methods=['GET']) + @bp.route('/api/public_documents/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -349,7 +349,7 @@ def api_get_public_document(doc_id): return error_response return get_document(user_id=user_id, document_id=doc_id, public_workspace_id=active_ws) - @app.route('/api/public_workspace_documents//versions', methods=['GET']) + @bp.route('/api/public_workspace_documents//versions', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -411,7 +411,7 @@ def _authorize_public_document_download(user_id, document_id): return None, None, (jsonify({'error': 'Document not found or access denied'}), 404) return active_ws, document_record, None - @app.route('/api/public_documents//download', methods=['GET']) + @bp.route('/api/public_documents//download', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -437,7 +437,7 @@ def api_download_public_document(doc_id): ) return jsonify({'error': 'Unable to download document'}), 500 - @app.route('/api/public_documents/download', methods=['POST']) + @bp.route('/api/public_documents/download', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -495,7 +495,7 @@ def api_download_public_documents(): ) return jsonify({'error': 'Unable to download selected documents'}), 500 - @app.route('/api/public_documents/', methods=['PATCH']) + @bp.route('/api/public_documents/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -565,7 +565,7 @@ def api_patch_public_document(doc_id): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/public_documents/', methods=['DELETE']) + @bp.route('/api/public_documents/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -619,7 +619,7 @@ def api_delete_public_document(doc_id): except Exception as e: return jsonify({'error':str(e)}), 500 - @app.route('/api/public_documents//approve-generated-artifact', methods=['POST']) + @bp.route('/api/public_documents//approve-generated-artifact', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -727,7 +727,7 @@ def api_approve_public_generated_artifact(doc_id): except Exception as e: return jsonify({'error': f'Error approving generated artifact: {str(e)}'}), 500 - @app.route('/api/public_documents//deny-generated-artifact', methods=['POST']) + @bp.route('/api/public_documents//deny-generated-artifact', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -800,7 +800,7 @@ def api_deny_public_generated_artifact(doc_id): except Exception as e: return jsonify({'error': f'Error denying generated artifact: {str(e)}'}), 500 - @app.route('/api/public_documents//cancel-generated-artifact', methods=['POST']) + @bp.route('/api/public_documents//cancel-generated-artifact', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -852,7 +852,7 @@ def api_cancel_public_generated_artifact(doc_id): except Exception as e: return jsonify({'error': f'Error canceling generated artifact: {str(e)}'}), 500 - @app.route('/api/public_documents//extract_metadata', methods=['POST']) + @bp.route('/api/public_documents//extract_metadata', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -872,7 +872,7 @@ def api_extract_metadata_public_document(doc_id): executor.submit(process_metadata_extraction_background, document_id=doc_id, user_id=user_id, public_workspace_id=active_ws) return jsonify({'message':'Extraction queued'}), 200 - @app.route('/api/public_documents/reprocess_extraction', methods=['POST']) + @bp.route('/api/public_documents/reprocess_extraction', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -960,7 +960,7 @@ def api_reprocess_public_document_extraction(): 'errors': errors, }), status_code - @app.route('/api/public_documents/upgrade_legacy', methods=['POST']) + @bp.route('/api/public_documents/upgrade_legacy', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -979,7 +979,7 @@ def api_upgrade_legacy_public_documents(): except Exception as e: return jsonify({'error':str(e)}), 500 - @app.route('/api/public_workspace_documents/tags', methods=['GET']) + @bp.route('/api/public_workspace_documents/tags', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1019,7 +1019,7 @@ def api_get_public_workspace_document_tags(): merged = sorted(all_tags.values(), key=lambda t: t['name']) return jsonify({'tags': merged}), 200 - @app.route('/api/public_workspace_documents/tags', methods=['POST']) + @bp.route('/api/public_workspace_documents/tags', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1090,7 +1090,7 @@ def api_create_public_workspace_tag(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/public_workspace_documents/bulk-tag', methods=['POST']) + @bp.route('/api/public_workspace_documents/bulk-tag', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1214,7 +1214,7 @@ def api_bulk_tag_public_documents(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/public_workspace_documents/tags/', methods=['PATCH']) + @bp.route('/api/public_workspace_documents/tags/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1338,7 +1338,7 @@ def api_update_public_workspace_tag(tag_name): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/public_workspace_documents/tags/', methods=['DELETE']) + @bp.route('/api/public_workspace_documents/tags/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_public_prompts.py b/application/single_app/route_backend_public_prompts.py index 712adcbb9..01dc46db7 100644 --- a/application/single_app/route_backend_public_prompts.py +++ b/application/single_app/route_backend_public_prompts.py @@ -1,6 +1,7 @@ # route_backend_public_prompts.py from config import * +from flask import current_app from functions_authentication import * from functions_settings import * @@ -25,12 +26,12 @@ def _get_active_public_workspace_or_error( except PermissionError: return None, (jsonify({'error': 'Access denied'}), 403) -def register_route_backend_public_prompts(app): +def register_route_backend_public_prompts(bp): """ Backend routes for public-workspace–scoped prompts management """ - @app.route('/api/public_prompts', methods=['GET']) + @bp.route('/api/public_prompts', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -56,10 +57,10 @@ def api_list_public_prompts(): 'total_count': total }), 200 except Exception as e: - app.logger.error(f"Error listing public prompts: {e}") + current_app.logger.error(f"Error listing public prompts: {e}") return jsonify({'error':'Unexpected error'}), 500 - @app.route('/api/public_prompts', methods=['POST']) + @bp.route('/api/public_prompts', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -90,10 +91,10 @@ def api_create_public_prompt(): ) return jsonify(result), 201 except Exception as e: - app.logger.error(f"Error creating public prompt: {e}") + current_app.logger.error(f"Error creating public prompt: {e}") return jsonify({'error':'Unexpected error'}), 500 - @app.route('/api/public_prompts/', methods=['GET']) + @bp.route('/api/public_prompts/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -116,10 +117,10 @@ def api_get_public_prompt(prompt_id): return jsonify({'error':'Not found'}), 404 return jsonify(item), 200 except Exception as e: - app.logger.error(f"Error fetching public prompt {prompt_id}: {e}") + current_app.logger.error(f"Error fetching public prompt {prompt_id}: {e}") return jsonify({'error':'Unexpected error'}), 500 - @app.route('/api/public_prompts/', methods=['PATCH']) + @bp.route('/api/public_prompts/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -159,10 +160,10 @@ def api_update_public_prompt(prompt_id): return jsonify({'error':'Not found'}), 404 return jsonify(result), 200 except Exception as e: - app.logger.error(f"Error updating public prompt {prompt_id}: {e}") + current_app.logger.error(f"Error updating public prompt {prompt_id}: {e}") return jsonify({'error':'Unexpected error'}), 500 - @app.route('/api/public_prompts/', methods=['DELETE']) + @bp.route('/api/public_prompts/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -187,5 +188,5 @@ def api_delete_public_prompt(prompt_id): return jsonify({'error':'Not found'}), 404 return jsonify({'message':'Deleted'}), 200 except Exception as e: - app.logger.error(f"Error deleting public prompt {prompt_id}: {e}") + current_app.logger.error(f"Error deleting public prompt {prompt_id}: {e}") return jsonify({'error':'Unexpected error'}), 500 diff --git a/application/single_app/route_backend_public_workspaces.py b/application/single_app/route_backend_public_workspaces.py index 85bde2aa6..8aba7bdf4 100644 --- a/application/single_app/route_backend_public_workspaces.py +++ b/application/single_app/route_backend_public_workspaces.py @@ -90,12 +90,12 @@ def get_user_details_from_graph(user_id): print(f"Failed to get user details for {user_id}: {e}") return {"displayName": "", "email": ""} -def register_route_backend_public_workspaces(app): +def register_route_backend_public_workspaces(bp): """ Register all public-workspace–related API endpoints under '/api/public_workspaces/...' """ - @app.route("/api/public_workspaces/discover", methods=["GET"]) + @bp.route("/api/public_workspaces/discover", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -125,7 +125,7 @@ def discover_public_workspaces(): return jsonify(results), 200 - @app.route("/api/public_workspaces", methods=["GET"]) + @bp.route("/api/public_workspaces", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -169,9 +169,13 @@ def api_list_public_workspaces(): total_count = len(all_ws) slice_ws = all_ws[offset: offset + page_size] - # get active from user settings - settings = get_user_settings(user_id) - active_id = settings["settings"].get("activePublicWorkspaceOid", "") + try: + active_id, _, _ = require_active_public_workspace( + user_id, + allowed_roles=("Owner", "Admin", "DocumentManager", "User"), + ) + except (ValueError, LookupError, PermissionError): + active_id = "" mapped = [] app_settings = get_settings() @@ -208,7 +212,7 @@ def api_list_public_workspaces(): "total_count": total_count }), 200 - @app.route("/api/public_workspaces", methods=["POST"]) + @bp.route("/api/public_workspaces", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -229,7 +233,7 @@ def api_create_public_workspace(): except Exception as ex: return jsonify({"error": str(ex)}), 400 - @app.route("/api/public_workspaces/", methods=["GET"]) + @bp.route("/api/public_workspaces/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -259,7 +263,7 @@ def api_get_public_workspace(ws_id): return jsonify(build_public_workspace_public_summary(ws)), 200 - @app.route("/api/public_workspaces//download-settings", methods=["PATCH"]) + @bp.route("/api/public_workspaces//download-settings", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -296,7 +300,7 @@ def api_update_public_workspace_download_settings(ws_id): "disable_file_downloads": ws["disable_file_downloads"], }), 200 - @app.route("/api/public_workspaces/", methods=["PATCH", "PUT"]) + @bp.route("/api/public_workspaces/", methods=["PATCH", "PUT"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -330,7 +334,7 @@ def api_update_public_workspace(ws_id): except exceptions.CosmosHttpResponseError as ex: return jsonify({"error": str(ex)}), 400 - @app.route("/api/public_workspaces//logo", methods=["GET"]) + @bp.route("/api/public_workspaces//logo", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -356,7 +360,7 @@ def api_get_public_workspace_logo(ws_id): download_name="workspace-logo.png", ) - @app.route("/api/public_workspaces//logo", methods=["POST"]) + @bp.route("/api/public_workspaces//logo", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -401,7 +405,7 @@ def api_upload_public_workspace_logo(ws_id): "logoVersion": ws["logoVersion"], }), 200 - @app.route("/api/public_workspaces/", methods=["DELETE"]) + @bp.route("/api/public_workspaces/", methods=["DELETE"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -423,7 +427,7 @@ def api_delete_public_workspace(ws_id): delete_public_workspace(ws_id) return jsonify({"message": "Deleted"}), 200 - @app.route("/api/public_workspaces/setActive", methods=["PATCH"]) + @bp.route("/api/public_workspaces/setActive", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -448,7 +452,7 @@ def api_set_active_public_workspace(): return jsonify({"message": f"Active set to {ws_id}"}), 200 - @app.route("/api/public_workspaces//requests", methods=["GET"]) + @bp.route("/api/public_workspaces//requests", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -475,7 +479,7 @@ def api_view_public_requests(ws_id): return jsonify(ws.get("pendingDocumentManagers", [])), 200 - @app.route("/api/public_workspaces//requests", methods=["POST"]) + @bp.route("/api/public_workspaces//requests", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -509,7 +513,7 @@ def api_request_public_workspace(ws_id): cosmos_public_workspaces_container.upsert_item(ws) return jsonify({"message": "Requested"}), 201 - @app.route("/api/public_workspaces//requests/", methods=["PATCH"]) + @bp.route("/api/public_workspaces//requests/", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -556,7 +560,7 @@ def api_handle_public_request(ws_id, req_id): cosmos_public_workspaces_container.upsert_item(ws) return jsonify({"message": msg}), 200 - @app.route("/api/public_workspaces//members", methods=["GET"]) + @bp.route("/api/public_workspaces//members", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -633,7 +637,7 @@ def keep(m): return jsonify([m for m in results if keep(m)]), 200 - @app.route("/api/public_workspaces//members", methods=["POST"]) + @bp.route("/api/public_workspaces//members", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -696,7 +700,7 @@ def api_add_public_member(ws_id): return jsonify({"message": "Member added"}), 200 - @app.route("/api/public_workspaces//members/", methods=["DELETE"]) + @bp.route("/api/public_workspaces//members/", methods=["DELETE"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -738,7 +742,7 @@ def api_remove_public_member(ws_id, member_id): cosmos_public_workspaces_container.upsert_item(ws) return jsonify({"success": True, "message": "Removed"}), 200 - @app.route("/api/public_workspaces//members/", methods=["PATCH"]) + @bp.route("/api/public_workspaces//members/", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -848,7 +852,7 @@ def api_update_public_member_role(ws_id, member_id): return jsonify({"success": True, "message": "Role updated"}), 200 - @app.route("/api/public_workspaces//transferOwnership", methods=["PATCH"]) + @bp.route("/api/public_workspaces//transferOwnership", methods=["PATCH"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -914,7 +918,7 @@ def api_transfer_public_ownership(ws_id): cosmos_public_workspaces_container.upsert_item(ws) return jsonify({"message": "Ownership transferred"}), 200 - @app.route("/api/public_workspaces//fileCount", methods=["GET"]) + @bp.route("/api/public_workspaces//fileCount", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -942,7 +946,7 @@ def api_public_file_count(ws_id): file_count = next(count_iter, 0) return jsonify({"fileCount": file_count}), 200 - @app.route("/api/public_workspaces//promptCount", methods=["GET"]) + @bp.route("/api/public_workspaces//promptCount", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -959,7 +963,7 @@ def api_public_prompt_count(ws_id): prompt_count = count_public_prompts_for_workspace(ws_id) return jsonify({"promptCount": prompt_count}), 200 - @app.route("/api/public_workspaces//stats", methods=["GET"]) + @bp.route("/api/public_workspaces//stats", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1195,7 +1199,7 @@ def api_public_workspace_stats(ws_id): return jsonify(stats), 200 - @app.route("/api/public_workspaces//activity", methods=["GET"]) + @bp.route("/api/public_workspaces//activity", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_retention_policy.py b/application/single_app/route_backend_retention_policy.py index 493207559..ecc43d72d 100644 --- a/application/single_app/route_backend_retention_policy.py +++ b/application/single_app/route_backend_retention_policy.py @@ -9,9 +9,9 @@ from functions_debug import debug_print -def register_route_backend_retention_policy(app): +def register_route_backend_retention_policy(bp): - @app.route('/api/admin/retention-policy/settings', methods=['GET']) + @bp.route('/api/admin/retention-policy/settings', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -47,7 +47,7 @@ def get_retention_policy_settings(): }), 500 - @app.route('/api/admin/retention-policy/settings', methods=['POST']) + @bp.route('/api/admin/retention-policy/settings', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -107,7 +107,7 @@ def update_retention_policy_settings(): }), 500 - @app.route('/api/retention-policy/defaults/', methods=['GET']) + @bp.route('/api/retention-policy/defaults/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -176,7 +176,7 @@ def get_retention_label(value): }), 500 - @app.route('/api/admin/retention-policy/execute', methods=['POST']) + @bp.route('/api/admin/retention-policy/execute', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -225,7 +225,7 @@ def manual_execute_retention_policy(): }), 500 - @app.route('/api/admin/retention-policy/force-push', methods=['POST']) + @bp.route('/api/admin/retention-policy/force-push', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -385,7 +385,7 @@ def force_push_retention_defaults(): }), 500 - @app.route('/api/retention-policy/user', methods=['POST']) + @bp.route('/api/retention-policy/user', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -476,7 +476,7 @@ def update_user_retention_settings(): }), 500 - @app.route('/api/retention-policy/group/', methods=['POST']) + @bp.route('/api/retention-policy/group/', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -586,7 +586,7 @@ def update_group_retention_settings(group_id): }), 500 - @app.route('/api/retention-policy/public/', methods=['POST']) + @bp.route('/api/retention-policy/public/', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_safety.py b/application/single_app/route_backend_safety.py index 5b1c5be4b..844402959 100644 --- a/application/single_app/route_backend_safety.py +++ b/application/single_app/route_backend_safety.py @@ -271,8 +271,8 @@ def _build_safety_export_response(logs, filename_prefix, include_user_id=False): ) return response -def register_route_backend_safety(app): - @app.route('/api/safety/logs', methods=['GET']) +def register_route_backend_safety(bp): + @bp.route('/api/safety/logs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @safety_violation_admin_required @@ -308,7 +308,7 @@ def get_safety_logs(): # Consider using Flask's logging mechanism return jsonify({"error": f"An error occurred while fetching safety logs: {str(e)}"}), 500 - @app.route('/api/safety/logs/stats', methods=['GET']) + @bp.route('/api/safety/logs/stats', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @safety_violation_admin_required @@ -325,7 +325,7 @@ def get_safety_log_stats(): except Exception as e: return jsonify({"error": f"Failed to retrieve safety stats: {str(e)}"}), 500 - @app.route('/api/safety/logs/export', methods=['GET']) + @bp.route('/api/safety/logs/export', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @safety_violation_admin_required @@ -342,7 +342,7 @@ def export_safety_logs(): except Exception as e: return jsonify({"error": f"Failed to export safety logs: {str(e)}"}), 500 - @app.route('/api/safety/logs/', methods=['PATCH']) + @bp.route('/api/safety/logs/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @safety_violation_admin_required @@ -490,7 +490,7 @@ def update_safety_log(log_id): }, level=logging.ERROR) return jsonify({'error': f'Failed to update safety log: {str(e)}'}), 500 - @app.route('/api/safety/logs/my', methods=['GET']) + @bp.route('/api/safety/logs/my', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -530,7 +530,7 @@ def get_my_safety_logs(): print(f"Error in get_my_safety_logs: {str(e)}") return jsonify({"error": f"An error occurred while fetching your safety logs: {str(e)}"}), 500 - @app.route('/api/safety/logs/my/stats', methods=['GET']) + @bp.route('/api/safety/logs/my/stats', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -552,7 +552,7 @@ def get_my_safety_log_stats(): except Exception as e: return jsonify({"error": f"Failed to retrieve safety stats: {str(e)}"}), 500 - @app.route('/api/safety/logs/my/export', methods=['GET']) + @bp.route('/api/safety/logs/my/export', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -574,7 +574,7 @@ def export_my_safety_logs(): except Exception as e: return jsonify({"error": f"Failed to export safety logs: {str(e)}"}), 500 - @app.route('/api/safety/logs/my/', methods=['PATCH']) + @bp.route('/api/safety/logs/my/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -610,4 +610,4 @@ def update_my_safety_log(log_id): return jsonify({"message": "Safety log updated successfully."}), 200 except exceptions.CosmosHttpResponseError as e: - return jsonify({"error": str(e)}), 404 \ No newline at end of file + return jsonify({"error": str(e)}), 404 diff --git a/application/single_app/route_backend_search.py b/application/single_app/route_backend_search.py index 5a5a98bc1..be350964e 100644 --- a/application/single_app/route_backend_search.py +++ b/application/single_app/route_backend_search.py @@ -11,8 +11,8 @@ from swagger_wrapper import swagger_route, get_auth_security -def register_route_backend_search(app): - @app.route('/api/search/documents', methods=['POST']) +def register_route_backend_search(bp): + @bp.route('/api/search/documents', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -47,7 +47,7 @@ def api_search_documents(): ) return jsonify({'error': 'Document search failed'}), 500 - @app.route('/api/search/document-chunks', methods=['POST']) + @bp.route('/api/search/document-chunks', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -88,7 +88,7 @@ def api_get_document_chunks(): ) return jsonify({'error': 'Document chunk retrieval failed'}), 500 - @app.route('/api/search/document-summary', methods=['POST']) + @bp.route('/api/search/document-summary', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -133,4 +133,4 @@ def api_summarize_document(): }, level=logging.ERROR, ) - return jsonify({'error': 'Document summarization failed'}), 500 \ No newline at end of file + return jsonify({'error': 'Document summarization failed'}), 500 diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py index 14c94af5f..4a704891c 100644 --- a/application/single_app/route_backend_settings.py +++ b/application/single_app/route_backend_settings.py @@ -188,8 +188,8 @@ def auto_fix_index_fields(idx_type: str, user_id: str = 'system', admin_email: s return {'error': str(e)} -def register_route_backend_settings(app): - @app.route('/api/admin/settings/check_index_fields', methods=['POST']) +def register_route_backend_settings(bp): + @bp.route('/api/admin/settings/check_index_fields', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -297,18 +297,18 @@ def check_index_fields(): 'needsCreation': True }), 404 else: - app.logger.error(f"Azure AI Search error: {search_error}") + current_app.logger.error(f"Azure AI Search error: {search_error}") return jsonify({ 'error': f'Failed to connect to Azure AI Search: {str(search_error)}', 'needsConfiguration': True }), 500 except Exception as e: - app.logger.error(f"Error in check_index_fields: {str(e)}") + current_app.logger.error(f"Error in check_index_fields: {str(e)}") return jsonify({'error': f'Unexpected error: {str(e)}'}), 500 - @app.route('/api/admin/settings/fix_index_fields', methods=['POST']) + @bp.route('/api/admin/settings/fix_index_fields', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -388,7 +388,7 @@ def fix_index_fields(): except Exception as e: return jsonify({ 'error': str(e) }), 500 - @app.route('/api/admin/settings/create_index', methods=['POST']) + @bp.route('/api/admin/settings/create_index', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -436,7 +436,7 @@ def create_index(): pass except Exception as e: # Other errors checking if index exists - app.logger.error(f"Error checking if index exists: {e}") + current_app.logger.error(f"Error checking if index exists: {e}") # Continue with creation attempt anyway # Create the index using the JSON definition @@ -454,10 +454,10 @@ def create_index(): }), 200 except Exception as e: - app.logger.error(f"Error creating index: {str(e)}") + current_app.logger.error(f"Error creating index: {str(e)}") return jsonify({'error': f'Failed to create index: {str(e)}'}), 500 - @app.route('/api/admin/settings/test_connection', methods=['POST']) + @bp.route('/api/admin/settings/test_connection', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -518,7 +518,7 @@ def test_connection(): except Exception as e: return jsonify({'error': str(e)}), 500 - @app.route('/api/admin/settings/cosmos-throughput/status', methods=['GET']) + @bp.route('/api/admin/settings/cosmos-throughput/status', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -560,7 +560,7 @@ def get_cosmos_throughput_admin_status(): ) return jsonify({'error': 'Failed to load Cosmos throughput status.'}), 500 - @app.route('/api/admin/settings/cosmos-throughput/validate-access', methods=['POST']) + @bp.route('/api/admin/settings/cosmos-throughput/validate-access', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -617,7 +617,7 @@ def validate_cosmos_throughput_admin_access(): ) return jsonify({'error': 'Failed to validate Cosmos throughput access.'}), 500 - @app.route('/api/admin/settings/cosmos-throughput/scale', methods=['POST']) + @bp.route('/api/admin/settings/cosmos-throughput/scale', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -685,7 +685,7 @@ def scale_cosmos_throughput_admin(): ) return jsonify({'error': 'Failed to scale Cosmos throughput.'}), 500 - @app.route('/api/admin/settings/cosmos-throughput/convert-autoscale', methods=['POST']) + @bp.route('/api/admin/settings/cosmos-throughput/convert-autoscale', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -765,7 +765,7 @@ def convert_cosmos_throughput_to_autoscale_admin(): ) return jsonify({'error': 'Cosmos throughput mode conversion failed.'}), 500 - @app.route('/api/admin/settings/send_feedback_email', methods=['POST']) + @bp.route('/api/admin/settings/send_feedback_email', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -828,7 +828,7 @@ def send_feedback_email(): ) return jsonify({'error': 'Failed to prepare feedback email'}), 500 - @app.route('/api/support/send_feedback_email', methods=['POST']) + @bp.route('/api/support/send_feedback_email', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -904,7 +904,7 @@ def send_support_feedback_email(): ) return jsonify({'error': 'Failed to prepare feedback email'}), 500 - @app.route('/api/admin/settings/release_notifications_registration', methods=['POST']) + @bp.route('/api/admin/settings/release_notifications_registration', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -1630,4 +1630,4 @@ def _test_key_vault_connection(payload): except Exception as e: log_event(f"[AKV_TEST] Key Vault connection error: {str(e)}", level="error") - return jsonify({'error': 'Key Vault connection failed. Check Application Insights using "[AKV_TEST]" for details.'}), 500 \ No newline at end of file + return jsonify({'error': 'Key Vault connection failed. Check Application Insights using "[AKV_TEST]" for details.'}), 500 diff --git a/application/single_app/route_backend_speech.py b/application/single_app/route_backend_speech.py index b50351a02..23c886e20 100644 --- a/application/single_app/route_backend_speech.py +++ b/application/single_app/route_backend_speech.py @@ -18,10 +18,10 @@ PYDUB_AVAILABLE = False print("Warning: pydub not available. Audio conversion may fail for non-WAV formats.") -def register_route_backend_speech(app): +def register_route_backend_speech(bp): """Register speech-to-text routes""" - @app.route('/api/speech/transcribe-chat', methods=['POST']) + @bp.route('/api/speech/transcribe-chat', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_thoughts.py b/application/single_app/route_backend_thoughts.py index 441e6db9c..cf24484bd 100644 --- a/application/single_app/route_backend_thoughts.py +++ b/application/single_app/route_backend_thoughts.py @@ -15,9 +15,9 @@ from functions_appinsights import log_event -def register_route_backend_thoughts(app): +def register_route_backend_thoughts(bp): - @app.route('/api/conversations//messages//thoughts', methods=['GET']) + @bp.route('/api/conversations//messages//thoughts', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -72,7 +72,7 @@ def api_get_message_thoughts(conversation_id, message_id): log_event(f"api_get_message_thoughts error: {e}", level="WARNING") return jsonify({'error': 'Failed to retrieve thoughts'}), 500 - @app.route('/api/conversations//thoughts/pending', methods=['GET']) + @bp.route('/api/conversations//thoughts/pending', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_tts.py b/application/single_app/route_backend_tts.py index 3c2e271d7..9e30420dd 100644 --- a/application/single_app/route_backend_tts.py +++ b/application/single_app/route_backend_tts.py @@ -266,12 +266,12 @@ def _build_tts_synthesizer(settings, speech_endpoint, speech_region, voice): return speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None) -def register_route_backend_tts(app): +def register_route_backend_tts(bp): """ Text-to-speech API routes using Azure Speech Services """ - @app.route("/api/chat/tts", methods=["POST"]) + @bp.route("/api/chat/tts", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -505,7 +505,7 @@ def synthesize_speech(): log_event("[TTS] Speech synthesis failed.", extra={"error": str(e)}, level=logging.ERROR, exceptionTraceback=True) return jsonify({"error": TTS_SYNTHESIS_ERROR_MESSAGE}), 500 - @app.route("/api/chat/tts/voices", methods=["GET"]) + @bp.route("/api/chat/tts/voices", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_user_agreement.py b/application/single_app/route_backend_user_agreement.py index b76213b36..83f34d73b 100644 --- a/application/single_app/route_backend_user_agreement.py +++ b/application/single_app/route_backend_user_agreement.py @@ -9,13 +9,13 @@ from functions_debug import debug_print -def register_route_backend_user_agreement(app): +def register_route_backend_user_agreement(bp): """ Register user agreement API endpoints under '/api/user_agreement/...' These endpoints handle checking and recording user agreement acceptance. """ - @app.route("/api/user_agreement/check", methods=["GET"]) + @bp.route("/api/user_agreement/check", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -99,7 +99,7 @@ def api_check_user_agreement(): "enableDailyAcceptance": enable_daily_acceptance }), 200 - @app.route("/api/user_agreement/accept", methods=["POST"]) + @bp.route("/api/user_agreement/accept", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_users.py b/application/single_app/route_backend_users.py index 66fbc7136..6e8c0b631 100644 --- a/application/single_app/route_backend_users.py +++ b/application/single_app/route_backend_users.py @@ -247,13 +247,13 @@ def _user_profile_not_found_response(): return jsonify({'error': 'User not found or access denied'}), 404 -def register_route_backend_users(app): +def register_route_backend_users(bp): """ This route will expose GET /api/userSearch?query= which calls Microsoft Graph to find users by displayName, mail, userPrincipalName, etc. """ - @app.route("/api/userSearch", methods=["GET"]) + @bp.route("/api/userSearch", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -315,7 +315,7 @@ def api_user_search(): "details": error_details }), getattr(e.response, 'status_code', 500) # Use response status code if available - @app.route("/api/user/info/", methods=["GET"]) + @bp.route("/api/user/info/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -368,7 +368,7 @@ def api_get_user_info(user_id): return _user_profile_not_found_response() - @app.route('/api/user/collaboration-suggestions', methods=['GET']) + @bp.route('/api/user/collaboration-suggestions', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -448,7 +448,7 @@ def add_suggestion(raw_value, source_label): return jsonify({'results': suggestions[:limit]}), 200 - @app.route('/api/user/settings', methods=['GET', 'POST']) + @bp.route('/api/user/settings', methods=['GET', 'POST']) @swagger_route(security=get_auth_security()) @login_required @user_required # Assuming this decorator confirms a valid user exists @@ -618,7 +618,7 @@ def user_settings(): print(f"Error retrieving settings for user {user_id}: {e}") return jsonify({"error": "Failed to retrieve user settings"}), 500 - @app.route('/api/user/profile-image/', methods=['GET']) + @bp.route('/api/user/profile-image/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_backend_workflows.py b/application/single_app/route_backend_workflows.py index 58ff766b8..3dce1a639 100644 --- a/application/single_app/route_backend_workflows.py +++ b/application/single_app/route_backend_workflows.py @@ -594,8 +594,8 @@ def _stream_group_workflow_activity(user_id, group_id, conversation_id='', workf time.sleep(0.5) -def register_route_backend_workflows(app): - @app.route('/api/workflows/draft-instructions', methods=['POST']) +def register_route_backend_workflows(bp): + @bp.route('/api/workflows/draft-instructions', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -660,8 +660,7 @@ def draft_workflow_instructions(): ) return jsonify({'error': 'Failed to draft workflow instructions.'}), 500 - - @app.route('/api/user/workflows', methods=['GET']) + @bp.route('/api/user/workflows', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -672,7 +671,7 @@ def get_user_workflows(): return jsonify({'workflows': get_personal_workflows(user_id)}) - @app.route('/api/user/workflows/file-sync-sources', methods=['GET']) + @bp.route('/api/user/workflows/file-sync-sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -692,7 +691,7 @@ def get_user_workflow_file_sync_sources(): return jsonify({'error': 'Unable to load File Sync sources right now.'}), 500 - @app.route('/api/user/workflows', methods=['POST']) + @bp.route('/api/user/workflows', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -739,7 +738,7 @@ def save_user_workflow(): return jsonify({'success': True, 'workflow': workflow}), 201 if is_create else 200 - @app.route('/api/user/workflows/', methods=['DELETE']) + @bp.route('/api/user/workflows/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -763,7 +762,7 @@ def delete_user_workflow(workflow_id): return jsonify({'success': True}) - @app.route('/api/user/workflows//runs', methods=['GET']) + @bp.route('/api/user/workflows//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -781,7 +780,7 @@ def get_user_workflow_runs(workflow_id): }) - @app.route('/api/user/workflows//runs//items', methods=['GET']) + @bp.route('/api/user/workflows//runs//items', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -804,7 +803,7 @@ def get_user_workflow_run_items(workflow_id, run_id): }) - @app.route('/api/user/workflows//runs//resume-failed', methods=['POST']) + @bp.route('/api/user/workflows//runs//resume-failed', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -873,7 +872,7 @@ def resume_failed_user_workflow_items(workflow_id, run_id): release_distributed_task_lock(lock_document) - @app.route('/api/group/workflows', methods=['GET']) + @bp.route('/api/group/workflows', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -892,7 +891,7 @@ def get_group_workflows_route(): return jsonify({'error': str(exc)}), 403 - @app.route('/api/group/workflows/file-sync-sources', methods=['GET']) + @bp.route('/api/group/workflows/file-sync-sources', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -920,7 +919,7 @@ def get_group_workflow_file_sync_sources(): return jsonify({'error': 'Unable to load File Sync sources right now.'}), 500 - @app.route('/api/group/workflows/agents', methods=['GET']) + @bp.route('/api/group/workflows/agents', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -947,7 +946,7 @@ def get_group_workflow_agent_options_route(): return jsonify({'error': 'Unable to load group workflow agents right now.'}), 500 - @app.route('/api/group/workflows', methods=['POST']) + @bp.route('/api/group/workflows', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1006,7 +1005,7 @@ def save_group_workflow_route(): return jsonify({'success': True, 'workflow': workflow}), 201 if is_create else 200 - @app.route('/api/group/workflows/', methods=['DELETE']) + @bp.route('/api/group/workflows/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1041,7 +1040,7 @@ def delete_group_workflow_route(workflow_id): return jsonify({'success': True}) - @app.route('/api/group/workflows//runs', methods=['GET']) + @bp.route('/api/group/workflows//runs', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1068,7 +1067,7 @@ def get_group_workflow_runs_route(workflow_id): }) - @app.route('/api/group/workflows//runs//items', methods=['GET']) + @bp.route('/api/group/workflows//runs//items', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1100,7 +1099,7 @@ def get_group_workflow_run_items_route(workflow_id, run_id): }) - @app.route('/api/group/workflows//runs//resume-failed', methods=['POST']) + @bp.route('/api/group/workflows//runs//resume-failed', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1186,7 +1185,7 @@ def resume_failed_group_workflow_items(workflow_id, run_id): release_distributed_task_lock(lock_document) - @app.route('/api/group/workflows/activity', methods=['GET']) + @bp.route('/api/group/workflows/activity', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1229,7 +1228,7 @@ def get_group_workflow_activity_snapshot(): return jsonify({'error': 'Unable to load workflow activity right now.'}), 500 - @app.route('/api/group/workflows/activity/stream', methods=['GET']) + @bp.route('/api/group/workflows/activity/stream', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1288,7 +1287,7 @@ def stream_group_workflow_activity(): ) - @app.route('/api/group/workflows//run', methods=['POST']) + @bp.route('/api/group/workflows//run', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1350,7 +1349,7 @@ def run_group_workflow_route(workflow_id): release_distributed_task_lock(lock_document) - @app.route('/api/user/workflows/activity', methods=['GET']) + @bp.route('/api/user/workflows/activity', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1389,7 +1388,7 @@ def get_user_workflow_activity_snapshot(): return jsonify({'error': 'Unable to load workflow activity right now.'}), 500 - @app.route('/api/user/workflows/activity/stream', methods=['GET']) + @bp.route('/api/user/workflows/activity/stream', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1443,7 +1442,7 @@ def stream_user_workflow_activity(): ) - @app.route('/api/user/workflows//run', methods=['POST']) + @bp.route('/api/user/workflows//run', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1492,4 +1491,4 @@ def run_user_workflow(workflow_id): return jsonify(response_body) return jsonify(response_body), 500 finally: - release_distributed_task_lock(lock_document) \ No newline at end of file + release_distributed_task_lock(lock_document) diff --git a/application/single_app/route_backend_workspace_identities.py b/application/single_app/route_backend_workspace_identities.py index 90795f287..dfd965dc9 100644 --- a/application/single_app/route_backend_workspace_identities.py +++ b/application/single_app/route_backend_workspace_identities.py @@ -25,7 +25,7 @@ WORKSPACE_IDENTITY_MANAGER_ROLES = ("Owner", "Admin", "DocumentManager") -def register_route_backend_workspace_identities(app): +def register_route_backend_workspace_identities(bp): def _error(message, status=400): return jsonify({"error": message}), status @@ -127,7 +127,7 @@ def _delete(scope_type, scope_id, identity_id, user_id): delete_result = delete_workspace_identity(scope_type, scope_id, identity_id, user_id) return jsonify({"message": "Workspace identity deleted", "delete_result": delete_result}), 200 - @app.route('/api/admin/workspace-identities/global/identities', methods=['GET']) + @bp.route('/api/admin/workspace-identities/global/identities', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -138,7 +138,7 @@ def api_admin_workspace_identities_global_list(): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities/global/identities', methods=['POST']) + @bp.route('/api/admin/workspace-identities/global/identities', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -149,7 +149,7 @@ def api_admin_workspace_identities_global_create(): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities/global/identities/', methods=['PATCH']) + @bp.route('/api/admin/workspace-identities/global/identities/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -160,7 +160,7 @@ def api_admin_workspace_identities_global_update(identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities/global/identities/', methods=['DELETE']) + @bp.route('/api/admin/workspace-identities/global/identities/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -171,7 +171,7 @@ def api_admin_workspace_identities_global_delete(identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/personal/identities', methods=['GET']) + @bp.route('/api/workspace-identities/personal/identities', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -182,7 +182,7 @@ def api_workspace_identities_personal_list(): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/personal/identities', methods=['POST']) + @bp.route('/api/workspace-identities/personal/identities', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -193,7 +193,7 @@ def api_workspace_identities_personal_create(): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/personal/identities/', methods=['PATCH']) + @bp.route('/api/workspace-identities/personal/identities/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -204,7 +204,7 @@ def api_workspace_identities_personal_update(identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/personal/identities/', methods=['DELETE']) + @bp.route('/api/workspace-identities/personal/identities/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -215,7 +215,7 @@ def api_workspace_identities_personal_delete(identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/group/identities', methods=['GET']) + @bp.route('/api/workspace-identities/group/identities', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -226,7 +226,7 @@ def api_workspace_identities_group_list(): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/group/identities', methods=['POST']) + @bp.route('/api/workspace-identities/group/identities', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -237,7 +237,7 @@ def api_workspace_identities_group_create(): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/group/identities/', methods=['PATCH']) + @bp.route('/api/workspace-identities/group/identities/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -248,7 +248,7 @@ def api_workspace_identities_group_update(identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/group/identities/', methods=['DELETE']) + @bp.route('/api/workspace-identities/group/identities/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -259,7 +259,7 @@ def api_workspace_identities_group_delete(identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/public//identities', methods=['GET']) + @bp.route('/api/workspace-identities/public//identities', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -270,7 +270,7 @@ def api_workspace_identities_public_list(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/public//identities', methods=['POST']) + @bp.route('/api/workspace-identities/public//identities', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -281,7 +281,7 @@ def api_workspace_identities_public_create(public_workspace_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/public//identities/', methods=['PATCH']) + @bp.route('/api/workspace-identities/public//identities/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -292,7 +292,7 @@ def api_workspace_identities_public_update(public_workspace_id, identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/workspace-identities/public//identities/', methods=['DELETE']) + @bp.route('/api/workspace-identities/public//identities/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -303,7 +303,7 @@ def api_workspace_identities_public_delete(public_workspace_id, identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities///identities', methods=['GET']) + @bp.route('/api/admin/workspace-identities///identities', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -314,7 +314,7 @@ def api_admin_workspace_identities_list(scope_type, scope_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities///identities', methods=['POST']) + @bp.route('/api/admin/workspace-identities///identities', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -325,7 +325,7 @@ def api_admin_workspace_identities_create(scope_type, scope_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities///identities/', methods=['PATCH']) + @bp.route('/api/admin/workspace-identities///identities/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -336,7 +336,7 @@ def api_admin_workspace_identities_update(scope_type, scope_id, identity_id): except Exception as error: return _map_exception(error) - @app.route('/api/admin/workspace-identities///identities/', methods=['DELETE']) + @bp.route('/api/admin/workspace-identities///identities/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -345,4 +345,4 @@ def api_admin_workspace_identities_delete(scope_type, scope_id, identity_id): admin_user_id, target_scope_id = _require_admin_target_context(scope_type, scope_id) return _delete(scope_type, target_scope_id, identity_id, admin_user_id) except Exception as error: - return _map_exception(error) \ No newline at end of file + return _map_exception(error) diff --git a/application/single_app/route_custom_pages.py b/application/single_app/route_custom_pages.py index d65165d5d..c68fef491 100644 --- a/application/single_app/route_custom_pages.py +++ b/application/single_app/route_custom_pages.py @@ -100,22 +100,22 @@ def _render_custom_page_response(slug): ) -def register_route_custom_pages(app): - @app.route("/custom/", methods=["GET"]) +def register_route_custom_pages(bp): + @bp.route("/custom/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required def custom_page(slug): """Render a trusted custom page.""" return _render_custom_page_response(slug) - @app.route("/custom/.html", methods=["GET"]) + @bp.route("/custom/.html", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required def custom_page_html_alias(slug): """Render a trusted custom page from a familiar .html URL alias.""" return _render_custom_page_response(slug) - @app.route("/custom/assets///", methods=["GET"]) + @bp.route("/custom/assets///", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required def custom_page_asset(slug, folder, filename): @@ -139,7 +139,7 @@ def custom_page_asset(slug, folder, filename): response.headers["Content-Type"] = "application/javascript" return response - @app.route("/api/custom//", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) + @bp.route("/api/custom//", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) @swagger_route(security=get_auth_security()) @login_required def custom_page_api(slug, operation): @@ -157,7 +157,7 @@ def custom_page_api(slug, operation): except NotImplementedError: return jsonify({"error": "Custom page API not found"}), 404 - @app.route("/api/admin/custom-pages", methods=["GET"]) + @bp.route("/api/admin/custom-pages", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -169,7 +169,7 @@ def admin_list_custom_pages(): safe_pages = [{key: value for key, value in page.items() if key != "extension"} for page in pages] return jsonify({"pages": safe_pages}) - @app.route("/api/admin/custom-pages/developer-guide", methods=["GET"]) + @bp.route("/api/admin/custom-pages/developer-guide", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -192,7 +192,7 @@ def admin_custom_pages_developer_guide(): ) return jsonify({"error": "Custom Pages developer guide could not be loaded."}), 500 - @app.route("/api/admin/custom-pages", methods=["POST"]) + @bp.route("/api/admin/custom-pages", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -208,7 +208,7 @@ def admin_create_custom_page(): saved = save_custom_page(payload, user_id=_current_admin_user_id()) return jsonify(saved), 201 - @app.route("/api/admin/custom-pages/request-access-example", methods=["POST"]) + @bp.route("/api/admin/custom-pages/request-access-example", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -245,7 +245,7 @@ def admin_create_request_access_custom_page(): }) return jsonify({"page": saved, "access_request_button_enabled": True}), 201 - @app.route("/api/admin/custom-pages/", methods=["PUT"]) + @bp.route("/api/admin/custom-pages/", methods=["PUT"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -259,7 +259,7 @@ def admin_update_custom_page(slug): saved = save_custom_page(payload, user_id=_current_admin_user_id()) return jsonify(saved), 200 - @app.route("/api/admin/custom-pages/", methods=["DELETE"]) + @bp.route("/api/admin/custom-pages/", methods=["DELETE"]) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -270,4 +270,4 @@ def admin_delete_custom_page(slug): return jsonify({"error": "Python-backed custom pages are managed in code."}), 400 if not delete_custom_page(slug): return jsonify({"error": "Custom page not found."}), 404 - return Response(status=204) \ No newline at end of file + return Response(status=204) diff --git a/application/single_app/route_enhanced_citations.py b/application/single_app/route_enhanced_citations.py index 66b0c767c..193fdca7e 100644 --- a/application/single_app/route_enhanced_citations.py +++ b/application/single_app/route_enhanced_citations.py @@ -206,10 +206,10 @@ def _log_enhanced_citations_error(message, error, **details): ) -def register_enhanced_citations_routes(app): +def register_enhanced_citations_routes(bp): """Register enhanced citations routes""" - @app.route("/api/enhanced_citations/document_metadata", methods=["GET"]) + @bp.route("/api/enhanced_citations/document_metadata", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -247,7 +247,7 @@ def get_enhanced_citation_document_metadata(): debug_print(f"Error getting enhanced citation document metadata: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/image", methods=["GET"]) + @bp.route("/api/enhanced_citations/image", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -285,7 +285,7 @@ def get_enhanced_citation_image(): except Exception as e: return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/video", methods=["GET"]) + @bp.route("/api/enhanced_citations/video", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -323,7 +323,7 @@ def get_enhanced_citation_video(): except Exception as e: return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/audio", methods=["GET"]) + @bp.route("/api/enhanced_citations/audio", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -361,7 +361,7 @@ def get_enhanced_citation_audio(): except Exception as e: return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/pdf", methods=["GET"]) + @bp.route("/api/enhanced_citations/pdf", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -423,7 +423,7 @@ def get_enhanced_citation_pdf(): ) return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/tabular", methods=["GET"]) + @bp.route("/api/enhanced_citations/tabular", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -517,7 +517,7 @@ def get_enhanced_citation_tabular(): debug_print(f"Error serving tabular citation: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/tabular_workspace", methods=["GET"]) + @bp.route("/api/enhanced_citations/tabular_workspace", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -553,7 +553,7 @@ def get_enhanced_citation_tabular_workspace(): debug_print(f"Error serving tabular workspace citation: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/workspace_documents/download", methods=["GET"]) + @bp.route("/api/workspace_documents/download", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -578,7 +578,7 @@ def download_workspace_document(): debug_print(f"Error serving workspace document download: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/chat_artifacts/download", methods=["GET"]) + @bp.route("/api/chat_artifacts/download", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -613,7 +613,7 @@ def download_chat_artifact(): debug_print(f"Error serving chat artifact download: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/chat_artifacts/promote", methods=["POST"]) + @bp.route("/api/chat_artifacts/promote", methods=["POST"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -855,7 +855,7 @@ def promote_chat_artifact_to_workspace(): debug_print(f"Error promoting chat artifact: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/tabular_preview", methods=["GET"]) + @bp.route("/api/enhanced_citations/tabular_preview", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -993,7 +993,7 @@ def get_enhanced_citation_tabular_preview(): debug_print(f"Error generating tabular preview: {e}") return jsonify({"error": str(e)}), 500 - @app.route("/api/enhanced_citations/visio", methods=["GET"]) + @bp.route("/api/enhanced_citations/visio", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_external_health.py b/application/single_app/route_external_health.py index c994d9bb4..a36d67fef 100644 --- a/application/single_app/route_external_health.py +++ b/application/single_app/route_external_health.py @@ -7,8 +7,8 @@ from functions_prompts import * from swagger_wrapper import swagger_route, get_auth_security -def register_route_external_health(app): - @app.route('/external/healthcheck', methods=['GET']) +def register_route_external_health(bp): + @bp.route('/external/healthcheck', methods=['GET']) @swagger_route(security=get_auth_security()) @swagger_route() @enabled_required("enable_external_healthcheck") @@ -19,12 +19,12 @@ def external_health_check(): return time_string, 200 -def register_no_auth_health(app): - @app.route('/external/healthcheckz', methods=['GET']) +def register_no_auth_health(bp): + @bp.route('/external/healthcheckz', methods=['GET']) @swagger_route(security=get_auth_security()) @enabled_required("enable_no_auth_external_healthcheck") def no_auth_external_healthcheck(): return { "status": "ok", "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - }, 200 \ No newline at end of file + }, 200 diff --git a/application/single_app/route_external_public_documents.py b/application/single_app/route_external_public_documents.py index fa04ac970..e525099aa 100644 --- a/application/single_app/route_external_public_documents.py +++ b/application/single_app/route_external_public_documents.py @@ -47,14 +47,14 @@ def _require_external_public_workspace_context(allowed_roles, operation_type=Non return user_id, active_workspace_id, workspace_doc, role, None -def register_route_external_public_documents(app): +def register_route_external_public_documents(bp): """ Provides backend routes for public-level document management: - GET /external/public_documents (list) - POST /external/public_documents/upload - DELETE /external/public_documents/ """ - @app.route('/external/public_documents/upload', methods=['POST']) + @bp.route('/external/public_documents/upload', methods=['POST']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -168,7 +168,7 @@ def external_upload_public_document(): }), response_status - @app.route('/external/public_documents', methods=['GET']) + @bp.route('/external/public_documents', methods=['GET']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -295,7 +295,7 @@ def external_get_public_documents(): }), 200 - @app.route('/external/public_documents/', methods=['GET']) + @bp.route('/external/public_documents/', methods=['GET']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -313,7 +313,7 @@ def external_get_public_document(document_id): return get_document(user_id=user_id, document_id=document_id, public_workspace_id=active_workspace_id) - @app.route('/external/public_documents/', methods=['PATCH']) + @bp.route('/external/public_documents/', methods=['PATCH']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -433,7 +433,7 @@ def external_patch_public_document(document_id): - @app.route('/external/public_documents/', methods=['DELETE']) + @bp.route('/external/public_documents/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -468,7 +468,7 @@ def external_delete_public_document(document_id): return jsonify({'error': f'Error deleting public document: {str(e)}'}), 500 - @app.route('/external/public_documents//extract_metadata', methods=['POST']) + @bp.route('/external/public_documents//extract_metadata', methods=['POST']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -506,7 +506,7 @@ def external_extract_public_metadata(document_id): 'document_id': document_id }), 200 - @app.route('/external/public_documents/upgrade_legacy', methods=['POST']) + @bp.route('/external/public_documents/upgrade_legacy', methods=['POST']) @swagger_route(security=get_auth_security()) @accesstoken_required @enabled_required("enable_public_workspaces") @@ -526,4 +526,4 @@ def external_upgrade_legacy_public_documents(): "message": f"Upgraded {count} public document(s) to the new format." }), 200 except Exception as e: - return jsonify({'error': str(e)}), 500 \ No newline at end of file + return jsonify({'error': str(e)}), 500 diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py index 1c0511ee5..822bdb2f2 100644 --- a/application/single_app/route_frontend_admin_settings.py +++ b/application/single_app/route_frontend_admin_settings.py @@ -5,6 +5,8 @@ from config import * from functions_documents import * from functions_authentication import * +from flask import current_app + from functions_keyvault import keyvault_model_endpoint_cleanup_helper, keyvault_model_endpoint_delete_helper, keyvault_model_endpoint_save_helper, redact_model_endpoint_secret_values from functions_settings import * from functions_file_sync import FILE_SYNC_DEFAULTS, get_file_sync_config @@ -53,6 +55,12 @@ } HEX_COLOR_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$') + +def _is_update_version_newer(latest_version, current_version): + """Return True only when the discovered release version is newer than the running version.""" + return compare_versions(latest_version, current_version) == 1 + + def allowed_file(filename, allowed_extensions): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in allowed_extensions @@ -109,8 +117,8 @@ def normalize_agents_page_text(value, fallback, max_length): candidate = fallback return candidate[:max_length] -def register_route_frontend_admin_settings(app): - @app.route('/admin/settings', methods=['GET', 'POST']) +def register_route_frontend_admin_settings(bp): + @bp.route('/admin/settings', methods=['GET', 'POST']) @swagger_route(security=get_auth_security()) @login_required @admin_required @@ -476,7 +484,7 @@ def admin_settings(): log_event(f"Error retrieving GPT deployments: {e}", level=logging.ERROR) # Check for application updates - current_version = app.config['VERSION'] + current_version = current_app.config['VERSION'] update_available = False latest_version = None download_url = "https://github.com/microsoft/simplechat/releases" @@ -506,7 +514,7 @@ def admin_settings(): } # Compare with current version - if latest_version and compare_versions(latest_version, current_version) == 1: + if _is_update_version_newer(latest_version, current_version): new_settings['update_available'] = True else: new_settings['update_available'] = False @@ -519,8 +527,14 @@ def admin_settings(): log_event(f"Error checking for updates: {e}", level=logging.ERROR) # Get the persisted values for template rendering - update_available = settings.get('update_available', False) latest_version = settings.get('latest_version_available') + update_available = _is_update_version_newer(latest_version, current_version) + if settings.get('update_available') != update_available: + try: + update_settings({'update_available': update_available}) + settings['update_available'] = update_available + except Exception as e: + log_event(f"Error normalizing cached update availability: {e}", level=logging.WARNING) # Get user settings for profile and navigation user_id = get_current_user_id() @@ -1651,7 +1665,7 @@ def is_valid_url(url): if cosmos_throughput_validation_errors: for validation_error in cosmos_throughput_validation_errors: flash(validation_error, 'danger') - return redirect(url_for('admin_settings')) + return redirect(url_for('frontend_admin_settings.admin_settings')) cosmos_throughput_settings = normalize_cosmos_throughput_settings(cosmos_throughput_candidate_settings) @@ -2388,7 +2402,7 @@ def is_valid_url(url): # Redirect back to settings page - return redirect(url_for('admin_settings')) + return redirect(url_for('frontend_admin_settings.admin_settings')) # Fallback if not GET or POST (shouldn't happen with standard routing) - return redirect(url_for('admin_settings')) \ No newline at end of file + return redirect(url_for('frontend_admin_settings.admin_settings')) diff --git a/application/single_app/route_frontend_agents.py b/application/single_app/route_frontend_agents.py index e5c2f3ee2..e3a48fee1 100644 --- a/application/single_app/route_frontend_agents.py +++ b/application/single_app/route_frontend_agents.py @@ -69,8 +69,8 @@ def build_agents_page_config(settings): } -def register_route_frontend_agents(app): - @app.route('/agents', methods=['GET']) +def register_route_frontend_agents(bp): + @bp.route('/agents', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -82,4 +82,4 @@ def agents(): 'agents.html', settings=public_settings, agents_page_config=build_agents_page_config(public_settings), - ) \ No newline at end of file + ) diff --git a/application/single_app/route_frontend_authentication.py b/application/single_app/route_frontend_authentication.py index 6bb70ae0f..bce84d021 100644 --- a/application/single_app/route_frontend_authentication.py +++ b/application/single_app/route_frontend_authentication.py @@ -51,10 +51,9 @@ def _use_app_service_easy_auth_logout(): def _build_app_service_easy_auth_logout_url(): """Build the Easy Auth logout URL that resets the upstream session before re-entering Flask login.""" - post_logout_redirect_uri = quote(url_for('login'), safe='') + post_logout_redirect_uri = quote(url_for('frontend_authentication.login'), safe='') return f"/.auth/logout?post_logout_redirect_uri={post_logout_redirect_uri}" - def _decode_teams_assertion_claims(teams_token): """Decode claims from a Teams assertion after MSAL has accepted it for OBO.""" try: @@ -137,8 +136,8 @@ def _build_teams_session_user(result, teams_token): return {key: value for key, value in session_user.items() if value} -def register_route_frontend_authentication(app): - @app.route('/login') +def register_route_frontend_authentication(bp): + @bp.route('/login') @swagger_route(security=get_auth_security()) def login(): # Clear potentially stale cache/user info before starting new login @@ -175,9 +174,9 @@ def login(): redirect_uri = login_redirect_url else: # Fall back to environment variable if Front Door is enabled but no URL is set - redirect_uri = LOGIN_REDIRECT_URL or url_for('authorized', _external=True, _scheme='https') + redirect_uri = LOGIN_REDIRECT_URL or url_for('frontend_authentication.authorized', _external=True, _scheme='https') else: - redirect_uri = url_for('authorized', _external=True, _scheme='https') + redirect_uri = url_for('frontend_authentication.authorized', _external=True, _scheme='https') debug_print(f"LOGIN_REDIRECT_URL (env): {LOGIN_REDIRECT_URL}") debug_print(f"front_door_url (db): {settings.get('front_door_url')}") @@ -192,12 +191,12 @@ def login(): #auth_url= auth_url.replace('https://', 'http://') # Ensure HTTPS for security return redirect(auth_url) - @app.route('/ci-auth/session', methods=['POST']) + @bp.route('/ci-auth/session', methods=['POST']) @swagger_route(security=get_auth_security()) def ci_auth_session(): return create_ci_bearer_session() - @app.route('/getAToken') # This is your redirect URI path + @bp.route('/getAToken') # This is your redirect URI path @swagger_route(security=get_auth_security()) def authorized(): # Check for errors passed back from Azure AD @@ -226,9 +225,9 @@ def authorized(): redirect_uri = login_redirect_url else: # Fall back to environment variable if Front Door is enabled but no URL is set - redirect_uri = LOGIN_REDIRECT_URL or url_for('authorized', _external=True, _scheme='https') + redirect_uri = LOGIN_REDIRECT_URL or url_for('frontend_authentication.authorized', _external=True, _scheme='https') else: - redirect_uri = url_for('authorized', _external=True, _scheme='https') + redirect_uri = url_for('frontend_authentication.authorized', _external=True, _scheme='https') print(f"Token exchange using redirect_uri: {redirect_uri}") @@ -287,11 +286,11 @@ def authorized(): print(f"Redirecting to environment HOME_REDIRECT_URL: {HOME_REDIRECT_URL}") return redirect(HOME_REDIRECT_URL) - debug_print(f"Front Door not enabled or URLs not set, falling back to url_for('index')") - return redirect(url_for('index')) # Or another appropriate page + debug_print(f"Front Door not enabled or URLs not set, falling back to url_for('public_app.index')") + return redirect(url_for('public_app.index')) # Or another appropriate page # This route is for API calls that need a token, not the web app login flow. This does not kick off a session. - @app.route('/getATokenApi') # This is your redirect URI path + @bp.route('/getATokenApi') # This is your redirect URI path @swagger_route(security=get_auth_security()) def authorized_api(): # Check for errors passed back from Azure AD @@ -318,9 +317,9 @@ def authorized_api(): home_url, login_redirect_url = build_front_door_urls(front_door_url) redirect_uri = login_redirect_url else: - redirect_uri = LOGIN_REDIRECT_URL or url_for('authorized', _external=True, _scheme='https') + redirect_uri = LOGIN_REDIRECT_URL or url_for('frontend_authentication.authorized', _external=True, _scheme='https') else: - redirect_uri = url_for('authorized', _external=True, _scheme='https') + redirect_uri = url_for('frontend_authentication.authorized', _external=True, _scheme='https') requested_scopes = get_requested_oauth_scopes(clear_after_read=True) result = msal_app.acquire_token_by_authorization_code( @@ -336,7 +335,7 @@ def authorized_api(): return jsonify(result, 200) - @app.route('/auth/teams/token-exchange', methods=['POST']) + @bp.route('/auth/teams/token-exchange', methods=['POST']) @swagger_route(security=get_auth_security()) def teams_token_exchange(): """Exchange a Teams SSO assertion for a SimpleChat Flask session.""" @@ -430,7 +429,7 @@ def teams_token_exchange(): }, }), 200 - @app.route('/logout/local') + @bp.route('/logout/local') @swagger_route(security=get_auth_security()) def local_logout(): """ @@ -461,13 +460,13 @@ def local_logout(): elif HOME_REDIRECT_URL: logout_uri = HOME_REDIRECT_URL else: - logout_uri = url_for('index') + logout_uri = url_for('public_app.index') else: - logout_uri = url_for('index') + logout_uri = url_for('public_app.index') return redirect(logout_uri) - @app.route('/logout') + @bp.route('/logout') @swagger_route(security=get_auth_security()) def logout(): user_name = session.get("user", {}).get("name", "User") @@ -496,9 +495,9 @@ def logout(): # Fall back to environment variable if Front Door is enabled but no URL is set logout_uri = HOME_REDIRECT_URL else: - logout_uri = url_for('index', _external=True) + logout_uri = url_for('public_app.index', _external=True) else: - logout_uri = url_for('index', _external=True) + logout_uri = url_for('public_app.index', _external=True) debug_print(f"Front Door enabled: {settings.get('enable_front_door', False)}") debug_print(f"Front Door URL: {settings.get('front_door_url')}") @@ -513,4 +512,4 @@ def logout(): logout_url += f"&logout_hint={quote(user_email)}" debug_print(f"{user_name} logged out. Redirecting to Azure AD logout.") - return redirect(logout_url) \ No newline at end of file + return redirect(logout_url) diff --git a/application/single_app/route_frontend_chats.py b/application/single_app/route_frontend_chats.py index a63b57e81..55401dfb6 100644 --- a/application/single_app/route_frontend_chats.py +++ b/application/single_app/route_frontend_chats.py @@ -647,15 +647,15 @@ def _build_chat_prompt_catalog(*, user_id, settings, user_groups_raw, user_visib return catalog -def register_route_frontend_chats(app): - @app.route('/chats', methods=['GET']) +def register_route_frontend_chats(bp): + @bp.route('/chats', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required def chats(): user_id = get_current_user_id() if not user_id: - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) settings = get_settings() user_settings = get_user_settings(user_id) @@ -728,7 +728,7 @@ def chats(): }) if not user_id: - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) # Get user display name from user settings user_display_name = user_settings.get('display_name', '') @@ -822,14 +822,14 @@ def chats(): initial_chat_model_selection=initial_chat_model_selection, ) - @app.route('/workflow-activity', methods=['GET']) + @bp.route('/workflow-activity', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required def workflow_activity(): user_id = get_current_user_id() if not user_id: - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) settings = get_settings() try: @@ -851,7 +851,7 @@ def workflow_activity(): settings=public_settings, ) - @app.route('/upload', methods=['POST']) + @bp.route('/upload', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1552,7 +1552,7 @@ def upload_file(): }), 200 # THIS IS THE OLD ROUTE, KEEPING IT FOR REFERENCE, WILL DELETE LATER - @app.route("/view_pdf", methods=["GET"]) + @bp.route("/view_pdf", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1707,7 +1707,7 @@ def view_pdf(): os.remove(temp_pdf_path) # --- Updated route --- - @app.route('/view_document') + @bp.route('/view_document') @swagger_route(security=get_auth_security()) @login_required @user_required @@ -1901,4 +1901,4 @@ def view_document(): os.remove(local_file_path) except OSError as e: # Log error but don't prevent response from being sent - print(f"Error cleaning up file {local_file_path}: {e}") \ No newline at end of file + print(f"Error cleaning up file {local_file_path}: {e}") diff --git a/application/single_app/route_frontend_control_center.py b/application/single_app/route_frontend_control_center.py index 23c5ab1f8..1ecd0ce86 100644 --- a/application/single_app/route_frontend_control_center.py +++ b/application/single_app/route_frontend_control_center.py @@ -9,8 +9,8 @@ import json from functions_debug import debug_print -def register_route_frontend_control_center(app): - @app.route('/admin/control-center', methods=['GET']) +def register_route_frontend_control_center(bp): + @bp.route('/admin/control-center', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @control_center_required('dashboard') @@ -55,9 +55,9 @@ def control_center(): except Exception as e: debug_print(f"Error loading control center: {e}") flash(f"Error loading control center: {str(e)}", "error") - return redirect(url_for('admin_settings')) + return redirect(url_for('frontend_admin_settings.admin_settings')) - @app.route('/approvals', methods=['GET']) + @bp.route('/approvals', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -87,7 +87,7 @@ def approvals(): debug_print(f"Error loading approvals: {e}\n{error_trace}") print(f"ERROR IN APPROVALS ROUTE: {e}\n{error_trace}") flash(f"Error loading approvals: {str(e)}", "error") - return redirect(url_for('index')) + return redirect(url_for('public_app.index')) def get_control_center_statistics(): """ @@ -280,4 +280,4 @@ def get_control_center_statistics(): 'recent_activity_24h': {'chats': 0, 'documents': 0, 'logins': 0}, 'blocked_users': 0, 'alerts': [] - } \ No newline at end of file + } diff --git a/application/single_app/route_frontend_conversations.py b/application/single_app/route_frontend_conversations.py index 48a4f8529..2997549c2 100644 --- a/application/single_app/route_frontend_conversations.py +++ b/application/single_app/route_frontend_conversations.py @@ -53,7 +53,7 @@ def _authorize_frontend_personal_conversation_access(user_id, conversation_id): return conversation_item -def register_route_frontend_conversations(app): +def register_route_frontend_conversations(bp): def _disable_response_caching(response): response.headers['Cache-Control'] = 'no-store, max-age=0' response.headers['Pragma'] = 'no-cache' @@ -79,14 +79,14 @@ def _refresh_azure_maps_message_payloads(messages): return refreshed_messages - @app.route('/conversations') + @bp.route('/conversations') @swagger_route(security=get_auth_security()) @login_required @user_required def conversations(): user_id = get_current_user_id() if not user_id: - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) query = f""" SELECT * @@ -100,14 +100,14 @@ def conversations(): )) return render_template('conversations.html', conversations=items) - @app.route('/conversation/', methods=['GET']) + @bp.route('/conversation/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required def view_conversation(conversation_id): user_id = get_current_user_id() if not user_id: - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) try: _authorize_frontend_personal_conversation_access(user_id, conversation_id) except LookupError: @@ -130,7 +130,7 @@ def view_conversation(conversation_id): messages = _refresh_azure_maps_message_payloads(messages) return render_template('chat.html', conversation_id=conversation_id, messages=messages) - @app.route('/conversation//messages', methods=['GET']) + @bp.route('/conversation//messages', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -214,7 +214,7 @@ def get_conversation_messages(conversation_id): response = jsonify({'messages': messages}) return _disable_response_caching(response) - @app.route('/api/conversation//agent-citation/', methods=['GET']) + @bp.route('/api/conversation//agent-citation/', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -276,7 +276,7 @@ def get_agent_citation_artifact(conversation_id, artifact_id): response = jsonify({'citation': refresh_azure_maps_citation_payload(citation)}) return _disable_response_caching(response) - @app.route('/api/azure-maps/tile', methods=['GET']) + @bp.route('/api/azure-maps/tile', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -349,7 +349,7 @@ def get_azure_maps_tile(): proxy_response.headers['X-Content-Type-Options'] = 'nosniff' return proxy_response - @app.route('/api/message//metadata', methods=['GET']) + @bp.route('/api/message//metadata', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -414,4 +414,4 @@ def get_message_metadata(message_id): except Exception as e: log_event(f"get_message_metadata failed: {e}", level="WARNING") - return jsonify({'error': 'Failed to fetch message metadata'}), 500 \ No newline at end of file + return jsonify({'error': 'Failed to fetch message metadata'}), 500 diff --git a/application/single_app/route_frontend_feedback.py b/application/single_app/route_frontend_feedback.py index 524420c00..bdb9fb272 100644 --- a/application/single_app/route_frontend_feedback.py +++ b/application/single_app/route_frontend_feedback.py @@ -5,9 +5,9 @@ from functions_settings import * from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_feedback(app): +def register_route_frontend_feedback(bp): - @app.route("/admin/feedback_review") + @bp.route("/admin/feedback_review") @swagger_route(security=get_auth_security()) @login_required @feedback_admin_required @@ -19,7 +19,7 @@ def admin_feedback_review(): return render_template("admin_feedback_review.html") - @app.route("/my_feedback") + @bp.route("/my_feedback") @swagger_route(security=get_auth_security()) @login_required @user_required @@ -29,4 +29,4 @@ def my_feedback(): Redirects the user to the consolidated profile feedback tab. """ - return redirect(url_for('profile', tab='feedback')) \ No newline at end of file + return redirect(url_for('frontend_profile.profile', tab='feedback')) diff --git a/application/single_app/route_frontend_group_workspaces.py b/application/single_app/route_frontend_group_workspaces.py index 21725c2b1..aedc2d762 100644 --- a/application/single_app/route_frontend_group_workspaces.py +++ b/application/single_app/route_frontend_group_workspaces.py @@ -8,8 +8,8 @@ from functions_file_sync import FILE_SYNC_MANAGER_ROLES, is_file_sync_enabled_for_group from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_group_workspaces(app): - @app.route('/group_workspaces', methods=['GET']) +def register_route_frontend_group_workspaces(bp): + @bp.route('/group_workspaces', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -37,7 +37,7 @@ def group_workspaces(): enable_audio_file_support = settings.get('enable_audio_file_support', False) if not user_id: print("User not authenticated.") - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) query = """ SELECT VALUE COUNT(1) @@ -96,7 +96,7 @@ def group_workspaces(): workspace_governance=workspace_governance ) - @app.route('/set_active_group', methods=['POST']) + @bp.route('/set_active_group', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -114,4 +114,4 @@ def set_active_group(): except PermissionError: return "You are not a member of this group", 403 - return redirect(url_for('group_workspaces')) + return redirect(url_for('frontend_group_workspaces.group_workspaces')) diff --git a/application/single_app/route_frontend_groups.py b/application/single_app/route_frontend_groups.py index 5a57da52a..2288fe574 100644 --- a/application/single_app/route_frontend_groups.py +++ b/application/single_app/route_frontend_groups.py @@ -5,8 +5,8 @@ from functions_settings import * from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_groups(app): - @app.route("/my_groups", methods=["GET"]) +def register_route_frontend_groups(bp): + @bp.route("/my_groups", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -15,9 +15,9 @@ def my_groups(): """ Redirects the legacy My Groups page to the profile Groups tab. """ - return redirect(url_for('profile', tab='groups')) + return redirect(url_for('frontend_profile.profile', tab='groups')) - @app.route("/groups/", methods=["GET"]) + @bp.route("/groups/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_frontend_notifications.py b/application/single_app/route_frontend_notifications.py index 9897f01c2..2e3797680 100644 --- a/application/single_app/route_frontend_notifications.py +++ b/application/single_app/route_frontend_notifications.py @@ -5,9 +5,9 @@ from functions_settings import * from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_notifications(app): +def register_route_frontend_notifications(bp): - @app.route("/notifications") + @bp.route("/notifications") @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_frontend_profile.py b/application/single_app/route_frontend_profile.py index e45280f6b..8f7fca32c 100644 --- a/application/single_app/route_frontend_profile.py +++ b/application/single_app/route_frontend_profile.py @@ -16,8 +16,8 @@ from swagger_wrapper import swagger_route, get_auth_security import traceback -def register_route_frontend_profile(app): - @app.route('/profile') +def register_route_frontend_profile(bp): + @bp.route('/profile') @swagger_route(security=get_auth_security()) @login_required def profile(): @@ -80,7 +80,7 @@ def get_profile_fact_memory_payload(user_id): 'facts': [serialize_fact_memory_item(fact) for fact in facts], } - @app.route('/api/profile/image/refresh', methods=['POST']) + @bp.route('/api/profile/image/refresh', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -126,7 +126,7 @@ def refresh_profile_image(): log_event(f"Error refreshing profile image for user {user_id}: {str(e)}", level=logging.ERROR) return jsonify({"error": "Internal server error"}), 500 - @app.route('/api/user/activity-trends', methods=['GET']) + @bp.route('/api/user/activity-trends', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -387,7 +387,7 @@ def get_user_activity_trends(): traceback.print_exc() return jsonify({"error": "Failed to fetch activity trends"}), 500 - @app.route('/api/user/settings', methods=['GET']) + @bp.route('/api/user/settings', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -444,7 +444,7 @@ def get_user_settings_api(): traceback.print_exc() return jsonify({"error": "Failed to fetch user settings"}), 500 - @app.route('/api/profile/fact-memory', methods=['GET']) + @bp.route('/api/profile/fact-memory', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -467,7 +467,7 @@ def get_profile_fact_memory(): ) return jsonify({'error': 'Failed to fetch fact memory'}), 500 - @app.route('/api/profile/fact-memory', methods=['POST']) + @bp.route('/api/profile/fact-memory', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -513,7 +513,7 @@ def create_profile_fact_memory(): ) return jsonify({'error': 'Failed to create fact memory'}), 500 - @app.route('/api/profile/fact-memory/', methods=['PUT']) + @bp.route('/api/profile/fact-memory/', methods=['PUT']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -555,7 +555,7 @@ def update_profile_fact_memory(fact_id): ) return jsonify({'error': 'Failed to update fact memory'}), 500 - @app.route('/api/profile/fact-memory/', methods=['DELETE']) + @bp.route('/api/profile/fact-memory/', methods=['DELETE']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -586,4 +586,4 @@ def delete_profile_fact_memory(fact_id): level=logging.ERROR, exceptionTraceback=True, ) - return jsonify({'error': 'Failed to delete fact memory'}), 500 \ No newline at end of file + return jsonify({'error': 'Failed to delete fact memory'}), 500 diff --git a/application/single_app/route_frontend_public_workspaces.py b/application/single_app/route_frontend_public_workspaces.py index 51ff3cea8..d6f9ea362 100644 --- a/application/single_app/route_frontend_public_workspaces.py +++ b/application/single_app/route_frontend_public_workspaces.py @@ -7,16 +7,16 @@ from functions_file_sync import FILE_SYNC_MANAGER_ROLES, assert_public_workspace_role, is_file_sync_enabled_for_public_workspace from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_public_workspaces(app): - @app.route("/my_public_workspaces", methods=["GET"]) +def register_route_frontend_public_workspaces(bp): + @bp.route("/my_public_workspaces", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @enabled_required("enable_public_workspaces") def my_public_workspaces(): - return redirect(url_for('profile', tab='public-workspaces')) + return redirect(url_for('frontend_profile.profile', tab='public-workspaces')) - @app.route("/public_workspaces/", methods=["GET"]) + @bp.route("/public_workspaces/", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -39,7 +39,7 @@ def manage_public_workspace(workspace_id): file_sync_enabled=file_sync_enabled ) - @app.route("/public_workspaces", methods=["GET"]) + @bp.route("/public_workspaces", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -76,7 +76,7 @@ def public_workspaces(): allowed_extensions=allowed_extensions_str ) - @app.route("/public_directory", methods=["GET"]) + @bp.route("/public_directory", methods=["GET"]) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -95,7 +95,7 @@ def public_directory(): app_settings=public_settings ) - @app.route('/set_active_public_workspace', methods=['POST']) + @bp.route('/set_active_public_workspace', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -111,4 +111,4 @@ def set_active_public_workspace(): except LookupError: return "Workspace not found", 404 - return redirect(url_for('public_workspaces')) \ No newline at end of file + return redirect(url_for('frontend_public_workspaces.public_workspaces')) diff --git a/application/single_app/route_frontend_safety.py b/application/single_app/route_frontend_safety.py index 71c20b0e3..22cda9210 100644 --- a/application/single_app/route_frontend_safety.py +++ b/application/single_app/route_frontend_safety.py @@ -5,9 +5,9 @@ from functions_settings import * from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_safety(app): +def register_route_frontend_safety(bp): - @app.route('/admin/safety_violations', methods=['GET']) + @bp.route('/admin/safety_violations', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @safety_violation_admin_required @@ -18,7 +18,7 @@ def admin_safety_violations(): """ return render_template('admin_safety_violations.html') - @app.route('/safety_violations', methods=['GET']) + @bp.route('/safety_violations', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -28,4 +28,4 @@ def my_safety_violations(): Redirects the user to the consolidated profile violations tab. """ - return redirect(url_for('profile', tab='violations')) \ No newline at end of file + return redirect(url_for('frontend_profile.profile', tab='violations')) diff --git a/application/single_app/route_frontend_support.py b/application/single_app/route_frontend_support.py index 668fb6849..e81ab6e79 100644 --- a/application/single_app/route_frontend_support.py +++ b/application/single_app/route_frontend_support.py @@ -16,9 +16,9 @@ def _support_menu_access_allowed(): return 'Admin' in roles or 'User' in roles -def register_route_frontend_support(app): +def register_route_frontend_support(bp): - @app.route('/support/latest-features') + @bp.route('/support/latest-features') @swagger_route(security=get_auth_security()) @login_required @user_required @@ -50,7 +50,7 @@ def support_latest_features(): support_previous_release_feature_groups=previous_release_feature_groups, ) - @app.route('/support/send-feedback') + @bp.route('/support/send-feedback') @swagger_route(security=get_auth_security()) @login_required @user_required @@ -65,4 +65,4 @@ def support_send_feedback(): if not settings.get('enable_support_send_feedback', True) or not recipient_email: return 'Not Found', 404 - return render_template('support_send_feedback.html') \ No newline at end of file + return render_template('support_send_feedback.html') diff --git a/application/single_app/route_frontend_workspace.py b/application/single_app/route_frontend_workspace.py index bedbbbd87..84bb93425 100644 --- a/application/single_app/route_frontend_workspace.py +++ b/application/single_app/route_frontend_workspace.py @@ -12,8 +12,8 @@ from functions_source_review import is_url_access_enabled_for_user from swagger_wrapper import swagger_route, get_auth_security -def register_route_frontend_workspace(app): - @app.route('/workspace', methods=['GET']) +def register_route_frontend_workspace(bp): + @bp.route('/workspace', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -41,7 +41,7 @@ def workspace(): file_sync_enabled = is_file_sync_enabled_for_user(settings, user_id, user_info.get('email'), user_info=user_info) if user_id else False if not user_id: print("User not authenticated.") - return redirect(url_for('login')) + return redirect(url_for('frontend_authentication.login')) query = """ SELECT VALUE COUNT(1) @@ -137,4 +137,4 @@ def workspace(): workspace_governance=workspace_governance ) - \ No newline at end of file + diff --git a/application/single_app/route_migration.py b/application/single_app/route_migration.py index a6cde2d67..9c4f572e2 100644 --- a/application/single_app/route_migration.py +++ b/application/single_app/route_migration.py @@ -5,7 +5,7 @@ """ from flask import Blueprint, jsonify, request -from functions_authentication import login_required, user_required, get_current_user_id +from functions_authentication import get_current_user_id, login_required, user_required, user_required_blueprint from functions_personal_agents import migrate_agents_from_user_settings, get_personal_agents from functions_personal_actions import migrate_actions_from_user_settings, get_personal_actions from functions_appinsights import log_event @@ -13,6 +13,7 @@ import logging bp_migration = Blueprint('migration', __name__) +bp_migration.before_request(user_required_blueprint()) @bp_migration.route('/api/migrate/agents', methods=['POST']) @swagger_route( diff --git a/application/single_app/route_openapi.py b/application/single_app/route_openapi.py index 16b085508..674dc3ef4 100644 --- a/application/single_app/route_openapi.py +++ b/application/single_app/route_openapi.py @@ -15,10 +15,10 @@ from swagger_wrapper import swagger_route, get_auth_security from functions_debug import debug_print -def register_openapi_routes(app): +def register_openapi_routes(bp): """Register OpenAPI-related routes.""" - @app.route('/api/openapi/upload', methods=['POST']) + @bp.route('/api/openapi/upload', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -134,7 +134,7 @@ def upload_openapi_spec(): 'error': 'Internal server error during upload' }), 500 - @app.route('/api/openapi/list-uploaded', methods=['GET']) + @bp.route('/api/openapi/list-uploaded', methods=['GET']) @swagger_route(security=get_auth_security()) @login_required @user_required @@ -191,7 +191,7 @@ def list_uploaded_specs(): 'error': 'Internal server error while listing specifications' }), 500 - @app.route('/api/openapi/analyze-auth', methods=['POST']) + @bp.route('/api/openapi/analyze-auth', methods=['POST']) @swagger_route(security=get_auth_security()) @login_required @user_required diff --git a/application/single_app/route_plugin_logging.py b/application/single_app/route_plugin_logging.py index bfc56b42a..f23f0125a 100644 --- a/application/single_app/route_plugin_logging.py +++ b/application/single_app/route_plugin_logging.py @@ -4,13 +4,14 @@ """ from flask import Blueprint, jsonify, request -from functions_authentication import admin_required, login_required, user_required, get_current_user_id +from functions_authentication import admin_required, get_current_user_id, login_required, login_required_blueprint, user_required from functions_appinsights import log_event from semantic_kernel_plugins.plugin_invocation_logger import get_plugin_logger from swagger_wrapper import swagger_route, get_auth_security import logging bpl = Blueprint('plugin_logging', __name__) +bpl.before_request(login_required_blueprint()) @bpl.route('/api/plugins/invocations', methods=['GET']) diff --git a/application/single_app/semantic_kernel_loader.py b/application/single_app/semantic_kernel_loader.py index b4fdda638..8140b173e 100644 --- a/application/single_app/semantic_kernel_loader.py +++ b/application/single_app/semantic_kernel_loader.py @@ -26,7 +26,7 @@ from semantic_kernel_plugins.document_search_plugin import DocumentSearchPlugin from semantic_kernel_plugins.chart_plugin import ChartPlugin from semantic_kernel_plugins.tabular_processing_plugin import TabularProcessingPlugin -from functions_settings import get_settings, get_user_settings, is_tabular_processing_enabled +from functions_settings import get_settings, get_user_settings, is_tabular_processing_enabled, resolve_model_endpoint_foundry_scope from foundry_agent_runtime import ( AzureAIFoundryChatCompletionAgent, AzureAIFoundryNewChatCompletionAgent, @@ -466,26 +466,7 @@ def resolve_aoai_scope(): return str(cognitive_services_scope or "").strip() def resolve_foundry_scope(auth_settings, endpoint=None): - custom_scope = (auth_settings.get("foundry_scope") or "").strip() - if custom_scope: - return custom_scope - - management_cloud = (auth_settings.get("management_cloud") or "public").lower() - if management_cloud in ("government", "usgovernment", "usgov"): - return "https://ai.azure.us/.default" - if management_cloud == "china": - return "https://ai.azure.cn/.default" - if management_cloud == "germany": - return "https://ai.azure.de/.default" - - endpoint_value = (endpoint or "").lower() - if "azure.us" in endpoint_value: - return "https://ai.azure.us/.default" - if "azure.cn" in endpoint_value: - return "https://ai.azure.cn/.default" - if "azure.de" in endpoint_value: - return "https://ai.azure.de/.default" - return "https://ai.azure.com/.default" + return resolve_model_endpoint_foundry_scope(auth_settings, endpoint=endpoint) def build_token_provider(auth_settings, provider="aoai", endpoint=None): auth_type = (auth_settings.get("type") or "managed_identity").lower() diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index 170ac7239..162ec1310 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -19,6 +19,7 @@ let externalLinksMenuName = window.externalLinksMenuName || 'External Links'; let agentsPagePromotedPopularAgents = Array.isArray(window.agentsPagePromotedPopularAgents) ? window.agentsPagePromotedPopularAgents : []; +let enableSemanticKernel = Boolean(window.enableSemanticKernel); let releaseNotificationsRegistration = window.releaseNotificationsRegistration || { registered: false, name: '', @@ -389,6 +390,19 @@ async function loadPromotedPopularAvailableAgents() { if (!promotedPopularAgentsSelect) { return; } + if (!enableSemanticKernel) { + agentsPagePromotedAvailableAgents = []; + setPromotedPopularLoadError(''); + promotedPopularAgentsSelect.textContent = ''; + const option = document.createElement('option'); + option.value = ''; + option.textContent = 'Enable Agents to load available agents'; + promotedPopularAgentsSelect.appendChild(option); + if (promotedPopularAgentsAddButton) { + promotedPopularAgentsAddButton.disabled = true; + } + return; + } try { const response = await fetch('/api/agents/catalog?include_usage=true'); const payload = await response.json().catch(() => ({})); diff --git a/application/single_app/support_menu_config.py b/application/single_app/support_menu_config.py index 065a9534c..d84934bbc 100644 --- a/application/single_app/support_menu_config.py +++ b/application/single_app/support_menu_config.py @@ -5,6 +5,13 @@ _SUPPORT_LATEST_FEATURE_DOCS_SETTING_KEY = 'enable_support_latest_feature_documentation_links' +_LEGACY_ACTION_ENDPOINTS = { + 'chats': 'frontend_chats.chats', + 'workspace': 'frontend_workspace.workspace', + 'profile': 'frontend_profile.profile', + 'support_latest_features': 'frontend_support.support_latest_features', + 'support_send_feedback': 'frontend_support.support_send_feedback', +} def _latest_feature_card(feature_id, title, icon, summary, details, why, guidance, actions=None, image_label=None, image_title=None, image_caption=None, image_name=None, include_media=True): @@ -2160,6 +2167,17 @@ def _action_enabled(action, settings): return all(_setting_enabled(settings, setting_key) for setting_key in required_settings) +def _normalize_action_endpoint(action): + endpoint = action.get('endpoint') + if endpoint in _LEGACY_ACTION_ENDPOINTS: + action['endpoint'] = _LEGACY_ACTION_ENDPOINTS[endpoint] + + +def _normalize_feature_actions(feature): + for action in feature.get('actions', []): + _normalize_action_endpoint(action) + + def _normalize_feature_media(feature): """Ensure every visible feature exposes at least one image entry for the template.""" images = feature.get('images') or [] @@ -2186,12 +2204,19 @@ def _normalize_feature_media(feature): def get_support_latest_feature_catalog(): """Return a copy of the support latest-features catalog.""" - return _flatten_support_feature_groups(_SUPPORT_LATEST_FEATURE_RELEASE_GROUPS) + features = _flatten_support_feature_groups(_SUPPORT_LATEST_FEATURE_RELEASE_GROUPS) + for feature in features: + _normalize_feature_actions(feature) + return features def get_support_latest_feature_release_groups(): """Return grouped latest-feature metadata organized by release.""" - return deepcopy(_SUPPORT_LATEST_FEATURE_RELEASE_GROUPS) + feature_groups = deepcopy(_SUPPORT_LATEST_FEATURE_RELEASE_GROUPS) + for feature_group in feature_groups: + for feature in feature_group.get('features', []): + _normalize_feature_actions(feature) + return feature_groups def get_default_support_latest_features_visibility(): @@ -2235,6 +2260,7 @@ def get_visible_support_latest_features(settings): action for action in visible_item.get('actions', []) if _action_enabled(action, settings) ] + _normalize_feature_actions(visible_item) visible_item = _apply_support_application_title(visible_item, app_title) _normalize_feature_media(visible_item) visible_items.append(visible_item) @@ -2262,6 +2288,7 @@ def get_visible_support_latest_feature_groups(settings): action for action in visible_feature.get('actions', []) if _action_enabled(action, settings) ] + _normalize_feature_actions(visible_feature) visible_feature = _apply_support_application_title(visible_feature, app_title) _normalize_feature_media(visible_feature) visible_features.append(visible_feature) @@ -2286,6 +2313,7 @@ def get_support_latest_feature_release_groups_for_settings(settings): action for action in feature.get('actions', []) if _action_enabled(action, settings) ] + _normalize_feature_actions(feature) feature.update(_apply_support_application_title(feature, app_title)) _normalize_feature_media(feature) @@ -2305,6 +2333,7 @@ def get_admin_latest_feature_release_groups_for_settings(settings): action for action in feature.get('actions', []) if _action_enabled(action, settings) ] + _normalize_feature_actions(feature) feature.update(_apply_support_application_title(feature, app_title)) _normalize_feature_media(feature) diff --git a/application/single_app/swagger_wrapper.py b/application/single_app/swagger_wrapper.py index ce4a358a2..c7ec3a8b5 100644 --- a/application/single_app/swagger_wrapper.py +++ b/application/single_app/swagger_wrapper.py @@ -87,7 +87,7 @@ def example_endpoint(): - Memory-efficient single-entry cache per app instance """ -from flask import Flask, jsonify, render_template_string, request, make_response +from flask import Blueprint, Flask, jsonify, render_template_string, request, make_response from functools import wraps from typing import Dict, List, Optional, Any, Union import json @@ -1555,8 +1555,11 @@ def register_swagger_routes(app: Flask): if not settings.get('enable_swagger', True): # Default to True if setting not found print("Swagger documentation is disabled in admin settings.") return + + swagger_bp = Blueprint('swagger_docs', __name__) + swagger_bp.before_request(apply_blueprint_auth('login_required')) - @app.route('/swagger') + @swagger_bp.route('/swagger') @swagger_route( summary="Interactive Swagger UI", description="Serve the Swagger UI interface for API documentation and testing.", @@ -2049,7 +2052,7 @@ def swagger_ui(): """ return swagger_html - @app.route('/swagger.json') + @swagger_bp.route('/swagger.json') @swagger_route( summary="OpenAPI Specification", description="Serve the OpenAPI 3.0 specification as JSON with caching and rate limiting.", @@ -2105,7 +2108,7 @@ def swagger_json(): return response - @app.route('/swagger.yaml') + @swagger_bp.route('/swagger.yaml') @swagger_route( summary="OpenAPI Specification (YAML)", description="Serve the OpenAPI 3.0 specification as YAML with caching and rate limiting.", @@ -2161,7 +2164,7 @@ def swagger_yaml(): return response - @app.route('/api/swagger/routes') + @swagger_bp.route('/api/swagger/routes') @swagger_route( summary="List Documented Routes", description="List all routes and their documentation status with cache statistics.", @@ -2233,7 +2236,7 @@ def list_documented_routes(): 'cache_stats': _swagger_cache.get_cache_stats() }) - @app.route('/api/swagger/cache', methods=['GET', 'DELETE']) + @swagger_bp.route('/api/swagger/cache', methods=['GET', 'DELETE']) @swagger_route( summary="Swagger Cache Management", description="Manage swagger specification cache - get cache statistics or clear cache.", @@ -2275,6 +2278,8 @@ def swagger_cache_management(): 'message': 'Use DELETE method to clear cache' }) + app.register_blueprint(swagger_bp) + # Utility function to create common response schemas def create_response_schema(success_schema: Optional[Dict[str, Any]] = None, error_schema: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ diff --git a/application/single_app/templates/_sidebar_nav.html b/application/single_app/templates/_sidebar_nav.html index 03bd658ee..08e9b31bc 100644 --- a/application/single_app/templates/_sidebar_nav.html +++ b/application/single_app/templates/_sidebar_nav.html @@ -25,7 +25,7 @@ {% endif %} {% if app_settings.show_logo %} - + {% if app_settings.custom_logo_base64 or app_settings.custom_logo_dark_base64 %} {% if app_settings.custom_logo_base64 %} @@ -57,7 +57,7 @@ {% endif %} {% elif not app_settings.hide_app_title %} - + {{ app_settings.app_title }} @@ -84,19 +84,19 @@ {% else %} - + Personal {% endif %} @@ -237,7 +237,7 @@ {% else %} - + Groups {% endif %} @@ -245,7 +245,7 @@ {% endif %} {% if app_settings.enable_public_workspaces %} @@ -321,7 +321,7 @@ {% else %} - Login + Login {% endif %} @@ -1112,4 +1112,4 @@ {% if not app_settings.show_logo and app_settings.hide_app_title %} {% endif %} - \ No newline at end of file + diff --git a/application/single_app/templates/_sidebar_short_nav.html b/application/single_app/templates/_sidebar_short_nav.html index 2860ec865..74cd2d028 100644 --- a/application/single_app/templates/_sidebar_short_nav.html +++ b/application/single_app/templates/_sidebar_short_nav.html @@ -48,25 +48,25 @@
Chat Navigation
{% if app_settings.enable_user_workspace %} - + Personal {% endif %} {% if app_settings.enable_group_workspaces %} - + Groups {% endif %} {% if app_settings.enable_public_workspaces %} - + Public {% endif %} - + Chat {% if sidebar_settings.enable_semantic_kernel %} - + Agents {% endif %} @@ -91,19 +91,19 @@
Chat Navigation
{% else %} - Login + Login {% endif %} diff --git a/application/single_app/templates/_top_nav.html b/application/single_app/templates/_top_nav.html index ce1bd139a..0da9a8882 100644 --- a/application/single_app/templates/_top_nav.html +++ b/application/single_app/templates/_top_nav.html @@ -1,9 +1,9 @@ -{% set is_chat_page = request.endpoint == 'chats' %} +{% set is_chat_page = request.endpoint == 'frontend_chats.chats' %}