diff --git a/api/apps/auth/README.md b/api/apps/auth/README.md index 372e75cfbd..8edab999f8 100644 --- a/api/apps/auth/README.md +++ b/api/apps/auth/README.md @@ -20,7 +20,7 @@ oauth_config = { "authorization_url": "https://your-oauth-provider.com/oauth/authorize", "token_url": "https://your-oauth-provider.com/oauth/token", "userinfo_url": "https://your-oauth-provider.com/oauth/userinfo", - "redirect_uri": "https://your-app.com/v1/user/oauth/callback/" + "redirect_uri": "https://your-app.com/api/v1/auth/oauth//callback" } # OIDC configuration @@ -29,7 +29,7 @@ oidc_config = { "issuer": "https://your-oauth-provider.com/oidc", "client_id": "your_client_id", "client_secret": "your_client_secret", - "redirect_uri": "https://your-app.com/v1/user/oauth/callback/" + "redirect_uri": "https://your-app.com/api/v1/auth/oauth//callback" } # Github OAuth configuration diff --git a/api/apps/user_app.py b/api/apps/restful_apis/user_api.py similarity index 75% rename from api/apps/user_app.py rename to api/apps/restful_apis/user_api.py index 7424899269..714453ac6f 100644 --- a/api/apps/user_app.py +++ b/api/apps/restful_apis/user_api.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import json import logging import string import os @@ -60,10 +59,9 @@ from api.utils.web_utils import ( captcha_key, ) from common import settings -from common.http_client import async_request -@manager.route("/login", methods=["POST", "GET"]) # noqa: F821 +@manager.route("/auth/login", methods=["POST"]) # noqa: F821 async def login(): """ User login endpoint. @@ -140,7 +138,7 @@ async def login(): ) -@manager.route("/login/channels", methods=["GET"]) # noqa: F821 +@manager.route("/auth/login/channels", methods=["GET"]) # noqa: F821 async def get_login_channels(): """ Get all supported authentication channels. @@ -161,7 +159,7 @@ async def get_login_channels(): return get_json_result(data=[], message=f"Load channels failure, error: {str(e)}", code=RetCode.EXCEPTION_ERROR) -@manager.route("/login/", methods=["GET"]) # noqa: F821 +@manager.route("/auth/login/", methods=["GET"]) # noqa: F821 async def oauth_login(channel): channel_config = settings.OAUTH_CONFIG.get(channel) if not channel_config: @@ -174,7 +172,7 @@ async def oauth_login(channel): return redirect(auth_url) -@manager.route("/oauth/callback/", methods=["GET"]) # noqa: F821 +@manager.route("/auth/oauth//callback", methods=["GET"]) # noqa: F821 async def oauth_callback(channel): """ Handle the OAuth/OIDC callback for various channels dynamically. @@ -269,224 +267,7 @@ async def oauth_callback(channel): return redirect(f"/?error={str(e)}") -@manager.route("/github_callback", methods=["GET"]) # noqa: F821 -async def github_callback(): - """ - **Deprecated**, Use `/oauth/callback/` instead. - - GitHub OAuth callback endpoint. - --- - tags: - - OAuth - parameters: - - in: query - name: code - type: string - required: true - description: Authorization code from GitHub. - responses: - 200: - description: Authentication successful. - schema: - type: object - """ - res = await async_request( - "POST", - settings.GITHUB_OAUTH.get("url"), - data={ - "client_id": settings.GITHUB_OAUTH.get("client_id"), - "client_secret": settings.GITHUB_OAUTH.get("secret_key"), - "code": request.args.get("code"), - }, - headers={"Accept": "application/json"}, - ) - res = res.json() - if "error" in res: - return redirect("/?error=%s" % res["error_description"]) - - if "user:email" not in res["scope"].split(","): - return redirect("/?error=user:email not in scope") - - session["access_token"] = res["access_token"] - session["access_token_from"] = "github" - user_info = await user_info_from_github(session["access_token"]) - email_address = user_info["email"] - users = UserService.query(email=email_address) - user_id = get_uuid() - if not users: - # User isn't try to register - try: - try: - avatar = await download_img(user_info["avatar_url"]) - except Exception as e: - logging.exception(e) - avatar = "" - users = user_register( - user_id, - { - "access_token": session["access_token"], - "email": email_address, - "avatar": avatar, - "nickname": user_info["login"], - "login_channel": "github", - "last_login_time": get_format_time(), - "is_superuser": False, - }, - ) - if not users: - raise Exception(f"Fail to register {email_address}.") - if len(users) > 1: - raise Exception(f"Same email: {email_address} exists!") - - # Try to log in - user = users[0] - login_user(user) - return redirect("/?auth=%s" % user.get_id()) - except Exception as e: - rollback_user_registration(user_id) - logging.exception(e) - return redirect("/?error=%s" % str(e)) - - # User has already registered, try to log in - user = users[0] - user.access_token = get_uuid() - if user and hasattr(user, 'is_active') and user.is_active == "0": - return redirect("/?error=user_inactive") - login_user(user) - user.save() - return redirect("/?auth=%s" % user.get_id()) - - -@manager.route("/feishu_callback", methods=["GET"]) # noqa: F821 -async def feishu_callback(): - """ - Feishu OAuth callback endpoint. - --- - tags: - - OAuth - parameters: - - in: query - name: code - type: string - required: true - description: Authorization code from Feishu. - responses: - 200: - description: Authentication successful. - schema: - type: object - """ - app_access_token_res = await async_request( - "POST", - settings.FEISHU_OAUTH.get("app_access_token_url"), - data=json.dumps( - { - "app_id": settings.FEISHU_OAUTH.get("app_id"), - "app_secret": settings.FEISHU_OAUTH.get("app_secret"), - } - ), - headers={"Content-Type": "application/json; charset=utf-8"}, - ) - app_access_token_res = app_access_token_res.json() - if app_access_token_res["code"] != 0: - return redirect("/?error=%s" % app_access_token_res) - - res = await async_request( - "POST", - settings.FEISHU_OAUTH.get("user_access_token_url"), - data=json.dumps( - { - "grant_type": settings.FEISHU_OAUTH.get("grant_type"), - "code": request.args.get("code"), - } - ), - headers={ - "Content-Type": "application/json; charset=utf-8", - "Authorization": f"Bearer {app_access_token_res['app_access_token']}", - }, - ) - res = res.json() - if res["code"] != 0: - return redirect("/?error=%s" % res["message"]) - - if "contact:user.email:readonly" not in res["data"]["scope"].split(): - return redirect("/?error=contact:user.email:readonly not in scope") - session["access_token"] = res["data"]["access_token"] - session["access_token_from"] = "feishu" - user_info = await user_info_from_feishu(session["access_token"]) - email_address = user_info["email"] - users = UserService.query(email=email_address) - user_id = get_uuid() - if not users: - # User isn't try to register - try: - try: - avatar = await download_img(user_info["avatar_url"]) - except Exception as e: - logging.exception(e) - avatar = "" - users = user_register( - user_id, - { - "access_token": session["access_token"], - "email": email_address, - "avatar": avatar, - "nickname": user_info["en_name"], - "login_channel": "feishu", - "last_login_time": get_format_time(), - "is_superuser": False, - }, - ) - if not users: - raise Exception(f"Fail to register {email_address}.") - if len(users) > 1: - raise Exception(f"Same email: {email_address} exists!") - - # Try to log in - user = users[0] - login_user(user) - return redirect("/?auth=%s" % user.get_id()) - except Exception as e: - rollback_user_registration(user_id) - logging.exception(e) - return redirect("/?error=%s" % str(e)) - - # User has already registered, try to log in - user = users[0] - if user and hasattr(user, 'is_active') and user.is_active == "0": - return redirect("/?error=user_inactive") - user.access_token = get_uuid() - login_user(user) - user.save() - return redirect("/?auth=%s" % user.get_id()) - - -async def user_info_from_feishu(access_token): - headers = { - "Content-Type": "application/json; charset=utf-8", - "Authorization": f"Bearer {access_token}", - } - res = await async_request("GET", "https://open.feishu.cn/open-apis/authen/v1/user_info", headers=headers) - user_info = res.json()["data"] - user_info["email"] = None if user_info.get("email") == "" else user_info["email"] - return user_info - - -async def user_info_from_github(access_token): - headers = {"Accept": "application/json", "Authorization": f"token {access_token}"} - res = await async_request("GET", f"https://api.github.com/user?access_token={access_token}", headers=headers) - user_info = res.json() - email_info_response = await async_request( - "GET", - f"https://api.github.com/user/emails?access_token={access_token}", - headers=headers, - ) - email_info = email_info_response.json() - user_info["email"] = next((email for email in email_info if email["primary"]), None)["email"] - return user_info - - -@manager.route("/logout", methods=["GET"]) # noqa: F821 +@manager.route("/auth/logout", methods=["POST"]) # noqa: F821 @login_required async def log_out(): """ @@ -508,7 +289,7 @@ async def log_out(): return get_json_result(data=True) -@manager.route("/setting", methods=["POST"]) # noqa: F821 +@manager.route("/users/me", methods=["PATCH"]) # noqa: F821 @login_required async def setting_user(): """ @@ -576,7 +357,7 @@ async def setting_user(): return get_json_result(data=False, message="Update failure!", code=RetCode.EXCEPTION_ERROR) -@manager.route("/info", methods=["GET"]) # noqa: F821 +@manager.route("/users/me", methods=["GET"]) # noqa: F821 @login_required async def user_profile(): """ @@ -667,7 +448,7 @@ def user_register(user_id, user): return UserService.query(email=user["email"]) -@manager.route("/register", methods=["POST"]) # noqa: F821 +@manager.route("/users", methods=["POST"]) # noqa: F821 @validate_request("nickname", "email", "password") async def user_add(): """ @@ -761,7 +542,7 @@ async def user_add(): ) -@manager.route("/tenant_info", methods=["GET"]) # noqa: F821 +@manager.route("/users/me/models", methods=["GET"]) # noqa: F821 @login_required async def tenant_info(): """ @@ -799,7 +580,7 @@ async def tenant_info(): return server_error_response(e) -@manager.route("/set_tenant_info", methods=["POST"]) # noqa: F821 +@manager.route("/users/me/models", methods=["PATCH"]) # noqa: F821 @login_required @validate_request("tenant_id", "asr_id", "embd_id", "img2txt_id", "llm_id") async def set_tenant_info(): @@ -849,7 +630,7 @@ async def set_tenant_info(): return server_error_response(e) -@manager.route("/forget/captcha", methods=["GET"]) # noqa: F821 +@manager.route("/auth/password/forgot/captcha", methods=["POST"]) # noqa: F821 async def forget_get_captcha(): """ GET /forget/captcha?email= @@ -877,7 +658,7 @@ async def forget_get_captcha(): return response -@manager.route("/forget/otp", methods=["POST"]) # noqa: F821 +@manager.route("/auth/password/forgot/otp", methods=["POST"]) # noqa: F821 async def forget_send_otp(): """ POST /forget/otp @@ -947,7 +728,7 @@ def _verified_key(email: str) -> str: return f"otp:verified:{email}" -@manager.route("/forget/verify-otp", methods=["POST"]) # noqa: F821 +@manager.route("/auth/password/forgot/otp/verify", methods=["POST"]) # noqa: F821 async def forget_verify_otp(): """ Verify email + OTP only. On success: @@ -1008,7 +789,7 @@ async def forget_verify_otp(): return get_json_result(data=True, code=RetCode.SUCCESS, message="otp verified") -@manager.route("/forget/reset-password", methods=["POST"]) # noqa: F821 +@manager.route("/auth/password/reset", methods=["POST"]) # noqa: F821 async def forget_reset_password(): """ Reset password after successful OTP verification. diff --git a/sdk/python/test/conftest.py b/sdk/python/test/conftest.py index a6ba0ea4e4..682a715923 100644 --- a/sdk/python/test/conftest.py +++ b/sdk/python/test/conftest.py @@ -40,7 +40,7 @@ X8f7fp9c7vUsfOCkM+gHY3PadG+QHa7KI7mzTKgUTZImK6BZtfRBATDTthEUbbaTewY4H0MnWiCeeDhc def register(): - url = HOST_ADDRESS + "/v1/user/register" + url = HOST_ADDRESS + "/api/v1/users" name = "user" register_data = {"email": EMAIL, "nickname": name, "password": PASSWORD} res = requests.post(url=url, json=register_data) @@ -50,7 +50,7 @@ def register(): def login(): - url = HOST_ADDRESS + "/v1/user/login" + url = HOST_ADDRESS + "/api/v1/auth/login" login_data = {"email": EMAIL, "password": PASSWORD} response = requests.post(url=url, json=login_data) res = response.json() @@ -119,7 +119,7 @@ def add_models(auth): def get_tenant_info(auth): - url = HOST_ADDRESS + "/v1/user/tenant_info" + url = HOST_ADDRESS + "/api/v1/users/me/models" authorization = {"Authorization": auth} response = requests.get(url=url, headers=authorization) res = response.json() @@ -136,7 +136,7 @@ def set_tenant_info(get_auth): tenant_id = get_tenant_info(auth) except Exception as e: pytest.exit(f"Error in set_tenant_info: {str(e)}") - url = HOST_ADDRESS + "/v1/user/set_tenant_info" + url = HOST_ADDRESS + "/api/v1/users/me/models" authorization = {"Authorization": get_auth} tenant_info = { "tenant_id": tenant_id, @@ -146,7 +146,7 @@ def set_tenant_info(get_auth): "asr_id": "", "tts_id": None, } - response = requests.post(url=url, headers=authorization, json=tenant_info) + response = requests.patch(url=url, headers=authorization, json=tenant_info) res = response.json() if res.get("code") != 0: raise Exception(res.get("message")) diff --git a/test/benchmark/README.md b/test/benchmark/README.md index 031d92d5b3..085f782621 100644 --- a/test/benchmark/README.md +++ b/test/benchmark/README.md @@ -55,7 +55,7 @@ Auth and bootstrap flags (used when --api-key is not provided) --login-password Login password (encrypted client-side). Requires pycryptodomex in the test group. --allow-register - Attempt /user/register before login (best effort). + Attempt /users before login (best effort). --token-name Optional API token name for /system/new_token. --bootstrap-llm @@ -70,7 +70,7 @@ Auth and bootstrap flags (used when --api-key is not provided) Optional LLM API base URL. Env: RAGFLOW_LLM_API_BASE --set-tenant-info - Set tenant defaults via /user/set_tenant_info. + Set tenant defaults via /users/me/models. --tenant-llm-id Tenant chat model ID. Env: RAGFLOW_TENANT_LLM_ID diff --git a/test/benchmark/auth.py b/test/benchmark/auth.py index d9c9355d3e..135907dafa 100644 --- a/test/benchmark/auth.py +++ b/test/benchmark/auth.py @@ -18,7 +18,7 @@ def encrypt_password(password_plain: str) -> str: def register_user(client: HttpClient, email: str, nickname: str, password_enc: str) -> None: payload = {"email": email, "nickname": nickname, "password": password_enc} - res = client.request_json("POST", "/user/register", use_api_base=False, auth_kind=None, json_body=payload) + res = client.request_json("POST", "/users", use_api_base=True, auth_kind=None, json_body=payload) if res.get("code") == 0: return msg = res.get("message", "") @@ -29,7 +29,7 @@ def register_user(client: HttpClient, email: str, nickname: str, password_enc: s def login_user(client: HttpClient, email: str, password_enc: str) -> str: payload = {"email": email, "password": password_enc} - response = client.request("POST", "/user/login", use_api_base=False, auth_kind=None, json_body=payload) + response = client.request("POST", "/auth/login", use_api_base=True, auth_kind=None, json_body=payload) try: res = response.json() except Exception as exc: @@ -76,13 +76,13 @@ def set_llm_api_key( def get_tenant_info(client: HttpClient) -> Dict[str, Any]: - res = client.request_json("GET", "/user/tenant_info", use_api_base=False, auth_kind="login") + res = client.request_json("GET", "/users/me/models", use_api_base=True, auth_kind="login") if res.get("code") != 0: raise AuthError(f"Failed to get tenant info: {res.get('message')}") return res.get("data", {}) def set_tenant_info(client: HttpClient, payload: Dict[str, Any]) -> None: - res = client.request_json("POST", "/user/set_tenant_info", use_api_base=False, auth_kind="login", json_body=payload) + res = client.request_json("PATCH", "/users/me/models", use_api_base=True, auth_kind="login", json_body=payload) if res.get("code") != 0: raise AuthError(f"Failed to set tenant info: {res.get('message')}") diff --git a/test/benchmark/cli.py b/test/benchmark/cli.py index 53a04321b6..971540aab3 100644 --- a/test/benchmark/cli.py +++ b/test/benchmark/cli.py @@ -59,7 +59,7 @@ def _parse_args() -> argparse.Namespace: base_parser.add_argument("--login-email", default=os.getenv("RAGFLOW_EMAIL"), help="Login email") base_parser.add_argument("--login-nickname", default=os.getenv("RAGFLOW_NICKNAME"), help="Nickname for registration") base_parser.add_argument("--login-password", help="Login password (encrypted client-side)") - base_parser.add_argument("--allow-register", action="store_true", help="Attempt /user/register before login") + base_parser.add_argument("--allow-register", action="store_true", help="Attempt /users before login") base_parser.add_argument("--token-name", help="Optional API token name") base_parser.add_argument("--bootstrap-llm", action="store_true", help="Ensure LLM factory API key is configured") base_parser.add_argument("--llm-factory", default=os.getenv("RAGFLOW_LLM_FACTORY"), help="LLM factory name") diff --git a/test/playwright/auth/test_register_success_optional.py b/test/playwright/auth/test_register_success_optional.py index 57337212d0..1b9cc4184a 100644 --- a/test/playwright/auth/test_register_success_optional.py +++ b/test/playwright/auth/test_register_success_optional.py @@ -167,7 +167,7 @@ def step_03_submit_registration( snap("retry_submitted" if retried else "submitted"), ), lambda resp: resp.request.method == "POST" - and "/v1/user/register" in resp.url, + and "/api/v1/users" in resp.url, timeout_ms=RESULT_TIMEOUT_MS, ) except PlaywrightTimeoutError as exc: diff --git a/test/playwright/auth/test_register_then_login_flow.py b/test/playwright/auth/test_register_then_login_flow.py index dc1ae5ee3d..5c4fce040e 100644 --- a/test/playwright/auth/test_register_then_login_flow.py +++ b/test/playwright/auth/test_register_then_login_flow.py @@ -172,7 +172,7 @@ def step_03_register_user( snap("register_submitted"), ), lambda resp: resp.request.method == "POST" - and "/v1/user/register" in resp.url, + and "/api/v1/users" in resp.url, timeout_ms=RESULT_TIMEOUT_MS, ) except PlaywrightTimeoutError as exc: diff --git a/test/playwright/auth/test_sso_optional.py b/test/playwright/auth/test_sso_optional.py index a33ab1feae..aae3c1c0fb 100644 --- a/test/playwright/auth/test_sso_optional.py +++ b/test/playwright/auth/test_sso_optional.py @@ -30,7 +30,7 @@ def step_02_initiate_sso(flow_page, flow_state, login_url, active_auth_context, if not clicked: pytest.skip("SSO buttons were present but not interactable") - page.wait_for_url(re.compile(r".*/v1/user/login/"), timeout=5000) + page.wait_for_url(re.compile(r".*/api/v1/auth/login/"), timeout=5000) flow_state["sso_clicked"] = True snap("sso_clicked") diff --git a/test/playwright/conftest.py b/test/playwright/conftest.py index 51cee55080..e73445129f 100644 --- a/test/playwright/conftest.py +++ b/test/playwright/conftest.py @@ -429,7 +429,7 @@ def _is_register_disabled_message(message: str) -> bool: def _api_register_user(base_url: str, email: str, password: str, nickname: str) -> None: - url = _build_url(base_url, "/v1/user/register") + url = _build_url(base_url, "/api/v1/users") encrypted_password = _rsa_encrypt_password(password) status, payload = _api_post_json( url, @@ -446,7 +446,7 @@ def _api_register_user(base_url: str, email: str, password: str, nickname: str) def _api_login_user(base_url: str, email: str, password: str) -> None: - url = _build_url(base_url, "/v1/user/login") + url = _build_url(base_url, "/api/v1/auth/login") encrypted_password = _rsa_encrypt_password(password) status, payload = _api_post_json( url, @@ -1047,7 +1047,7 @@ def _ensure_model_provider_ready_via_api(base_url: str, auth_header: str) -> dic pytest.skip("No model provider configured and ZHIPU_AI_API_KEY is not set.") _, tenant_payload = _api_request_json( - _build_url(base_url, "/v1/user/tenant_info"), headers=headers + _build_url(base_url, "/api/v1/users/me/models"), headers=headers ) tenant_data = _response_data(tenant_payload) tenant_id = tenant_data.get("tenant_id") @@ -1123,8 +1123,8 @@ def _ensure_model_provider_ready_via_api(base_url: str, auth_header: str) -> dic "tts_id": target_tts, } _, set_tenant_payload = _api_request_json( - _build_url(base_url, "/v1/user/set_tenant_info"), - method="POST", + _build_url(base_url, "/api/v1/users/me/models"), + method="PATCH", payload=tenant_payload, headers=headers, ) diff --git a/test/playwright/helpers/model_providers.py b/test/playwright/helpers/model_providers.py index 1d15775f8c..81b63f0b5b 100644 --- a/test/playwright/helpers/model_providers.py +++ b/test/playwright/helpers/model_providers.py @@ -306,8 +306,8 @@ def select_default_model( capture_response( page, trigger, - lambda resp: resp.request.method == "POST" - and "/v1/user/set_tenant_info" in resp.url, + lambda resp: resp.request.method == "PATCH" + and "/api/v1/users/me/models" in resp.url, ) except PlaywrightTimeoutError: if not selected[0]: diff --git a/test/testcases/conftest.py b/test/testcases/conftest.py index 22fc01ed0b..a4de7aebc8 100644 --- a/test/testcases/conftest.py +++ b/test/testcases/conftest.py @@ -128,7 +128,7 @@ def pytest_configure(config: pytest.Config) -> None: def register(): - url = HOST_ADDRESS + f"/{VERSION}/user/register" + url = HOST_ADDRESS + f"/api/{VERSION}/users" name = "qa" register_data = {"email": EMAIL, "nickname": name, "password": PASSWORD} res = requests.post(url=url, json=register_data) @@ -138,7 +138,7 @@ def register(): def login(): - url = HOST_ADDRESS + f"/{VERSION}/user/login" + url = HOST_ADDRESS + f"/api/{VERSION}/auth/login" login_data = {"email": EMAIL, "password": PASSWORD} response = requests.post(url=url, json=login_data) res = response.json() @@ -198,7 +198,7 @@ def add_models(auth): def get_tenant_info(auth): - url = HOST_ADDRESS + f"/{VERSION}/user/tenant_info" + url = HOST_ADDRESS + f"/api/{VERSION}/users/me/models" authorization = {"Authorization": auth} response = requests.get(url=url, headers=authorization) res = response.json() @@ -215,7 +215,7 @@ def set_tenant_info(auth): tenant_id = get_tenant_info(auth) except Exception as e: pytest.exit(f"Error in set_tenant_info: {str(e)}") - url = HOST_ADDRESS + f"/{VERSION}/user/set_tenant_info" + url = HOST_ADDRESS + f"/api/{VERSION}/users/me/models" authorization = {"Authorization": auth} tenant_info = { "tenant_id": tenant_id, @@ -225,7 +225,7 @@ def set_tenant_info(auth): "asr_id": "", "tts_id": None, } - response = requests.post(url=url, headers=authorization, json=tenant_info) + response = requests.patch(url=url, headers=authorization, json=tenant_info) res = response.json() if res.get("code") != 0: raise Exception(res.get("message")) diff --git a/test/testcases/test_admin_api/test_user_api_key_management/test_delete_user_api_key.py b/test/testcases/test_admin_api/test_user_api_key_management/test_delete_user_api_key.py index abbda6bbe1..6d91d3779d 100644 --- a/test/testcases/test_admin_api/test_user_api_key_management/test_delete_user_api_key.py +++ b/test/testcases/test_admin_api/test_user_api_key_management/test_delete_user_api_key.py @@ -151,7 +151,7 @@ class TestDeleteUserApiKey: user_name: str = EMAIL # create second user - url: str = HOST_ADDRESS + f"/{VERSION}/user/register" + url: str = HOST_ADDRESS + f"/api/{VERSION}/users" user2_email: str = "qa2@ragflow.io" register_data: dict[str, str] = {"email": user2_email, "nickname": "qa2", "password": PASSWORD} res: Any = requests.post(url=url, json=register_data) diff --git a/test/testcases/test_web_api/test_user_app/test_user_app_unit.py b/test/testcases/test_web_api/test_user_app/test_user_app_unit.py index e2c345c16b..fb576799e9 100644 --- a/test/testcases/test_web_api/test_user_app/test_user_app_unit.py +++ b/test/testcases/test_web_api/test_user_app/test_user_app_unit.py @@ -450,7 +450,7 @@ def _load_user_app(monkeypatch): monkeypatch.setitem(sys.modules, "rag.utils.redis_conn", redis_mod) module_name = "test_user_app_unit_module" - module_path = repo_root / "api" / "apps" / "user_app.py" + module_path = repo_root / "api" / "apps" / "restful_apis" / "user_api.py" spec = importlib.util.spec_from_file_location(module_name, module_path) module = importlib.util.module_from_spec(spec) module.manager = _DummyManager() @@ -689,236 +689,6 @@ def test_oauth_callback_matrix_unit(monkeypatch): assert login_calls and login_calls[-1] is existing_user -@pytest.mark.p2 -def test_github_callback_matrix_unit(monkeypatch): - module = _load_user_app(monkeypatch) - - _set_request_args(monkeypatch, module, {"code": "code"}) - module.session.clear() - - async def _request_error(_method, _url, **_kwargs): - return _DummyHTTPResponse({"error": "bad", "error_description": "boom"}) - - monkeypatch.setattr(module, "async_request", _request_error) - res = _run(module.github_callback()) - assert res["redirect"] == "/?error=boom" - - async def _request_scope_missing(_method, _url, **_kwargs): - return _DummyHTTPResponse({"scope": "repo", "access_token": "token-gh"}) - - monkeypatch.setattr(module, "async_request", _request_scope_missing) - res = _run(module.github_callback()) - assert res["redirect"] == "/?error=user:email not in scope" - - async def _request_token(_method, _url, **_kwargs): - return _DummyHTTPResponse({"scope": "user:email,repo", "access_token": "token-gh"}) - - monkeypatch.setattr(module, "async_request", _request_token) - monkeypatch.setattr( - module, - "user_info_from_github", - lambda _token: _AwaitableValue({"email": "gh@example.com", "avatar_url": "http://img", "login": "gh-user"}), - ) - monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: []) - rollback_calls = [] - monkeypatch.setattr(module, "rollback_user_registration", lambda user_id: rollback_calls.append(user_id)) - monkeypatch.setattr(module, "get_uuid", lambda: "gh-user-id") - - def _raise_download(_url): - raise RuntimeError("download explode") - - monkeypatch.setattr(module, "download_img", _raise_download) - monkeypatch.setattr(module, "user_register", lambda _user_id, _user: None) - res = _run(module.github_callback()) - assert "Fail to register gh@example.com." in res["redirect"] - assert rollback_calls == ["gh-user-id"] - - monkeypatch.setattr(module, "download_img", lambda _url: "avatar") - monkeypatch.setattr( - module, - "user_register", - lambda _user_id, _user: [_DummyUser("dup-1", "gh@example.com"), _DummyUser("dup-2", "gh@example.com")], - ) - rollback_calls.clear() - res = _run(module.github_callback()) - assert "Same email: gh@example.com exists!" in res["redirect"] - assert rollback_calls == ["gh-user-id"] - - new_user = _DummyUser("gh-new-user", "gh@example.com") - login_calls = [] - monkeypatch.setattr(module, "login_user", lambda user: login_calls.append(user)) - monkeypatch.setattr(module, "user_register", lambda _user_id, _user: [new_user]) - res = _run(module.github_callback()) - assert res["redirect"] == "/?auth=gh-new-user" - assert login_calls and login_calls[-1] is new_user - - inactive_user = _DummyUser("gh-existing", "gh@example.com", is_active="0") - monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [inactive_user]) - res = _run(module.github_callback()) - assert res["redirect"] == "/?error=user_inactive" - - existing_user = _DummyUser("gh-existing", "gh@example.com") - login_calls.clear() - monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [existing_user]) - monkeypatch.setattr(module, "login_user", lambda user: login_calls.append(user)) - monkeypatch.setattr(module, "get_uuid", lambda: "gh-existing-token") - res = _run(module.github_callback()) - assert res["redirect"] == "/?auth=gh-existing" - assert existing_user.access_token == "gh-existing-token" - assert existing_user.save_calls == 1 - assert login_calls and login_calls[-1] is existing_user - - -@pytest.mark.p2 -def test_feishu_callback_matrix_unit(monkeypatch): - module = _load_user_app(monkeypatch) - - _set_request_args(monkeypatch, module, {"code": "code"}) - module.session.clear() - - def _patch_async_queue(payloads): - queue = list(payloads) - - async def _request(_method, _url, **_kwargs): - return _DummyHTTPResponse(queue.pop(0)) - - monkeypatch.setattr(module, "async_request", _request) - - _patch_async_queue([{"code": 1}]) - res = _run(module.feishu_callback()) - assert "/?error=" in res["redirect"] - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 1, "message": "bad token"}, - ] - ) - res = _run(module.feishu_callback()) - assert res["redirect"] == "/?error=bad token" - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 0, "data": {"scope": "other", "access_token": "feishu-access"}}, - ] - ) - res = _run(module.feishu_callback()) - assert "contact:user.email:readonly not in scope" in res["redirect"] - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 0, "data": {"scope": "contact:user.email:readonly", "access_token": "feishu-access"}}, - ] - ) - monkeypatch.setattr( - module, - "user_info_from_feishu", - lambda _token: _AwaitableValue({"email": "fs@example.com", "avatar_url": "http://img", "en_name": "fs-user"}), - ) - monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: []) - rollback_calls = [] - monkeypatch.setattr(module, "rollback_user_registration", lambda user_id: rollback_calls.append(user_id)) - monkeypatch.setattr(module, "get_uuid", lambda: "fs-user-id") - - def _raise_download(_url): - raise RuntimeError("download explode") - - monkeypatch.setattr(module, "download_img", _raise_download) - monkeypatch.setattr(module, "user_register", lambda _user_id, _user: None) - res = _run(module.feishu_callback()) - assert "Fail to register fs@example.com." in res["redirect"] - assert rollback_calls == ["fs-user-id"] - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 0, "data": {"scope": "contact:user.email:readonly", "access_token": "feishu-access"}}, - ] - ) - monkeypatch.setattr(module, "download_img", lambda _url: "avatar") - monkeypatch.setattr( - module, - "user_register", - lambda _user_id, _user: [_DummyUser("dup-1", "fs@example.com"), _DummyUser("dup-2", "fs@example.com")], - ) - rollback_calls.clear() - res = _run(module.feishu_callback()) - assert "Same email: fs@example.com exists!" in res["redirect"] - assert rollback_calls == ["fs-user-id"] - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 0, "data": {"scope": "contact:user.email:readonly", "access_token": "feishu-access"}}, - ] - ) - new_user = _DummyUser("fs-new-user", "fs@example.com") - login_calls = [] - monkeypatch.setattr(module, "login_user", lambda user: login_calls.append(user)) - monkeypatch.setattr(module, "user_register", lambda _user_id, _user: [new_user]) - res = _run(module.feishu_callback()) - assert res["redirect"] == "/?auth=fs-new-user" - assert login_calls and login_calls[-1] is new_user - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 0, "data": {"scope": "contact:user.email:readonly", "access_token": "feishu-access"}}, - ] - ) - inactive_user = _DummyUser("fs-existing", "fs@example.com", is_active="0") - monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [inactive_user]) - res = _run(module.feishu_callback()) - assert res["redirect"] == "/?error=user_inactive" - - _patch_async_queue( - [ - {"code": 0, "app_access_token": "app-token"}, - {"code": 0, "data": {"scope": "contact:user.email:readonly", "access_token": "feishu-access"}}, - ] - ) - existing_user = _DummyUser("fs-existing", "fs@example.com") - login_calls.clear() - monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [existing_user]) - monkeypatch.setattr(module, "login_user", lambda user: login_calls.append(user)) - monkeypatch.setattr(module, "get_uuid", lambda: "fs-existing-token") - res = _run(module.feishu_callback()) - assert res["redirect"] == "/?auth=fs-existing" - assert existing_user.access_token == "fs-existing-token" - assert existing_user.save_calls == 1 - assert login_calls and login_calls[-1] is existing_user - - -@pytest.mark.p2 -def test_oauth_user_info_helpers_unit(monkeypatch): - module = _load_user_app(monkeypatch) - - async def _request_feishu(_method, _url, **_kwargs): - return _DummyHTTPResponse({"data": {"email": "", "en_name": "Feishu User"}}) - - monkeypatch.setattr(module, "async_request", _request_feishu) - feishu_user = _run(module.user_info_from_feishu("token-feishu")) - assert feishu_user["email"] is None - assert feishu_user["en_name"] == "Feishu User" - - async def _request_github(_method, url, **_kwargs): - if "emails" in url: - return _DummyHTTPResponse( - [ - {"email": "secondary@example.com", "primary": False}, - {"email": "primary@example.com", "primary": True}, - ] - ) - return _DummyHTTPResponse({"login": "gh-user"}) - - monkeypatch.setattr(module, "async_request", _request_github) - github_user = _run(module.user_info_from_github("token-github")) - assert github_user["login"] == "gh-user" - assert github_user["email"] == "primary@example.com" - - @pytest.mark.p2 def test_logout_setting_profile_matrix_unit(monkeypatch): module = _load_user_app(monkeypatch) diff --git a/web/src/services/user-service.ts b/web/src/services/user-service.ts index 09d7d682d5..1637dcfe16 100644 --- a/web/src/services/user-service.ts +++ b/web/src/services/user-service.ts @@ -33,7 +33,7 @@ const methods = { }, logout: { url: logout, - method: 'get', + method: 'post', }, register: { url: register, @@ -41,7 +41,7 @@ const methods = { }, setting: { url: setting, - method: 'post', + method: 'patch', }, userInfo: { url: userInfo, @@ -53,7 +53,7 @@ const methods = { }, setTenantInfo: { url: setTenantInfo, - method: 'post', + method: 'patch', }, factoriesList: { url: factoriesList, diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 315c238cf9..56ceaa6f12 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -5,15 +5,15 @@ export { restAPIv1, webAPI }; export default { // user - login: `${webAPI}/user/login`, - logout: `${webAPI}/user/logout`, - register: `${webAPI}/user/register`, - setting: `${webAPI}/user/setting`, - userInfo: `${webAPI}/user/info`, - tenantInfo: `${webAPI}/user/tenant_info`, - setTenantInfo: `${webAPI}/user/set_tenant_info`, - loginChannels: `${webAPI}/user/login/channels`, - loginChannel: (channel: string) => `${webAPI}/user/login/${channel}`, + login: `${restAPIv1}/auth/login`, + logout: `${restAPIv1}/auth/logout`, + register: `${restAPIv1}/users`, + setting: `${restAPIv1}/users/me`, + userInfo: `${restAPIv1}/users/me`, + tenantInfo: `${restAPIv1}/users/me/models`, + setTenantInfo: `${restAPIv1}/users/me/models`, + loginChannels: `${restAPIv1}/auth/login/channels`, + loginChannel: (channel: string) => `${restAPIv1}/auth/login/${channel}`, // team addTenantUser: (tenantId: string) => `${restAPIv1}/tenants/${tenantId}/users`, diff --git a/web/src/utils/llm-util.ts b/web/src/utils/llm-util.ts index 6086e8fac8..b8a843db3a 100644 --- a/web/src/utils/llm-util.ts +++ b/web/src/utils/llm-util.ts @@ -78,7 +78,7 @@ const modelParamMap: ModelParamMap = { // API endpoint whitelist - only these endpoints will have tenant parameters added const API_WHITELIST = [ - '/v1/user/set_tenant_info', + '/api/v1/users/me/models', '/api/v1/chats', '/v1/canvas/set', '/v1/canvas/setting',