diff --git a/api/apps/restful_apis/config_api.py b/api/apps/restful_apis/config_api.py new file mode 100644 index 0000000000..e5b8ed394e --- /dev/null +++ b/api/apps/restful_apis/config_api.py @@ -0,0 +1,71 @@ +# +# Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from api.apps import login_required +from api.utils.api_utils import get_json_result, get_data_error_result +from common.log_utils import get_log_levels, set_log_level + +@manager.route("/config/log", methods=["GET"]) # noqa: F821 +@login_required +async def get_logger_levels(): + """ + Get current log levels for all packages. + --- + tags: + - System + responses: + 200: + description: Return current log levels + """ + return get_json_result(data=get_log_levels()) + + +@manager.route("/config/log", methods=["PUT"]) # noqa: F821 +@login_required +async def set_logger_level(): + """ + Set log level for a package. + --- + tags: + - System + parameters: + - in: body + name: body + required: true + schema: + type: object + properties: + pkg_name: + type: string + description: Package name (e.g., "rag.utils.es_conn") + level: + type: string + description: Log level (DEBUG, INFO, WARNING, ERROR) + responses: + 200: + description: Log level updated successfully + """ + from quart import request + data = await request.get_json() + if not data or "pkg_name" not in data or "level" not in data: + return get_data_error_result(message="pkg_name and level are required") + pkg_name = data["pkg_name"] + level = data["level"] + success = set_log_level(pkg_name, level) + if success: + return get_json_result(data={"pkg_name": pkg_name, "level": level}) + else: + return get_data_error_result(message=f"Invalid log level: {level}") diff --git a/api/apps/restful_apis/system_api.py b/api/apps/restful_apis/system_api.py index b6b4e0e56f..b65f2b6707 100644 --- a/api/apps/restful_apis/system_api.py +++ b/api/apps/restful_apis/system_api.py @@ -14,10 +14,21 @@ # limitations under the License. # -from api.apps import login_required +from quart import jsonify -from api.utils.api_utils import get_json_result +from api.apps import login_required, current_user +from api.utils.api_utils import get_json_result, get_data_error_result, server_error_response, generate_confirmation_token +from api.utils.health_utils import run_health_checks from common.versions import get_ragflow_version +from datetime import datetime +from common.time_utils import current_timestamp, datetime_format +from api.db.db_models import APIToken +from api.db.services.api_service import APITokenService +from api.db.services.user_service import UserTenantService + +@manager.route("/system/ping", methods=["GET"]) # noqa: F821 +async def ping(): + return "pong", 200 @manager.route("/system/version", methods=["GET"]) # noqa: F821 @login_required @@ -39,4 +50,144 @@ def version(): type: string description: Version number. """ - return get_json_result(data=get_ragflow_version()) \ No newline at end of file + return get_json_result(data=get_ragflow_version()) + +@manager.route("/system/healthz", methods=["GET"]) # noqa: F821 +def healthz(): + result, all_ok = run_health_checks() + return jsonify(result), (200 if all_ok else 500) + +@manager.route("/system/tokens", methods=["GET"]) # noqa: F821 +@login_required +def token_list(): + """ + List all API tokens for the current user. + --- + tags: + - API Tokens + security: + - ApiKeyAuth: [] + responses: + 200: + description: List of API tokens. + schema: + type: object + properties: + tokens: + type: array + items: + type: object + properties: + token: + type: string + description: The API token. + name: + type: string + description: Name of the token. + create_time: + type: string + description: Token creation time. + """ + try: + tenants = UserTenantService.query(user_id=current_user.id) + if not tenants: + return get_data_error_result(message="Tenant not found!") + + tenant_id = [tenant for tenant in tenants if tenant.role == "owner"][0].tenant_id + objs = APITokenService.query(tenant_id=tenant_id) + objs = [o.to_dict() for o in objs] + for o in objs: + if not o["beta"]: + o["beta"] = generate_confirmation_token().replace("ragflow-", "")[:32] + APITokenService.filter_update([APIToken.tenant_id == tenant_id, APIToken.token == o["token"]], o) + return get_json_result(data=objs) + except Exception as e: + return server_error_response(e) + + +@manager.route("/system/tokens", methods=["POST"]) # noqa: F821 +@login_required +def new_token(): + """ + Generate a new API token. + --- + tags: + - API Tokens + security: + - ApiKeyAuth: [] + parameters: + - in: query + name: name + type: string + required: false + description: Name of the token. + responses: + 200: + description: Token generated successfully. + schema: + type: object + properties: + token: + type: string + description: The generated API token. + """ + try: + tenants = UserTenantService.query(user_id=current_user.id) + if not tenants: + return get_data_error_result(message="Tenant not found!") + + tenant_id = [tenant for tenant in tenants if tenant.role == "owner"][0].tenant_id + obj = { + "tenant_id": tenant_id, + "token": generate_confirmation_token(), + "beta": generate_confirmation_token().replace("ragflow-", "")[:32], + "create_time": current_timestamp(), + "create_date": datetime_format(datetime.now()), + "update_time": None, + "update_date": None, + } + + if not APITokenService.save(**obj): + return get_data_error_result(message="Fail to new a dialog!") + + return get_json_result(data=obj) + except Exception as e: + return server_error_response(e) + + +@manager.route("/system/tokens/", methods=["DELETE"]) # noqa: F821 +@login_required +def rm(token): + """ + Remove an API token. + --- + tags: + - API Tokens + security: + - ApiKeyAuth: [] + parameters: + - in: path + name: token + type: string + required: true + description: The API token to remove. + responses: + 200: + description: Token removed successfully. + schema: + type: object + properties: + success: + type: boolean + description: Deletion status. + """ + try: + tenants = UserTenantService.query(user_id=current_user.id) + if not tenants: + return get_data_error_result(message="Tenant not found!") + + tenant_id = tenants[0].tenant_id + APITokenService.filter_delete([APIToken.tenant_id == tenant_id, APIToken.token == token]) + return get_json_result(data=True) + except Exception as e: + return server_error_response(e) \ No newline at end of file diff --git a/api/apps/system_app.py b/api/apps/system_app.py index fd10492f5b..833a7819dd 100644 --- a/api/apps/system_app.py +++ b/api/apps/system_app.py @@ -17,25 +17,17 @@ import logging from datetime import datetime import json -from api.apps import login_required, current_user +from api.apps import login_required -from api.db.db_models import APIToken -from api.db.services.api_service import APITokenService from api.db.services.knowledgebase_service import KnowledgebaseService -from api.db.services.user_service import UserTenantService from api.utils.api_utils import ( get_json_result, - get_data_error_result, - server_error_response, - generate_confirmation_token, ) -from common.time_utils import current_timestamp, datetime_format -from common.log_utils import get_log_levels, set_log_level + from timeit import default_timer as timer from rag.utils.redis_conn import REDIS_CONN -from quart import jsonify -from api.utils.health_utils import run_health_checks, get_oceanbase_status +from api.utils.health_utils import get_oceanbase_status from common import settings @manager.route("/status", methods=["GET"]) # noqa: F821 @@ -146,18 +138,6 @@ def status(): return get_json_result(data=res) - -@manager.route("/healthz", methods=["GET"]) # noqa: F821 -def healthz(): - result, all_ok = run_health_checks() - return jsonify(result), (200 if all_ok else 500) - - -@manager.route("/ping", methods=["GET"]) # noqa: F821 -async def ping(): - return "pong", 200 - - @manager.route("/oceanbase/status", methods=["GET"]) # noqa: F821 @login_required def oceanbase_status(): @@ -194,142 +174,6 @@ def oceanbase_status(): ) -@manager.route("/new_token", methods=["POST"]) # noqa: F821 -@login_required -def new_token(): - """ - Generate a new API token. - --- - tags: - - API Tokens - security: - - ApiKeyAuth: [] - parameters: - - in: query - name: name - type: string - required: false - description: Name of the token. - responses: - 200: - description: Token generated successfully. - schema: - type: object - properties: - token: - type: string - description: The generated API token. - """ - try: - tenants = UserTenantService.query(user_id=current_user.id) - if not tenants: - return get_data_error_result(message="Tenant not found!") - - tenant_id = [tenant for tenant in tenants if tenant.role == "owner"][0].tenant_id - obj = { - "tenant_id": tenant_id, - "token": generate_confirmation_token(), - "beta": generate_confirmation_token().replace("ragflow-", "")[:32], - "create_time": current_timestamp(), - "create_date": datetime_format(datetime.now()), - "update_time": None, - "update_date": None, - } - - if not APITokenService.save(**obj): - return get_data_error_result(message="Fail to new a dialog!") - - return get_json_result(data=obj) - except Exception as e: - return server_error_response(e) - - -@manager.route("/token_list", methods=["GET"]) # noqa: F821 -@login_required -def token_list(): - """ - List all API tokens for the current user. - --- - tags: - - API Tokens - security: - - ApiKeyAuth: [] - responses: - 200: - description: List of API tokens. - schema: - type: object - properties: - tokens: - type: array - items: - type: object - properties: - token: - type: string - description: The API token. - name: - type: string - description: Name of the token. - create_time: - type: string - description: Token creation time. - """ - try: - tenants = UserTenantService.query(user_id=current_user.id) - if not tenants: - return get_data_error_result(message="Tenant not found!") - - tenant_id = [tenant for tenant in tenants if tenant.role == "owner"][0].tenant_id - objs = APITokenService.query(tenant_id=tenant_id) - objs = [o.to_dict() for o in objs] - for o in objs: - if not o["beta"]: - o["beta"] = generate_confirmation_token().replace("ragflow-", "")[:32] - APITokenService.filter_update([APIToken.tenant_id == tenant_id, APIToken.token == o["token"]], o) - return get_json_result(data=objs) - except Exception as e: - return server_error_response(e) - - -@manager.route("/token/", methods=["DELETE"]) # noqa: F821 -@login_required -def rm(token): - """ - Remove an API token. - --- - tags: - - API Tokens - security: - - ApiKeyAuth: [] - parameters: - - in: path - name: token - type: string - required: true - description: The API token to remove. - responses: - 200: - description: Token removed successfully. - schema: - type: object - properties: - success: - type: boolean - description: Deletion status. - """ - try: - tenants = UserTenantService.query(user_id=current_user.id) - if not tenants: - return get_data_error_result(message="Tenant not found!") - - tenant_id = tenants[0].tenant_id - APITokenService.filter_delete([APIToken.tenant_id == tenant_id, APIToken.token == token]) - return get_json_result(data=True) - except Exception as e: - return server_error_response(e) - - @manager.route("/config", methods=["GET"]) # noqa: F821 def get_config(): """ @@ -351,56 +195,3 @@ def get_config(): "registerEnabled": settings.REGISTER_ENABLED, "disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN, }) - - -@manager.route("/log_levels", methods=["GET"]) # noqa: F821 -@login_required -async def get_logger_levels(): - """ - Get current log levels for all packages. - --- - tags: - - System - responses: - 200: - description: Return current log levels - """ - return get_json_result(data=get_log_levels()) - - -@manager.route("/log_levels", methods=["PUT"]) # noqa: F821 -@login_required -async def set_logger_level(): - """ - Set log level for a package. - --- - tags: - - System - parameters: - - in: body - name: body - required: true - schema: - type: object - properties: - pkg_name: - type: string - description: Package name (e.g., "rag.utils.es_conn") - level: - type: string - description: Log level (DEBUG, INFO, WARNING, ERROR) - responses: - 200: - description: Log level updated successfully - """ - from quart import request - data = await request.get_json() - if not data or "pkg_name" not in data or "level" not in data: - return get_data_error_result(message="pkg_name and level are required") - pkg_name = data["pkg_name"] - level = data["level"] - success = set_log_level(pkg_name, level) - if success: - return get_json_result(data={"pkg_name": pkg_name, "level": level}) - else: - return get_data_error_result(message=f"Invalid log level: {level}") diff --git a/api/utils/health_utils.py b/api/utils/health_utils.py index f8222a291f..288eb79ff6 100644 --- a/api/utils/health_utils.py +++ b/api/utils/health_utils.py @@ -290,7 +290,7 @@ def get_redis_info(): def check_ragflow_server_alive(): start_time = timer() try: - url = f'http://{settings.HOST_IP}:{settings.HOST_PORT}/v1/system/ping' + url = f'http://{settings.HOST_IP}:{settings.HOST_PORT}/api/v1/system/ping' if '0.0.0.0' in url: url = url.replace('0.0.0.0', '127.0.0.1') response = requests.get(url) diff --git a/docs/references/http_api_reference.md b/docs/references/http_api_reference.md index ac0fc58035..bfe0ef00ac 100644 --- a/docs/references/http_api_reference.md +++ b/docs/references/http_api_reference.md @@ -6841,14 +6841,14 @@ Failure ### Check system health -**GET** `/v1/system/healthz` +**GET** `/api/v1/system/healthz` Check the health status of RAGFlow’s dependencies (database, Redis, document engine, object storage). #### Request - Method: GET -- URL: `/v1/system/healthz` +- URL: `/api/v1/system/healthz` - Headers: - 'Content-Type: application/json' (no Authorization required) @@ -6857,7 +6857,7 @@ Check the health status of RAGFlow’s dependencies (database, Redis, document e ```bash curl --request GET - --url http://{address}/v1/system/healthz + --url http://{address}/api/v1/system/healthz --header 'Content-Type: application/json' ``` diff --git a/sdk/python/test/conftest.py b/sdk/python/test/conftest.py index 5aaaf8c1bc..a6ba0ea4e4 100644 --- a/sdk/python/test/conftest.py +++ b/sdk/python/test/conftest.py @@ -67,7 +67,7 @@ def get_api_key_fixture(): except Exception as e: print(e) auth = login() - url = HOST_ADDRESS + "/v1/system/new_token" + url = HOST_ADDRESS + "/v1/system/tokens" auth = {"Authorization": auth} response = requests.post(url=url, headers=auth) res = response.json() diff --git a/test/benchmark/auth.py b/test/benchmark/auth.py index 307dd4ed82..d9c9355d3e 100644 --- a/test/benchmark/auth.py +++ b/test/benchmark/auth.py @@ -45,7 +45,7 @@ def login_user(client: HttpClient, email: str, password_enc: str) -> str: def create_api_token(client: HttpClient, login_token: str, token_name: Optional[str] = None) -> str: client.login_token = login_token params = {"name": token_name} if token_name else None - res = client.request_json("POST", "/system/new_token", use_api_base=False, auth_kind="login", params=params) + res = client.request_json("POST", "/system/tokens", use_api_base=False, auth_kind="login", params=params) if res.get("code") != 0: raise AuthError(f"API token creation failed: {res.get('message')}") token = res.get("data", {}).get("token") diff --git a/test/testcases/conftest.py b/test/testcases/conftest.py index 16efb00b69..22fc01ed0b 100644 --- a/test/testcases/conftest.py +++ b/test/testcases/conftest.py @@ -160,7 +160,7 @@ def auth(): @pytest.fixture(scope="session") def token(auth): - url = HOST_ADDRESS + f"/{VERSION}/system/new_token" + url = HOST_ADDRESS + f"/api/{VERSION}/system/tokens" auth = {"Authorization": auth} response = requests.post(url=url, headers=auth) res = response.json() diff --git a/test/testcases/test_web_api/common.py b/test/testcases/test_web_api/common.py index 3d82bc009b..8b87698805 100644 --- a/test/testcases/test_web_api/common.py +++ b/test/testcases/test_web_api/common.py @@ -94,17 +94,17 @@ def api_stats(auth, params=None, *, headers=HEADERS): # SYSTEM APP def system_new_token(auth, payload=None, *, headers=HEADERS, data=None): - res = requests.post(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/new_token", headers=headers, auth=auth, json=payload, data=data) + res = requests.post(url=f"{HOST_ADDRESS}{SYSTEM_API_URL}/tokens", headers=headers, auth=auth, json=payload, data=data) return res.json() def system_token_list(auth, params=None, *, headers=HEADERS): - res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/token_list", headers=headers, auth=auth, params=params) + res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_API_URL}/tokens", headers=headers, auth=auth, params=params) return res.json() def system_delete_token(auth, token, *, headers=HEADERS): - res = requests.delete(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/token/{token}", headers=headers, auth=auth) + res = requests.delete(url=f"{HOST_ADDRESS}{SYSTEM_API_URL}/tokens/{token}", headers=headers, auth=auth) return res.json() diff --git a/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py b/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py index a00ecdbb6f..f3e52d89e6 100644 --- a/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py +++ b/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py @@ -214,106 +214,6 @@ def test_status_branch_matrix_unit(monkeypatch): assert "Lost connection!" in res["data"]["redis"]["error"] assert res["data"]["task_executor_heartbeats"] == {} - -@pytest.mark.p2 -def test_healthz_and_oceanbase_status_matrix_unit(monkeypatch): - module = _load_system_module(monkeypatch) - - monkeypatch.setattr(module, "run_health_checks", lambda: ({"status": "ok"}, True)) - payload, status = module.healthz() - assert status == 200 - assert payload["status"] == "ok" - - monkeypatch.setattr(module, "run_health_checks", lambda: ({"status": "degraded"}, False)) - payload, status = module.healthz() - assert status == 500 - assert payload["status"] == "degraded" - - monkeypatch.setattr(module, "get_oceanbase_status", lambda: {"status": "alive", "latency_ms": 8}) - res = module.oceanbase_status() - assert res["code"] == 0 - assert res["data"]["status"] == "alive" - - monkeypatch.setattr(module, "get_oceanbase_status", lambda: (_ for _ in ()).throw(RuntimeError("ocean boom"))) - res = module.oceanbase_status() - assert res["code"] == 500 - assert res["data"]["status"] == "error" - assert "ocean boom" in res["data"]["message"] - - -@pytest.mark.p2 -def test_system_token_routes_matrix_unit(monkeypatch): - module = _load_system_module(monkeypatch) - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: []) - res = module.new_token() - assert res["message"] == "Tenant not found!" - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(role="owner", tenant_id="tenant-1")]) - monkeypatch.setattr(module.APITokenService, "save", lambda **_kwargs: False) - res = module.new_token() - assert res["message"] == "Fail to new a dialog!" - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("tenant query boom"))) - res = module.new_token() - assert res["code"] == 100 - assert "tenant query boom" in res["message"] - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: []) - res = module.token_list() - assert res["message"] == "Tenant not found!" - - class _Token: - def __init__(self, token, beta): - self.token = token - self.beta = beta - - def to_dict(self): - return {"token": self.token, "beta": self.beta} - - filter_updates = [] - monkeypatch.setattr(module, "generate_confirmation_token", lambda: "ragflow-abcdefghijklmnopqrstuvwxyz0123456789") - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(role="owner", tenant_id="tenant-9")]) - monkeypatch.setattr(module.APITokenService, "query", lambda **_kwargs: [_Token("tok-1", ""), _Token("tok-2", "beta-2")]) - monkeypatch.setattr(module.APITokenService, "filter_update", lambda conds, payload: filter_updates.append((conds, payload))) - res = module.token_list() - assert res["code"] == 0 - assert len(res["data"]) == 2 - assert len(res["data"][0]["beta"]) == 32 - assert res["data"][1]["beta"] == "beta-2" - assert len(filter_updates) == 1 - - monkeypatch.setattr( - module.APITokenService, - "query", - lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("token list boom")), - ) - res = module.token_list() - assert res["code"] == 100 - assert "token list boom" in res["message"] - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: []) - res = module.rm("tok-1") - assert res["message"] == "Tenant not found!" - - deleted = [] - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(role="owner", tenant_id="tenant-3")]) - monkeypatch.setattr(module.APITokenService, "filter_delete", lambda conds: deleted.append(conds)) - res = module.rm("tok-1") - assert res["code"] == 0 - assert res["data"] is True - assert deleted - - monkeypatch.setattr( - module.APITokenService, - "filter_delete", - lambda _conds: (_ for _ in ()).throw(RuntimeError("delete boom")), - ) - res = module.rm("tok-1") - assert res["code"] == 100 - assert "delete boom" in res["message"] - - @pytest.mark.p2 def test_get_config_returns_register_enabled_unit(monkeypatch): module = _load_system_module(monkeypatch) diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 16c6454bad..480efba33d 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -171,9 +171,9 @@ export default { // system getSystemVersion: `${restAPIv1}/system/version`, - getSystemTokenList: `${webAPI}/system/token_list`, - createSystemToken: `${webAPI}/system/new_token`, - removeSystemToken: `${webAPI}/system/token`, + getSystemTokenList: `${restAPIv1}/system/tokens`, + createSystemToken: `${restAPIv1}/system/tokens`, + removeSystemToken: `${restAPIv1}/system/tokens`, getSystemConfig: `${webAPI}/system/config`, setLangfuseConfig: `${webAPI}/langfuse/api_key`,