mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 04:44:49 +08:00
Refactor user REST API (#14334)
### What problem does this PR solve? Refactor user REST API ### Type of change - [x] Refactoring
This commit is contained in:
@@ -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/<channel>"
|
||||
"redirect_uri": "https://your-app.com/api/v1/auth/oauth/<channel>/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/<channel>"
|
||||
"redirect_uri": "https://your-app.com/api/v1/auth/oauth/<channel>/callback"
|
||||
}
|
||||
|
||||
# Github OAuth configuration
|
||||
|
||||
@@ -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/<channel>", methods=["GET"]) # noqa: F821
|
||||
@manager.route("/auth/login/<channel>", 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/<channel>", methods=["GET"]) # noqa: F821
|
||||
@manager.route("/auth/oauth/<channel>/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/<channel>` 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=<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.
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user