chore: remove obsolete admin console routes (#35637)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
非法操作 2026-05-13 16:08:50 +08:00 committed by GitHub
parent 2afa39cdcb
commit 9d545144ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 5 additions and 1882 deletions

View File

@ -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",

View File

@ -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/<uuid:app_id>")
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/<uuid:banner_id>")
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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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,
}

View File

@ -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<LangContentPayload>
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]

View File

@ -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())