From 9d545144ce6d324b7c0dd106ffcc960fdaf75c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 13 May 2026 16:08:50 +0800 Subject: [PATCH] chore: remove obsolete admin console routes (#35637) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/__init__.py | 2 - api/controllers/console/admin.py | 405 +------- api/openapi/markdown/console-swagger.md | 165 ---- .../controllers/console/test_admin.py | 880 ------------------ .../console/workspace/test_workspace.py | 26 +- .../generated/api/console/admin/orpc.gen.ts | 153 --- .../generated/api/console/admin/types.gen.ts | 157 ---- .../generated/api/console/admin/zod.gen.ts | 99 -- 8 files changed, 5 insertions(+), 1882 deletions(-) delete mode 100644 api/tests/unit_tests/controllers/console/test_admin.py delete mode 100644 packages/contracts/generated/api/console/admin/orpc.gen.ts delete mode 100644 packages/contracts/generated/api/console/admin/types.gen.ts delete mode 100644 packages/contracts/generated/api/console/admin/zod.gen.ts diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index 980e828945..16fa07a6c2 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -33,7 +33,6 @@ for module_name in RESOURCE_MODULES: # Ensure resource modules are imported so route decorators are evaluated. # Import other controllers from . import ( - admin, apikey, extension, feature, @@ -142,7 +141,6 @@ api.add_namespace(console_ns) __all__ = [ "account", "activate", - "admin", "advanced_prompt_template", "agent", "agent_providers", diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index ae2b1007dd..0cfa1fb25b 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -1,64 +1,11 @@ -import csv -import io from collections.abc import Callable from functools import wraps -from typing import cast -from uuid import UUID from flask import request -from flask_restx import Resource -from pydantic import BaseModel, Field, field_validator -from sqlalchemy import select -from werkzeug.exceptions import BadRequest, NotFound, Unauthorized +from werkzeug.exceptions import Unauthorized from configs import dify_config -from constants.languages import supported_language -from controllers.common.schema import register_schema_models -from controllers.console import console_ns -from controllers.console.wraps import only_edition_cloud -from core.db.session_factory import session_factory -from extensions.ext_database import db from libs.token import extract_access_token -from models.model import App, ExporleBanner, InstalledApp, RecommendedApp, TrialApp -from services.billing_service import BillingService, LangContentDict - - -class InsertExploreAppPayload(BaseModel): - app_id: str = Field(...) - desc: str | None = None - copyright: str | None = None - privacy_policy: str | None = None - custom_disclaimer: str | None = None - language: str = Field(...) - category: str = Field(...) - position: int = Field(...) - can_trial: bool = Field(default=False) - trial_limit: int = Field(default=0) - - @field_validator("language") - @classmethod - def validate_language(cls, value: str) -> str: - return supported_language(value) - - -class InsertExploreBannerPayload(BaseModel): - category: str = Field(...) - title: str = Field(...) - description: str = Field(...) - img_src: str = Field(..., alias="img-src") - language: str = Field(default="en-US") - link: str = Field(...) - sort: int = Field(...) - - @field_validator("language") - @classmethod - def validate_language(cls, value: str) -> str: - return supported_language(value) - - model_config = {"populate_by_name": True} - - -register_schema_models(console_ns, InsertExploreAppPayload, InsertExploreBannerPayload) def admin_required[**P, R](view: Callable[P, R]) -> Callable[P, R]: @@ -76,353 +23,3 @@ def admin_required[**P, R](view: Callable[P, R]) -> Callable[P, R]: return view(*args, **kwargs) return decorated - - -@console_ns.route("/admin/insert-explore-apps") -class InsertExploreAppListApi(Resource): - @console_ns.doc("insert_explore_app") - @console_ns.doc(description="Insert or update an app in the explore list") - @console_ns.expect(console_ns.models[InsertExploreAppPayload.__name__]) - @console_ns.response(200, "App updated successfully") - @console_ns.response(201, "App inserted successfully") - @console_ns.response(404, "App not found") - @only_edition_cloud - @admin_required - def post(self): - payload = InsertExploreAppPayload.model_validate(console_ns.payload) - - app = db.session.execute(select(App).where(App.id == payload.app_id)).scalar_one_or_none() - if not app: - raise NotFound(f"App '{payload.app_id}' is not found") - - site = app.site - if not site: - desc = payload.desc or "" - copy_right = payload.copyright or "" - privacy_policy = payload.privacy_policy or "" - custom_disclaimer = payload.custom_disclaimer or "" - else: - desc = site.description or payload.desc or "" - copy_right = site.copyright or payload.copyright or "" - privacy_policy = site.privacy_policy or payload.privacy_policy or "" - custom_disclaimer = site.custom_disclaimer or payload.custom_disclaimer or "" - - with session_factory.create_session() as session: - recommended_app = session.execute( - select(RecommendedApp).where(RecommendedApp.app_id == payload.app_id) - ).scalar_one_or_none() - - if not recommended_app: - recommended_app = RecommendedApp( - app_id=app.id, - description=desc, - copyright=copy_right, - privacy_policy=privacy_policy, - custom_disclaimer=custom_disclaimer, - language=payload.language, - category=payload.category, - position=payload.position, - ) - - db.session.add(recommended_app) - if payload.can_trial: - trial_app = db.session.execute( - select(TrialApp).where(TrialApp.app_id == payload.app_id) - ).scalar_one_or_none() - if not trial_app: - db.session.add( - TrialApp( - app_id=payload.app_id, - tenant_id=app.tenant_id, - trial_limit=payload.trial_limit, - ) - ) - else: - trial_app.trial_limit = payload.trial_limit - - app.is_public = True - db.session.commit() - - return {"result": "success"}, 201 - else: - recommended_app.description = desc - recommended_app.copyright = copy_right - recommended_app.privacy_policy = privacy_policy - recommended_app.custom_disclaimer = custom_disclaimer - recommended_app.language = payload.language - recommended_app.category = payload.category - recommended_app.position = payload.position - - if payload.can_trial: - trial_app = db.session.execute( - select(TrialApp).where(TrialApp.app_id == payload.app_id) - ).scalar_one_or_none() - if not trial_app: - db.session.add( - TrialApp( - app_id=payload.app_id, - tenant_id=app.tenant_id, - trial_limit=payload.trial_limit, - ) - ) - else: - trial_app.trial_limit = payload.trial_limit - app.is_public = True - - db.session.commit() - - return {"result": "success"}, 200 - - -@console_ns.route("/admin/insert-explore-apps/") -class InsertExploreAppApi(Resource): - @console_ns.doc("delete_explore_app") - @console_ns.doc(description="Remove an app from the explore list") - @console_ns.doc(params={"app_id": "Application ID to remove"}) - @console_ns.response(204, "App removed successfully") - @only_edition_cloud - @admin_required - def delete(self, app_id: UUID): - with session_factory.create_session() as session: - recommended_app = session.execute( - select(RecommendedApp).where(RecommendedApp.app_id == str(app_id)) - ).scalar_one_or_none() - - if not recommended_app: - return {"result": "success"}, 204 - - with session_factory.create_session() as session: - app = session.execute(select(App).where(App.id == recommended_app.app_id)).scalar_one_or_none() - - if app: - app.is_public = False - - with session_factory.create_session() as session: - installed_apps = ( - session.execute( - select(InstalledApp).where( - InstalledApp.app_id == recommended_app.app_id, - InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id, - ) - ) - .scalars() - .all() - ) - - for installed_app in installed_apps: - session.delete(installed_app) - - trial_app = session.execute( - select(TrialApp).where(TrialApp.app_id == recommended_app.app_id) - ).scalar_one_or_none() - if trial_app: - session.delete(trial_app) - - db.session.delete(recommended_app) - db.session.commit() - - return {"result": "success"}, 204 - - -@console_ns.route("/admin/insert-explore-banner") -class InsertExploreBannerApi(Resource): - @console_ns.doc("insert_explore_banner") - @console_ns.doc(description="Insert an explore banner") - @console_ns.expect(console_ns.models[InsertExploreBannerPayload.__name__]) - @console_ns.response(201, "Banner inserted successfully") - @only_edition_cloud - @admin_required - def post(self): - payload = InsertExploreBannerPayload.model_validate(console_ns.payload) - - banner = ExporleBanner( - content={ - "category": payload.category, - "title": payload.title, - "description": payload.description, - "img-src": payload.img_src, - }, - link=payload.link, - sort=payload.sort, - language=payload.language, - ) - db.session.add(banner) - db.session.commit() - - return {"result": "success"}, 201 - - -@console_ns.route("/admin/delete-explore-banner/") -class DeleteExploreBannerApi(Resource): - @console_ns.doc("delete_explore_banner") - @console_ns.doc(description="Delete an explore banner") - @console_ns.doc(params={"banner_id": "Banner ID to delete"}) - @console_ns.response(204, "Banner deleted successfully") - @only_edition_cloud - @admin_required - def delete(self, banner_id): - banner = db.session.execute(select(ExporleBanner).where(ExporleBanner.id == banner_id)).scalar_one_or_none() - if not banner: - raise NotFound(f"Banner '{banner_id}' is not found") - - db.session.delete(banner) - db.session.commit() - - return {"result": "success"}, 204 - - -class LangContentPayload(BaseModel): - lang: str = Field(..., description="Language tag: 'zh' | 'en' | 'jp'") - title: str = Field(...) - subtitle: str | None = Field(default=None) - body: str = Field(...) - title_pic_url: str | None = Field(default=None) - - -class UpsertNotificationPayload(BaseModel): - notification_id: str | None = Field(default=None, description="Omit to create; supply UUID to update") - contents: list[LangContentPayload] = Field(..., min_length=1) - start_time: str | None = Field(default=None, description="RFC3339, e.g. 2026-03-01T00:00:00Z") - end_time: str | None = Field(default=None, description="RFC3339, e.g. 2026-03-20T23:59:59Z") - frequency: str = Field(default="once", description="'once' | 'every_page_load'") - status: str = Field(default="active", description="'active' | 'inactive'") - - -class BatchAddNotificationAccountsPayload(BaseModel): - notification_id: str = Field(...) - user_email: list[str] = Field(..., description="List of account email addresses") - - -register_schema_models(console_ns, UpsertNotificationPayload, BatchAddNotificationAccountsPayload) - - -@console_ns.route("/admin/upsert_notification") -class UpsertNotificationApi(Resource): - @console_ns.doc("upsert_notification") - @console_ns.doc( - description=( - "Create or update an in-product notification. " - "Supply notification_id to update an existing one; omit it to create a new one. " - "Pass at least one language variant in contents (zh / en / jp)." - ) - ) - @console_ns.expect(console_ns.models[UpsertNotificationPayload.__name__]) - @console_ns.response(200, "Notification upserted successfully") - @only_edition_cloud - @admin_required - def post(self): - payload = UpsertNotificationPayload.model_validate(console_ns.payload) - result = BillingService.upsert_notification( - contents=[cast(LangContentDict, c.model_dump()) for c in payload.contents], - frequency=payload.frequency, - status=payload.status, - notification_id=payload.notification_id, - start_time=payload.start_time, - end_time=payload.end_time, - ) - return {"result": "success", "notification_id": result.get("notificationId")}, 200 - - -@console_ns.route("/admin/batch_add_notification_accounts") -class BatchAddNotificationAccountsApi(Resource): - @console_ns.doc("batch_add_notification_accounts") - @console_ns.doc( - description=( - "Register target accounts for a notification by email address. " - 'JSON body: {"notification_id": "...", "user_email": ["a@example.com", ...]}. ' - "File upload: multipart/form-data with a 'file' field (CSV or TXT, one email per line) " - "plus a 'notification_id' field. " - "Emails that do not match any account are silently skipped." - ) - ) - @console_ns.response(200, "Accounts added successfully") - @only_edition_cloud - @admin_required - def post(self): - from models.account import Account - - if "file" in request.files: - notification_id = request.form.get("notification_id", "").strip() - if not notification_id: - raise BadRequest("notification_id is required.") - emails = self._parse_emails_from_file() - else: - payload = BatchAddNotificationAccountsPayload.model_validate(console_ns.payload) - notification_id = payload.notification_id - emails = payload.user_email - - if not emails: - raise BadRequest("No valid email addresses provided.") - - # Resolve emails → account IDs in chunks to avoid large IN-clause - account_ids: list[str] = [] - chunk_size = 500 - for i in range(0, len(emails), chunk_size): - chunk = emails[i : i + chunk_size] - rows = db.session.execute(select(Account.id, Account.email).where(Account.email.in_(chunk))).all() - account_ids.extend(str(row.id) for row in rows) - - if not account_ids: - raise BadRequest("None of the provided emails matched an existing account.") - - # Send to dify-saas in batches of 1000 - total_count = 0 - batch_size = 1000 - for i in range(0, len(account_ids), batch_size): - batch = account_ids[i : i + batch_size] - result = BillingService.batch_add_notification_accounts( - notification_id=notification_id, - account_ids=batch, - ) - total_count += result.get("count", 0) - - return { - "result": "success", - "emails_provided": len(emails), - "accounts_matched": len(account_ids), - "count": total_count, - }, 200 - - @staticmethod - def _parse_emails_from_file() -> list[str]: - """Parse email addresses from an uploaded CSV or TXT file.""" - file = request.files["file"] - if not file.filename: - raise BadRequest("Uploaded file has no filename.") - - filename_lower = file.filename.lower() - if not filename_lower.endswith((".csv", ".txt")): - raise BadRequest("Invalid file type. Only CSV (.csv) and TXT (.txt) files are allowed.") - - try: - content = file.stream.read().decode("utf-8") - except UnicodeDecodeError: - try: - file.stream.seek(0) - content = file.stream.read().decode("gbk") - except UnicodeDecodeError: - raise BadRequest("Unable to decode the file. Please use UTF-8 or GBK encoding.") - - emails: list[str] = [] - if filename_lower.endswith(".csv"): - reader = csv.reader(io.StringIO(content)) - for row in reader: - for cell in row: - cell = cell.strip() - if cell: - emails.append(cell) - else: - for line in content.splitlines(): - line = line.strip() - if line: - emails.append(line) - - # Deduplicate while preserving order - seen: set[str] = set() - unique_emails: list[str] = [] - for email in emails: - if email.lower() not in seen: - seen.add(email.lower()) - unique_emails.append(email) - - return unique_emails diff --git a/api/openapi/markdown/console-swagger.md b/api/openapi/markdown/console-swagger.md index e56d5f6fe5..ffde4a0971 100644 --- a/api/openapi/markdown/console-swagger.md +++ b/api/openapi/markdown/console-swagger.md @@ -340,116 +340,6 @@ Check if activation token is valid | ---- | ----------- | ------ | | 200 | Success | [ActivationCheckResponse](#activationcheckresponse) | -### /admin/batch_add_notification_accounts - -#### POST -##### Description - -Register target accounts for a notification by email address. JSON body: {"notification_id": "...", "user_email": ["a@example.com", ...]}. File upload: multipart/form-data with a 'file' field (CSV or TXT, one email per line) plus a 'notification_id' field. Emails that do not match any account are silently skipped. - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Accounts added successfully | - -### /admin/delete-explore-banner/{banner_id} - -#### DELETE -##### Description - -Delete an explore banner - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| banner_id | path | Banner ID to delete | Yes | string | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Banner deleted successfully | - -### /admin/insert-explore-apps - -#### POST -##### Description - -Insert or update an app in the explore list - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [InsertExploreAppPayload](#insertexploreapppayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | App updated successfully | -| 201 | App inserted successfully | -| 404 | App not found | - -### /admin/insert-explore-apps/{app_id} - -#### DELETE -##### Description - -Remove an app from the explore list - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID to remove | Yes | string | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | App removed successfully | - -### /admin/insert-explore-banner - -#### POST -##### Description - -Insert an explore banner - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [InsertExploreBannerPayload](#insertexplorebannerpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 201 | Banner inserted successfully | - -### /admin/upsert_notification - -#### POST -##### Description - -Create or update an in-product notification. Supply notification_id to update an existing one; omit it to create a new one. Pass at least one language variant in contents (zh / en / jp). - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [UpsertNotificationPayload](#upsertnotificationpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Notification upserted successfully | - ### /all-workspaces #### GET @@ -10731,13 +10621,6 @@ AppMCPServer Status Enum | ---- | ---- | ----------- | -------- | | text | string | Transcribed text from audio | Yes | -#### BatchAddNotificationAccountsPayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| notification_id | string | | Yes | -| user_email | [ string ] | List of account email addresses | Yes | - #### BatchImportPayload | Name | Type | Description | Required | @@ -12233,33 +12116,6 @@ Form input types. | model_type | [ModelType](#modeltype) | | Yes | | provider | | | No | -#### InsertExploreAppPayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| app_id | string | | Yes | -| can_trial | boolean | | No | -| category | string | | Yes | -| copyright | | | No | -| custom_disclaimer | | | No | -| desc | | | No | -| language | string | | Yes | -| position | integer | | Yes | -| privacy_policy | | | No | -| trial_limit | integer | | No | - -#### InsertExploreBannerPayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| category | string | | Yes | -| description | string | | Yes | -| img-src | string | | Yes | -| language | string | | No | -| link | string | | Yes | -| sort | integer | | Yes | -| title | string | | Yes | - #### InstallPermission | Name | Type | Description | Required | @@ -12370,16 +12226,6 @@ Enum class for large language model mode. | ---- | ---- | ----------- | -------- | | LLMMode | string | Enum class for large language model mode. | | -#### LangContentPayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| body | string | | Yes | -| lang | string | Language tag: 'zh' \| 'en' \| 'jp' | Yes | -| subtitle | | | No | -| title | string | | Yes | -| title_pic_url | | | No | - #### LegacyEndpointUpdatePayload | Name | Type | Description | Required | @@ -13937,17 +13783,6 @@ Tag type | video_file_size_limit | integer | | Yes | | workflow_file_upload_limit | integer | | Yes | -#### UpsertNotificationPayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| contents | [ [LangContentPayload](#langcontentpayload) ] | | Yes | -| end_time | | RFC3339, e.g. 2026-03-20T23:59:59Z | No | -| frequency | string | 'once' \| 'every_page_load' | No | -| notification_id | | Omit to create; supply UUID to update | No | -| start_time | | RFC3339, e.g. 2026-03-01T00:00:00Z | No | -| status | string | 'active' \| 'inactive' | No | - #### UserAction User action configuration. diff --git a/api/tests/unit_tests/controllers/console/test_admin.py b/api/tests/unit_tests/controllers/console/test_admin.py deleted file mode 100644 index 27f332ac51..0000000000 --- a/api/tests/unit_tests/controllers/console/test_admin.py +++ /dev/null @@ -1,880 +0,0 @@ -"""Final working unit tests for admin endpoints - tests business logic directly.""" - -import uuid -from unittest.mock import Mock, PropertyMock, patch - -import pytest -from pytest_mock import MockerFixture -from werkzeug.exceptions import NotFound, Unauthorized - -from controllers.console.admin import ( - DeleteExploreBannerApi, - InsertExploreAppApi, - InsertExploreAppListApi, - InsertExploreAppPayload, - InsertExploreBannerApi, - InsertExploreBannerPayload, -) -from models.model import App, InstalledApp, RecommendedApp - - -@pytest.fixture(autouse=True) -def bypass_only_edition_cloud(mocker: MockerFixture): - """ - Bypass only_edition_cloud decorator by setting EDITION to "CLOUD". - """ - mocker.patch( - "controllers.console.wraps.dify_config.EDITION", - new="CLOUD", - ) - - -@pytest.fixture -def mock_admin_auth(mocker: MockerFixture): - """ - Provide valid admin authentication for controller tests. - """ - mocker.patch( - "controllers.console.admin.dify_config.ADMIN_API_KEY", - "test-admin-key", - ) - mocker.patch( - "controllers.console.admin.extract_access_token", - return_value="test-admin-key", - ) - - -@pytest.fixture -def mock_console_payload(mocker: MockerFixture): - payload = { - "app_id": str(uuid.uuid4()), - "language": "en-US", - "category": "Productivity", - "position": 1, - } - - mocker.patch( - "flask_restx.namespace.Namespace.payload", - new_callable=PropertyMock, - return_value=payload, - ) - - return payload - - -@pytest.fixture -def mock_banner_payload(mocker: MockerFixture): - mocker.patch( - "flask_restx.namespace.Namespace.payload", - new_callable=PropertyMock, - return_value={ - "title": "Test Banner", - "description": "Banner description", - "img-src": "https://example.com/banner.png", - "link": "https://example.com", - "sort": 1, - "category": "homepage", - }, - ) - - -@pytest.fixture -def mock_session_factory(mocker: MockerFixture): - mock_session = Mock() - mock_session.execute = Mock() - mock_session.add = Mock() - mock_session.commit = Mock() - - mocker.patch( - "controllers.console.admin.session_factory.create_session", - return_value=Mock( - __enter__=lambda s: mock_session, - __exit__=Mock(return_value=False), - ), - ) - - -class TestDeleteExploreBannerApi: - def setup_method(self): - self.api = DeleteExploreBannerApi() - - def test_delete_banner_not_found(self, mocker: MockerFixture, mock_admin_auth): - mocker.patch( - "controllers.console.admin.db.session.execute", - return_value=Mock(scalar_one_or_none=lambda: None), - ) - - with pytest.raises(NotFound, match="is not found"): - self.api.delete(uuid.uuid4()) - - def test_delete_banner_success(self, mocker: MockerFixture, mock_admin_auth): - mock_banner = Mock() - - mocker.patch( - "controllers.console.admin.db.session.execute", - return_value=Mock(scalar_one_or_none=lambda: mock_banner), - ) - mocker.patch("controllers.console.admin.db.session.delete") - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.delete(uuid.uuid4()) - - assert status == 204 - assert response["result"] == "success" - - -class TestInsertExploreBannerApi: - def setup_method(self): - self.api = InsertExploreBannerApi() - - def test_insert_banner_success(self, mocker: MockerFixture, mock_admin_auth, mock_banner_payload): - mocker.patch("controllers.console.admin.db.session.add") - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.post() - - assert status == 201 - assert response["result"] == "success" - - def test_banner_payload_valid_language(self): - payload = { - "title": "Test Banner", - "description": "Banner description", - "img-src": "https://example.com/banner.png", - "link": "https://example.com", - "sort": 1, - "category": "homepage", - "language": "en-US", - } - - model = InsertExploreBannerPayload.model_validate(payload) - assert model.language == "en-US" - - def test_banner_payload_invalid_language(self): - payload = { - "title": "Test Banner", - "description": "Banner description", - "img-src": "https://example.com/banner.png", - "link": "https://example.com", - "sort": 1, - "category": "homepage", - "language": "invalid-lang", - } - - with pytest.raises(ValueError, match="invalid-lang is not a valid language"): - InsertExploreBannerPayload.model_validate(payload) - - -class TestInsertExploreAppApiDelete: - def setup_method(self): - self.api = InsertExploreAppApi() - - def test_delete_when_not_in_explore(self, mocker: MockerFixture, mock_admin_auth): - mocker.patch( - "controllers.console.admin.session_factory.create_session", - return_value=Mock( - __enter__=lambda s: s, - __exit__=Mock(return_value=False), - execute=lambda *_: Mock(scalar_one_or_none=lambda: None), - ), - ) - - response, status = self.api.delete(uuid.uuid4()) - - assert status == 204 - assert response["result"] == "success" - - def test_delete_when_in_explore_with_trial_app(self, mocker: MockerFixture, mock_admin_auth): - """Test deleting an app from explore that has a trial app.""" - app_id = uuid.uuid4() - - mock_recommended = Mock(spec=RecommendedApp) - mock_recommended.app_id = "app-123" - - mock_app = Mock(spec=App) - mock_app.is_public = True - - mock_trial = Mock() - - # Mock session context manager and its execute - mock_session = Mock() - mock_session.execute = Mock() - mock_session.delete = Mock() - - # Set up side effects for execute calls - mock_session.execute.side_effect = [ - Mock(scalar_one_or_none=lambda: mock_recommended), - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalars=Mock(return_value=Mock(all=lambda: []))), - Mock(scalar_one_or_none=lambda: mock_trial), - ] - - mocker.patch( - "controllers.console.admin.session_factory.create_session", - return_value=Mock( - __enter__=lambda s: mock_session, - __exit__=Mock(return_value=False), - ), - ) - - mocker.patch("controllers.console.admin.db.session.delete") - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.delete(app_id) - - assert status == 204 - assert response["result"] == "success" - assert mock_app.is_public is False - - def test_delete_with_installed_apps(self, mocker: MockerFixture, mock_admin_auth): - """Test deleting an app that has installed apps in other tenants.""" - app_id = uuid.uuid4() - - mock_recommended = Mock(spec=RecommendedApp) - mock_recommended.app_id = "app-123" - - mock_app = Mock(spec=App) - mock_app.is_public = True - - mock_installed_app = Mock(spec=InstalledApp) - - # Mock session - mock_session = Mock() - mock_session.execute = Mock() - mock_session.delete = Mock() - - mock_session.execute.side_effect = [ - Mock(scalar_one_or_none=lambda: mock_recommended), - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalars=Mock(return_value=Mock(all=lambda: [mock_installed_app]))), - Mock(scalar_one_or_none=lambda: None), - ] - - mocker.patch( - "controllers.console.admin.session_factory.create_session", - return_value=Mock( - __enter__=lambda s: mock_session, - __exit__=Mock(return_value=False), - ), - ) - - mocker.patch("controllers.console.admin.db.session.delete") - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.delete(app_id) - - assert status == 204 - assert mock_session.delete.called - - -class TestInsertExploreAppListApi: - def setup_method(self): - self.api = InsertExploreAppListApi() - - def test_app_not_found(self, mocker: MockerFixture, mock_admin_auth, mock_console_payload): - mocker.patch( - "controllers.console.admin.db.session.execute", - return_value=Mock(scalar_one_or_none=lambda: None), - ) - - with pytest.raises(NotFound, match="is not found"): - self.api.post() - - def test_create_recommended_app( - self, - mocker: MockerFixture, - mock_admin_auth, - mock_console_payload, - ): - mock_app = Mock(spec=App) - mock_app.id = "app-id" - mock_app.site = None - mock_app.tenant_id = "tenant" - mock_app.is_public = False - - # db.session.execute → fetch App - mocker.patch( - "controllers.console.admin.db.session.execute", - return_value=Mock(scalar_one_or_none=lambda: mock_app), - ) - - # session_factory.create_session → recommended_app lookup - mock_session = Mock() - mock_session.execute = Mock(return_value=Mock(scalar_one_or_none=lambda: None)) - - mocker.patch( - "controllers.console.admin.session_factory.create_session", - return_value=Mock( - __enter__=lambda s: mock_session, - __exit__=Mock(return_value=False), - ), - ) - - mocker.patch("controllers.console.admin.db.session.add") - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.post() - - assert status == 201 - assert response["result"] == "success" - assert mock_app.is_public is True - - def test_update_recommended_app( - self, mocker: MockerFixture, mock_admin_auth, mock_console_payload, mock_session_factory - ): - mock_app = Mock(spec=App) - mock_app.id = "app-id" - mock_app.site = None - mock_app.is_public = False - - mock_recommended = Mock(spec=RecommendedApp) - - mocker.patch( - "controllers.console.admin.db.session.execute", - side_effect=[ - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalar_one_or_none=lambda: mock_recommended), - ], - ) - - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.post() - - assert status == 200 - assert response["result"] == "success" - assert mock_app.is_public is True - - def test_site_data_overrides_payload( - self, - mocker: MockerFixture, - mock_admin_auth, - mock_console_payload, - mock_session_factory, - ): - site = Mock() - site.description = "Site Desc" - site.copyright = "Site Copyright" - site.privacy_policy = "Site Privacy" - site.custom_disclaimer = "Site Disclaimer" - - mock_app = Mock(spec=App) - mock_app.id = "app-id" - mock_app.site = site - mock_app.tenant_id = "tenant" - mock_app.is_public = False - - mocker.patch( - "controllers.console.admin.db.session.execute", - side_effect=[ - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalar_one_or_none=lambda: None), - Mock(scalar_one_or_none=lambda: None), - ], - ) - - commit_spy = mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.post() - - assert status == 200 - assert response["result"] == "success" - assert mock_app.is_public is True - commit_spy.assert_called_once() - - def test_create_trial_app_when_can_trial_enabled( - self, - mocker: MockerFixture, - mock_admin_auth, - mock_console_payload, - mock_session_factory, - ): - mock_console_payload["can_trial"] = True - mock_console_payload["trial_limit"] = 5 - - mock_app = Mock(spec=App) - mock_app.id = "app-id" - mock_app.site = None - mock_app.tenant_id = "tenant" - mock_app.is_public = False - - mocker.patch( - "controllers.console.admin.db.session.execute", - side_effect=[ - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalar_one_or_none=lambda: None), - Mock(scalar_one_or_none=lambda: None), - ], - ) - - add_spy = mocker.patch("controllers.console.admin.db.session.add") - mocker.patch("controllers.console.admin.db.session.commit") - - self.api.post() - - assert any(call.args[0].__class__.__name__ == "TrialApp" for call in add_spy.call_args_list) - - def test_update_recommended_app_with_trial( - self, - mocker: MockerFixture, - mock_admin_auth, - mock_console_payload, - mock_session_factory, - ): - """Test updating a recommended app when trial is enabled.""" - mock_console_payload["can_trial"] = True - mock_console_payload["trial_limit"] = 10 - - mock_app = Mock(spec=App) - mock_app.id = "app-id" - mock_app.site = None - mock_app.is_public = False - mock_app.tenant_id = "tenant-123" - - mock_recommended = Mock(spec=RecommendedApp) - - mocker.patch( - "controllers.console.admin.db.session.execute", - side_effect=[ - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalar_one_or_none=lambda: mock_recommended), - Mock(scalar_one_or_none=lambda: None), - ], - ) - - add_spy = mocker.patch("controllers.console.admin.db.session.add") - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.post() - - assert status == 200 - assert response["result"] == "success" - assert mock_app.is_public is True - - def test_update_recommended_app_without_trial( - self, - mocker: MockerFixture, - mock_admin_auth, - mock_console_payload, - mock_session_factory, - ): - """Test updating a recommended app without trial enabled.""" - mock_app = Mock(spec=App) - mock_app.id = "app-id" - mock_app.site = None - mock_app.is_public = False - - mock_recommended = Mock(spec=RecommendedApp) - - mocker.patch( - "controllers.console.admin.db.session.execute", - side_effect=[ - Mock(scalar_one_or_none=lambda: mock_app), - Mock(scalar_one_or_none=lambda: mock_recommended), - ], - ) - - mocker.patch("controllers.console.admin.db.session.commit") - - response, status = self.api.post() - - assert status == 200 - assert response["result"] == "success" - assert mock_app.is_public is True - - -class TestInsertExploreAppPayload: - """Test InsertExploreAppPayload validation.""" - - def test_valid_payload(self): - """Test creating payload with valid data.""" - payload_data = { - "app_id": str(uuid.uuid4()), - "desc": "Test app description", - "copyright": "© 2024 Test Company", - "privacy_policy": "https://example.com/privacy", - "custom_disclaimer": "Custom disclaimer text", - "language": "en-US", - "category": "Productivity", - "position": 1, - } - - payload = InsertExploreAppPayload.model_validate(payload_data) - - assert payload.app_id == payload_data["app_id"] - assert payload.desc == payload_data["desc"] - assert payload.copyright == payload_data["copyright"] - assert payload.privacy_policy == payload_data["privacy_policy"] - assert payload.custom_disclaimer == payload_data["custom_disclaimer"] - assert payload.language == payload_data["language"] - assert payload.category == payload_data["category"] - assert payload.position == payload_data["position"] - - def test_minimal_payload(self): - """Test creating payload with only required fields.""" - payload_data = { - "app_id": str(uuid.uuid4()), - "language": "en-US", - "category": "Productivity", - "position": 1, - } - - payload = InsertExploreAppPayload.model_validate(payload_data) - - assert payload.app_id == payload_data["app_id"] - assert payload.desc is None - assert payload.copyright is None - assert payload.privacy_policy is None - assert payload.custom_disclaimer is None - assert payload.language == payload_data["language"] - assert payload.category == payload_data["category"] - assert payload.position == payload_data["position"] - - def test_invalid_language(self): - """Test payload with invalid language code.""" - payload_data = { - "app_id": str(uuid.uuid4()), - "language": "invalid-lang", - "category": "Productivity", - "position": 1, - } - - with pytest.raises(ValueError, match="invalid-lang is not a valid language"): - InsertExploreAppPayload.model_validate(payload_data) - - -class TestAdminRequiredDecorator: - """Test admin_required decorator.""" - - def setup_method(self): - """Set up test fixtures.""" - # Mock dify_config - self.dify_config_patcher = patch("controllers.console.admin.dify_config") - self.mock_dify_config = self.dify_config_patcher.start() - self.mock_dify_config.ADMIN_API_KEY = "test-admin-key" - - # Mock extract_access_token - self.token_patcher = patch("controllers.console.admin.extract_access_token") - self.mock_extract_token = self.token_patcher.start() - - def teardown_method(self): - """Clean up test fixtures.""" - self.dify_config_patcher.stop() - self.token_patcher.stop() - - def test_admin_required_success(self): - """Test successful admin authentication.""" - from controllers.console.admin import admin_required - - @admin_required - def test_view(): - return {"success": True} - - self.mock_extract_token.return_value = "test-admin-key" - result = test_view() - assert result["success"] is True - - def test_admin_required_invalid_token(self): - """Test admin_required with invalid token.""" - from controllers.console.admin import admin_required - - @admin_required - def test_view(): - return {"success": True} - - self.mock_extract_token.return_value = "wrong-key" - with pytest.raises(Unauthorized, match="API key is invalid"): - test_view() - - def test_admin_required_no_api_key_configured(self): - """Test admin_required when no API key is configured.""" - from controllers.console.admin import admin_required - - self.mock_dify_config.ADMIN_API_KEY = None - - @admin_required - def test_view(): - return {"success": True} - - with pytest.raises(Unauthorized, match="API key is invalid"): - test_view() - - def test_admin_required_missing_authorization_header(self): - """Test admin_required with missing authorization header.""" - from controllers.console.admin import admin_required - - @admin_required - def test_view(): - return {"success": True} - - self.mock_extract_token.return_value = None - with pytest.raises(Unauthorized, match="Authorization header is missing"): - test_view() - - -class TestExploreAppBusinessLogicDirect: - """Test the core business logic of explore app management directly.""" - - def test_data_fusion_logic(self): - """Test the data fusion logic between payload and site data.""" - # Test cases for different data scenarios - test_cases = [ - { - "name": "site_data_overrides_payload", - "payload": {"desc": "Payload desc", "copyright": "Payload copyright"}, - "site": {"description": "Site desc", "copyright": "Site copyright"}, - "expected": { - "desc": "Site desc", - "copyright": "Site copyright", - "privacy_policy": "", - "custom_disclaimer": "", - }, - }, - { - "name": "payload_used_when_no_site", - "payload": {"desc": "Payload desc", "copyright": "Payload copyright"}, - "site": None, - "expected": { - "desc": "Payload desc", - "copyright": "Payload copyright", - "privacy_policy": "", - "custom_disclaimer": "", - }, - }, - { - "name": "empty_defaults_when_no_data", - "payload": {}, - "site": None, - "expected": {"desc": "", "copyright": "", "privacy_policy": "", "custom_disclaimer": ""}, - }, - ] - - for case in test_cases: - # Simulate the data fusion logic - payload_desc = case["payload"].get("desc") - payload_copyright = case["payload"].get("copyright") - payload_privacy_policy = case["payload"].get("privacy_policy") - payload_custom_disclaimer = case["payload"].get("custom_disclaimer") - - if case["site"]: - site_desc = case["site"].get("description") - site_copyright = case["site"].get("copyright") - site_privacy_policy = case["site"].get("privacy_policy") - site_custom_disclaimer = case["site"].get("custom_disclaimer") - - # Site data takes precedence - desc = site_desc or payload_desc or "" - copyright = site_copyright or payload_copyright or "" - privacy_policy = site_privacy_policy or payload_privacy_policy or "" - custom_disclaimer = site_custom_disclaimer or payload_custom_disclaimer or "" - else: - # Use payload data or empty defaults - desc = payload_desc or "" - copyright = payload_copyright or "" - privacy_policy = payload_privacy_policy or "" - custom_disclaimer = payload_custom_disclaimer or "" - - result = { - "desc": desc, - "copyright": copyright, - "privacy_policy": privacy_policy, - "custom_disclaimer": custom_disclaimer, - } - - assert result == case["expected"], f"Failed test case: {case['name']}" - - def test_app_visibility_logic(self): - """Test that apps are made public when added to explore list.""" - # Create a mock app - mock_app = Mock(spec=App) - mock_app.is_public = False - - # Simulate the business logic - mock_app.is_public = True - - assert mock_app.is_public is True - - def test_recommended_app_creation_logic(self): - """Test the creation of RecommendedApp objects.""" - app_id = str(uuid.uuid4()) - payload_data = { - "app_id": app_id, - "desc": "Test app description", - "copyright": "© 2024 Test Company", - "privacy_policy": "https://example.com/privacy", - "custom_disclaimer": "Custom disclaimer", - "language": "en-US", - "category": "Productivity", - "position": 1, - } - - # Simulate the creation logic - recommended_app = Mock(spec=RecommendedApp) - recommended_app.app_id = payload_data["app_id"] - recommended_app.description = payload_data["desc"] - recommended_app.copyright = payload_data["copyright"] - recommended_app.privacy_policy = payload_data["privacy_policy"] - recommended_app.custom_disclaimer = payload_data["custom_disclaimer"] - recommended_app.language = payload_data["language"] - recommended_app.category = payload_data["category"] - recommended_app.position = payload_data["position"] - - # Verify the data - assert recommended_app.app_id == app_id - assert recommended_app.description == "Test app description" - assert recommended_app.copyright == "© 2024 Test Company" - assert recommended_app.privacy_policy == "https://example.com/privacy" - assert recommended_app.custom_disclaimer == "Custom disclaimer" - assert recommended_app.language == "en-US" - assert recommended_app.category == "Productivity" - assert recommended_app.position == 1 - - def test_recommended_app_update_logic(self): - """Test the update logic for existing RecommendedApp objects.""" - mock_recommended_app = Mock(spec=RecommendedApp) - - update_data = { - "desc": "Updated description", - "copyright": "© 2024 Updated", - "language": "fr-FR", - "category": "Tools", - "position": 2, - } - - # Simulate the update logic - mock_recommended_app.description = update_data["desc"] - mock_recommended_app.copyright = update_data["copyright"] - mock_recommended_app.language = update_data["language"] - mock_recommended_app.category = update_data["category"] - mock_recommended_app.position = update_data["position"] - - # Verify the updates - assert mock_recommended_app.description == "Updated description" - assert mock_recommended_app.copyright == "© 2024 Updated" - assert mock_recommended_app.language == "fr-FR" - assert mock_recommended_app.category == "Tools" - assert mock_recommended_app.position == 2 - - def test_app_not_found_error_logic(self): - """Test error handling when app is not found.""" - app_id = str(uuid.uuid4()) - - # Simulate app lookup returning None - found_app = None - - # Test the error condition - if not found_app: - with pytest.raises(NotFound, match=f"App '{app_id}' is not found"): - raise NotFound(f"App '{app_id}' is not found") - - def test_recommended_app_not_found_error_logic(self): - """Test error handling when recommended app is not found for deletion.""" - app_id = str(uuid.uuid4()) - - # Simulate recommended app lookup returning None - found_recommended_app = None - - # Test the error condition - if not found_recommended_app: - with pytest.raises(NotFound, match=f"App '{app_id}' is not found in the explore list"): - raise NotFound(f"App '{app_id}' is not found in the explore list") - - def test_database_session_usage_patterns(self): - """Test the expected database session usage patterns.""" - # Mock session usage patterns - mock_session = Mock() - - # Test session.add pattern - mock_recommended_app = Mock(spec=RecommendedApp) - mock_session.add(mock_recommended_app) - mock_session.commit() - - # Verify session was used correctly - mock_session.add.assert_called_once_with(mock_recommended_app) - mock_session.commit.assert_called_once() - - # Test session.delete pattern - mock_recommended_app_to_delete = Mock(spec=RecommendedApp) - mock_session.delete(mock_recommended_app_to_delete) - mock_session.commit() - - # Verify delete pattern - mock_session.delete.assert_called_once_with(mock_recommended_app_to_delete) - - def test_payload_validation_integration(self): - """Test payload validation in the context of the business logic.""" - # Test valid payload - valid_payload_data = { - "app_id": str(uuid.uuid4()), - "desc": "Test app description", - "language": "en-US", - "category": "Productivity", - "position": 1, - } - - # This should succeed - payload = InsertExploreAppPayload.model_validate(valid_payload_data) - assert payload.app_id == valid_payload_data["app_id"] - - # Test invalid payload - invalid_payload_data = { - "app_id": str(uuid.uuid4()), - "language": "invalid-lang", # This should fail validation - "category": "Productivity", - "position": 1, - } - - # This should raise an exception - with pytest.raises(ValueError, match="invalid-lang is not a valid language"): - InsertExploreAppPayload.model_validate(invalid_payload_data) - - -class TestExploreAppDataHandling: - """Test specific data handling scenarios.""" - - def test_uuid_validation(self): - """Test UUID validation and handling.""" - # Test valid UUID - valid_uuid = str(uuid.uuid4()) - - # This should be a valid UUID - assert uuid.UUID(valid_uuid) is not None - - # Test invalid UUID - invalid_uuid = "not-a-valid-uuid" - - # This should raise a ValueError - with pytest.raises(ValueError): - uuid.UUID(invalid_uuid) - - def test_language_validation(self): - """Test language validation against supported languages.""" - from constants.languages import supported_language - - # Test supported language - assert supported_language("en-US") == "en-US" - assert supported_language("fr-FR") == "fr-FR" - - # Test unsupported language - with pytest.raises(ValueError, match="invalid-lang is not a valid language"): - supported_language("invalid-lang") - - def test_response_formatting(self): - """Test API response formatting.""" - # Test success responses - create_response = {"result": "success"} - update_response = {"result": "success"} - delete_response = None # 204 No Content returns None - - assert create_response["result"] == "success" - assert update_response["result"] == "success" - assert delete_response is None - - # Test status codes - create_status = 201 # Created - update_status = 200 # OK - delete_status = 204 # No Content - - assert create_status == 201 - assert update_status == 200 - assert delete_status == 204 diff --git a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py index a52518c2d2..0ee1877630 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py @@ -308,12 +308,7 @@ class TestWorkspaceListApi: method = unwrap(api.get) tenant = MagicMock(id="t1", name="T", status="active", created_at=naive_utc_now()) - - paginate_result = MagicMock( - items=[tenant], - has_next=False, - total=1, - ) + paginate_result = MagicMock(items=[tenant], has_next=False, total=1) with ( app.test_request_context("/all-workspaces", query_string={"page": 1, "limit": 20}), @@ -329,25 +324,12 @@ class TestWorkspaceListApi: api = WorkspaceListApi() method = unwrap(api.get) - tenant = MagicMock( - id="t1", - name="T", - status="active", - created_at=naive_utc_now(), - ) - - paginate_result = MagicMock( - items=[tenant], - has_next=True, - total=10, - ) + tenant = MagicMock(id="t1", name="T", status="active", created_at=naive_utc_now()) + paginate_result = MagicMock(items=[tenant], has_next=True, total=10) with ( app.test_request_context("/all-workspaces", query_string={"page": 1, "limit": 1}), - patch( - "controllers.console.workspace.workspace.db.paginate", - return_value=paginate_result, - ), + patch("controllers.console.workspace.workspace.db.paginate", return_value=paginate_result), ): result, status = method(api) diff --git a/packages/contracts/generated/api/console/admin/orpc.gen.ts b/packages/contracts/generated/api/console/admin/orpc.gen.ts deleted file mode 100644 index 93f012405a..0000000000 --- a/packages/contracts/generated/api/console/admin/orpc.gen.ts +++ /dev/null @@ -1,153 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { oc } from '@orpc/contract' -import * as z from 'zod' - -import { - zDeleteAdminDeleteExploreBannerByBannerIdPath, - zDeleteAdminDeleteExploreBannerByBannerIdResponse, - zDeleteAdminInsertExploreAppsByAppIdPath, - zDeleteAdminInsertExploreAppsByAppIdResponse, - zPostAdminBatchAddNotificationAccountsResponse, - zPostAdminInsertExploreAppsBody, - zPostAdminInsertExploreAppsResponse, - zPostAdminInsertExploreBannerBody, - zPostAdminInsertExploreBannerResponse, - zPostAdminUpsertNotificationBody, - zPostAdminUpsertNotificationResponse, -} from './zod.gen' - -/** - * Register target accounts for a notification by email address. JSON body: {"notification_id": "...", "user_email": ["a@example.com", ...]}. File upload: multipart/form-data with a 'file' field (CSV or TXT, one email per line) plus a 'notification_id' field. Emails that do not match any account are silently skipped. - */ -export const post = oc - .route({ - description: - 'Register target accounts for a notification by email address. JSON body: {"notification_id": "...", "user_email": ["a@example.com", ...]}. File upload: multipart/form-data with a \'file\' field (CSV or TXT, one email per line) plus a \'notification_id\' field. Emails that do not match any account are silently skipped.', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAdminBatchAddNotificationAccounts', - path: '/admin/batch_add_notification_accounts', - tags: ['console'], - }) - .output(zPostAdminBatchAddNotificationAccountsResponse) - -export const batchAddNotificationAccounts = { - post, -} - -/** - * Delete an explore banner - */ -export const delete_ = oc - .route({ - description: 'Delete an explore banner', - inputStructure: 'detailed', - method: 'DELETE', - operationId: 'deleteAdminDeleteExploreBannerByBannerId', - path: '/admin/delete-explore-banner/{banner_id}', - successStatus: 204, - tags: ['console'], - }) - .input(z.object({ params: zDeleteAdminDeleteExploreBannerByBannerIdPath })) - .output(zDeleteAdminDeleteExploreBannerByBannerIdResponse) - -export const byBannerId = { - delete: delete_, -} - -export const deleteExploreBanner = { - byBannerId, -} - -/** - * Remove an app from the explore list - */ -export const delete2 = oc - .route({ - description: 'Remove an app from the explore list', - inputStructure: 'detailed', - method: 'DELETE', - operationId: 'deleteAdminInsertExploreAppsByAppId', - path: '/admin/insert-explore-apps/{app_id}', - successStatus: 204, - tags: ['console'], - }) - .input(z.object({ params: zDeleteAdminInsertExploreAppsByAppIdPath })) - .output(zDeleteAdminInsertExploreAppsByAppIdResponse) - -export const byAppId = { - delete: delete2, -} - -/** - * Insert or update an app in the explore list - */ -export const post2 = oc - .route({ - description: 'Insert or update an app in the explore list', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAdminInsertExploreApps', - path: '/admin/insert-explore-apps', - tags: ['console'], - }) - .input(z.object({ body: zPostAdminInsertExploreAppsBody })) - .output(zPostAdminInsertExploreAppsResponse) - -export const insertExploreApps = { - post: post2, - byAppId, -} - -/** - * Insert an explore banner - */ -export const post3 = oc - .route({ - description: 'Insert an explore banner', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAdminInsertExploreBanner', - path: '/admin/insert-explore-banner', - successStatus: 201, - tags: ['console'], - }) - .input(z.object({ body: zPostAdminInsertExploreBannerBody })) - .output(zPostAdminInsertExploreBannerResponse) - -export const insertExploreBanner = { - post: post3, -} - -/** - * Create or update an in-product notification. Supply notification_id to update an existing one; omit it to create a new one. Pass at least one language variant in contents (zh / en / jp). - */ -export const post4 = oc - .route({ - description: - 'Create or update an in-product notification. Supply notification_id to update an existing one; omit it to create a new one. Pass at least one language variant in contents (zh / en / jp).', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAdminUpsertNotification', - path: '/admin/upsert_notification', - tags: ['console'], - }) - .input(z.object({ body: zPostAdminUpsertNotificationBody })) - .output(zPostAdminUpsertNotificationResponse) - -export const upsertNotification = { - post: post4, -} - -export const admin = { - batchAddNotificationAccounts, - deleteExploreBanner, - insertExploreApps, - insertExploreBanner, - upsertNotification, -} - -export const contract = { - admin, -} diff --git a/packages/contracts/generated/api/console/admin/types.gen.ts b/packages/contracts/generated/api/console/admin/types.gen.ts deleted file mode 100644 index d3fef01791..0000000000 --- a/packages/contracts/generated/api/console/admin/types.gen.ts +++ /dev/null @@ -1,157 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}/console/api` | (string & {}) -} - -export type InsertExploreAppPayload = { - app_id: string - can_trial?: boolean - category: string - copyright?: string | null - custom_disclaimer?: string | null - desc?: string | null - language: string - position: number - privacy_policy?: string | null - trial_limit?: number -} - -export type InsertExploreBannerPayload = { - 'category': string - 'description': string - 'img-src': string - 'language'?: string - 'link': string - 'sort': number - 'title': string -} - -export type UpsertNotificationPayload = { - contents: Array - end_time?: string | null - frequency?: string - notification_id?: string | null - start_time?: string | null - status?: string -} - -export type LangContentPayload = { - body: string - lang: string - subtitle?: string | null - title: string - title_pic_url?: string | null -} - -export type PostAdminBatchAddNotificationAccountsData = { - body?: never - path?: never - query?: never - url: '/admin/batch_add_notification_accounts' -} - -export type PostAdminBatchAddNotificationAccountsResponses = { - 200: { - [key: string]: unknown - } -} - -export type PostAdminBatchAddNotificationAccountsResponse - = PostAdminBatchAddNotificationAccountsResponses[keyof PostAdminBatchAddNotificationAccountsResponses] - -export type DeleteAdminDeleteExploreBannerByBannerIdData = { - body?: never - path: { - banner_id: string - } - query?: never - url: '/admin/delete-explore-banner/{banner_id}' -} - -export type DeleteAdminDeleteExploreBannerByBannerIdResponses = { - 204: { - [key: string]: unknown - } -} - -export type DeleteAdminDeleteExploreBannerByBannerIdResponse - = DeleteAdminDeleteExploreBannerByBannerIdResponses[keyof DeleteAdminDeleteExploreBannerByBannerIdResponses] - -export type PostAdminInsertExploreAppsData = { - body: InsertExploreAppPayload - path?: never - query?: never - url: '/admin/insert-explore-apps' -} - -export type PostAdminInsertExploreAppsErrors = { - 404: { - [key: string]: unknown - } -} - -export type PostAdminInsertExploreAppsError - = PostAdminInsertExploreAppsErrors[keyof PostAdminInsertExploreAppsErrors] - -export type PostAdminInsertExploreAppsResponses = { - 200: { - [key: string]: unknown - } - 201: { - [key: string]: unknown - } -} - -export type PostAdminInsertExploreAppsResponse - = PostAdminInsertExploreAppsResponses[keyof PostAdminInsertExploreAppsResponses] - -export type DeleteAdminInsertExploreAppsByAppIdData = { - body?: never - path: { - app_id: string - } - query?: never - url: '/admin/insert-explore-apps/{app_id}' -} - -export type DeleteAdminInsertExploreAppsByAppIdResponses = { - 204: { - [key: string]: unknown - } -} - -export type DeleteAdminInsertExploreAppsByAppIdResponse - = DeleteAdminInsertExploreAppsByAppIdResponses[keyof DeleteAdminInsertExploreAppsByAppIdResponses] - -export type PostAdminInsertExploreBannerData = { - body: InsertExploreBannerPayload - path?: never - query?: never - url: '/admin/insert-explore-banner' -} - -export type PostAdminInsertExploreBannerResponses = { - 201: { - [key: string]: unknown - } -} - -export type PostAdminInsertExploreBannerResponse - = PostAdminInsertExploreBannerResponses[keyof PostAdminInsertExploreBannerResponses] - -export type PostAdminUpsertNotificationData = { - body: UpsertNotificationPayload - path?: never - query?: never - url: '/admin/upsert_notification' -} - -export type PostAdminUpsertNotificationResponses = { - 200: { - [key: string]: unknown - } -} - -export type PostAdminUpsertNotificationResponse - = PostAdminUpsertNotificationResponses[keyof PostAdminUpsertNotificationResponses] diff --git a/packages/contracts/generated/api/console/admin/zod.gen.ts b/packages/contracts/generated/api/console/admin/zod.gen.ts deleted file mode 100644 index 9ebed93e1e..0000000000 --- a/packages/contracts/generated/api/console/admin/zod.gen.ts +++ /dev/null @@ -1,99 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod' - -/** - * InsertExploreAppPayload - */ -export const zInsertExploreAppPayload = z.object({ - app_id: z.string(), - can_trial: z.boolean().optional().default(false), - category: z.string(), - copyright: z.string().nullish(), - custom_disclaimer: z.string().nullish(), - desc: z.string().nullish(), - language: z.string(), - position: z.int(), - privacy_policy: z.string().nullish(), - trial_limit: z.int().optional().default(0), -}) - -/** - * InsertExploreBannerPayload - */ -export const zInsertExploreBannerPayload = z.object({ - 'category': z.string(), - 'description': z.string(), - 'img-src': z.string(), - 'language': z.string().optional().default('en-US'), - 'link': z.string(), - 'sort': z.int(), - 'title': z.string(), -}) - -/** - * LangContentPayload - */ -export const zLangContentPayload = z.object({ - body: z.string(), - lang: z.string(), - subtitle: z.string().nullish(), - title: z.string(), - title_pic_url: z.string().nullish(), -}) - -/** - * UpsertNotificationPayload - */ -export const zUpsertNotificationPayload = z.object({ - contents: z.array(zLangContentPayload).min(1), - end_time: z.string().nullish(), - frequency: z.string().optional().default('once'), - notification_id: z.string().nullish(), - start_time: z.string().nullish(), - status: z.string().optional().default('active'), -}) - -/** - * Accounts added successfully - */ -export const zPostAdminBatchAddNotificationAccountsResponse = z.record(z.string(), z.unknown()) - -export const zDeleteAdminDeleteExploreBannerByBannerIdPath = z.object({ - banner_id: z.string(), -}) - -/** - * Banner deleted successfully - */ -export const zDeleteAdminDeleteExploreBannerByBannerIdResponse = z.record(z.string(), z.unknown()) - -export const zPostAdminInsertExploreAppsBody = zInsertExploreAppPayload - -export const zPostAdminInsertExploreAppsResponse = z.union([ - z.record(z.string(), z.unknown()), - z.record(z.string(), z.unknown()), -]) - -export const zDeleteAdminInsertExploreAppsByAppIdPath = z.object({ - app_id: z.string(), -}) - -/** - * App removed successfully - */ -export const zDeleteAdminInsertExploreAppsByAppIdResponse = z.record(z.string(), z.unknown()) - -export const zPostAdminInsertExploreBannerBody = zInsertExploreBannerPayload - -/** - * Banner inserted successfully - */ -export const zPostAdminInsertExploreBannerResponse = z.record(z.string(), z.unknown()) - -export const zPostAdminUpsertNotificationBody = zUpsertNotificationPayload - -/** - * Notification upserted successfully - */ -export const zPostAdminUpsertNotificationResponse = z.record(z.string(), z.unknown())