From 26b0137c8328123c732720911137206fc3c60138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 18 Jun 2026 14:30:01 +0800 Subject: [PATCH] chore: improve invite member flow (#37479) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jingyi --- api/controllers/console/auth/activate.py | 77 ++++++-- api/controllers/console/workspace/members.py | 6 +- api/openapi/markdown/console-openapi.md | 8 +- api/services/account_service.py | 30 +-- .../services/test_account_service.py | 35 ++-- .../console/auth/test_account_activation.py | 126 ++++++++++++- .../console/workspace/test_members.py | 4 +- .../services/test_account_service.py | 68 +++++-- eslint-suppressions.json | 15 +- .../api/console/activate/types.gen.ts | 8 +- .../generated/api/console/activate/zod.gen.ts | 8 +- .../invited-modal/__tests__/index.spec.tsx | 28 +++ .../members-page/invited-modal/index.tsx | 54 +++++- web/app/signin/__tests__/normal-form.spec.tsx | 113 ++++++++++++ .../invite-settings/__tests__/page.spec.tsx | 95 ++++++++++ web/app/signin/invite-settings/page.tsx | 172 ++++++++++-------- web/app/signin/normal-form.tsx | 23 ++- web/i18n/ar-TN/common.json | 3 + web/i18n/de-DE/common.json | 3 + web/i18n/en-US/common.json | 3 + web/i18n/es-ES/common.json | 3 + web/i18n/fa-IR/common.json | 3 + web/i18n/fr-FR/common.json | 3 + web/i18n/hi-IN/common.json | 3 + web/i18n/id-ID/common.json | 3 + web/i18n/it-IT/common.json | 3 + web/i18n/ja-JP/common.json | 3 + web/i18n/ko-KR/common.json | 3 + web/i18n/nl-NL/common.json | 3 + web/i18n/pl-PL/common.json | 3 + web/i18n/pt-BR/common.json | 3 + web/i18n/ro-RO/common.json | 3 + web/i18n/ru-RU/common.json | 3 + web/i18n/sl-SI/common.json | 3 + web/i18n/th-TH/common.json | 3 + web/i18n/tr-TR/common.json | 3 + web/i18n/uk-UA/common.json | 3 + web/i18n/vi-VN/common.json | 3 + web/i18n/zh-Hans/common.json | 3 + web/i18n/zh-Hant/common.json | 3 + web/models/common.ts | 4 + web/service/common.ts | 21 ++- web/service/use-common.ts | 2 +- 43 files changed, 786 insertions(+), 180 deletions(-) create mode 100644 web/app/signin/__tests__/normal-form.spec.tsx diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 7e7810d86da..f61bb8f6802 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -1,6 +1,7 @@ from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator +from sqlalchemy import select from configs import dify_config from constants.languages import supported_language @@ -11,7 +12,8 @@ from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from libs.helper import EmailStr, timezone from models import AccountStatus -from services.account_service import RegisterService +from models.account import TenantAccountJoin, TenantAccountRole +from services.account_service import RegisterService, TenantService from services.billing_service import BillingService @@ -25,18 +27,22 @@ class ActivatePayload(BaseModel): workspace_id: str | None = Field(default=None) email: EmailStr | None = Field(default=None) token: str - name: str = Field(..., max_length=30) - interface_language: str = Field(...) - timezone: str = Field(...) + name: str | None = Field(default=None, max_length=30) + interface_language: str | None = Field(default=None) + timezone: str | None = Field(default=None) @field_validator("interface_language") @classmethod - def validate_lang(cls, value: str) -> str: + def validate_lang(cls, value: str | None) -> str | None: + if value is None: + return None return supported_language(value) @field_validator("timezone") @classmethod - def validate_tz(cls, value: str) -> str: + def validate_tz(cls, value: str | None) -> str | None: + if value is None: + return None return timezone(value) @@ -48,6 +54,8 @@ class ActivationCheckData(BaseModel): workspace_name: str | None workspace_id: str | None email: str | None + account_status: str | None = None + requires_setup: bool | None = None class ActivationCheckResponse(BaseModel): @@ -95,9 +103,20 @@ class ActivateCheckApi(Resource): workspace_name = tenant.name if tenant else None workspace_id = tenant.id if tenant else None invitee_email = data.get("email") if data else None + account = invitation.get("account") + account_status = account.status if account else None + requires_setup = data.get("requires_setup") + if requires_setup is None: + requires_setup = account_status == AccountStatus.PENDING return { "is_valid": invitation is not None, - "data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email}, + "data": { + "workspace_name": workspace_name, + "workspace_id": workspace_id, + "email": invitee_email, + "account_status": account_status, + "requires_setup": requires_setup, + }, } else: return {"is_valid": False} @@ -126,15 +145,45 @@ class ActivateApi(Resource): if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(account.email): raise AccountInFreezeError() + tenant = invitation["tenant"] + raw_role = invitation["data"].get("role") + try: + role = TenantAccountRole(raw_role) if raw_role else TenantAccountRole.NORMAL + except ValueError: + role = TenantAccountRole.NORMAL + if not TenantAccountRole.is_non_owner_role(role): + role = TenantAccountRole.NORMAL + + membership_id = db.session.scalar( + select(TenantAccountJoin.id).where( + TenantAccountJoin.tenant_id == tenant.id, + TenantAccountJoin.account_id == account.id, + ) + ) + + requires_setup = invitation["data"].get("requires_setup") + if requires_setup is None: + requires_setup = account.status == AccountStatus.PENDING + + setup_fields: tuple[str, str, str] | None = None + if requires_setup: + if not args.name or not args.interface_language or not args.timezone: + raise AlreadyActivateError() + setup_fields = (args.name, args.interface_language, args.timezone) + RegisterService.revoke_token(args.workspace_id, normalized_request_email, args.token) - account.name = args.name + if membership_id is None: + TenantService.create_tenant_member(tenant, account, str(role)) - account.interface_language = args.interface_language - account.timezone = args.timezone - account.interface_theme = "light" - account.status = AccountStatus.ACTIVE - account.initialized_at = naive_utc_now() - db.session.commit() + if setup_fields: + account.name = setup_fields[0] + account.interface_language = setup_fields[1] + account.timezone = setup_fields[2] + account.interface_theme = "light" + account.status = AccountStatus.ACTIVE + account.initialized_at = naive_utc_now() + + TenantService.switch_tenant(account, tenant.id) return {"result": "success"} diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index 6fea5417152..59fd3e2c5b5 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -232,7 +232,11 @@ class MemberInviteEmailApi(Resource): ) except AccountAlreadyInTenantError: invitation_results.append( - {"status": "success", "email": invitee_email, "url": f"{console_web_url}/signin"} + { + "status": "already_member", + "email": invitee_email, + "message": "Account already in workspace.", + } ) except Exception as e: invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)}) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ccc4bc12487..70c7a1aa9f1 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -11253,9 +11253,9 @@ Default namespace | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | email | string | | No | -| interface_language | string | | Yes | -| name | string | | Yes | -| timezone | string | | Yes | +| interface_language | string | | No | +| name | string | | No | +| timezone | string | | No | | token | string | | Yes | | workspace_id | string | | No | @@ -11263,7 +11263,9 @@ Default namespace | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| account_status | string | | No | | email | string | | Yes | +| requires_setup | boolean | | No | | workspace_id | string | | Yes | | workspace_name | string | | Yes | diff --git a/api/services/account_service.py b/api/services/account_service.py index e39d13a3929..5c55c8bfc46 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -5,7 +5,7 @@ import secrets import uuid from datetime import UTC, datetime, timedelta from hashlib import sha256 -from typing import Any, TypedDict, cast +from typing import Any, NotRequired, TypedDict, cast from pydantic import BaseModel, TypeAdapter, ValidationError from sqlalchemy import Row, delete, func, select, update @@ -18,6 +18,8 @@ class InvitationData(TypedDict): account_id: str email: str workspace_id: str + role: NotRequired[str] + requires_setup: NotRequired[bool] _invitation_adapter: TypeAdapter[InvitationData] = TypeAdapter(InvitationData) @@ -1805,6 +1807,7 @@ class RegisterService: account = AccountService.get_account_by_email_with_case_fallback(email) + requires_setup = False if not account: TenantService.check_member_permission(tenant, inviter, None, "add") name = normalized_email.split("@")[0] @@ -1819,6 +1822,7 @@ class RegisterService: # Create new tenant member for invited tenant TenantService.create_tenant_member(tenant, account, role) TenantService.switch_tenant(account, tenant.id) + requires_setup = True else: TenantService.check_member_permission(tenant, inviter, account, "add") ta = db.session.scalar( @@ -1826,15 +1830,16 @@ class RegisterService: .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) .limit(1) ) + requires_setup = account.status == AccountStatus.PENDING - if not ta: + if not ta and account.status == AccountStatus.PENDING: TenantService.create_tenant_member(tenant, account, role) # Support resend invitation email when the account is pending status - if account.status != AccountStatus.PENDING: + if ta and account.status != AccountStatus.PENDING: raise AccountAlreadyInTenantError("Account already in tenant.") - token = cls.generate_invite_token(tenant, account) + token = cls.generate_invite_token(tenant, account, role, requires_setup=requires_setup) language = account.interface_language or "en-US" # send email @@ -1849,12 +1854,16 @@ class RegisterService: return token @classmethod - def generate_invite_token(cls, tenant: Tenant, account: Account) -> str: + def generate_invite_token( + cls, tenant: Tenant, account: Account, role: str = "normal", *, requires_setup: bool = False + ) -> str: token = str(uuid.uuid4()) invitation_data = { "account_id": account.id, "email": account.email, "workspace_id": tenant.id, + "role": str(role), + "requires_setup": requires_setup, } expiry_hours = dify_config.INVITE_EXPIRY_HOURS redis_client.setex(cls._get_invitation_token_key(token), expiry_hours * 60 * 60, json.dumps(invitation_data)) @@ -1889,16 +1898,7 @@ class RegisterService: if not tenant: return None - tenant_account = db.session.execute( - select(Account, TenantAccountJoin.role) - .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) - .where(Account.email == invitation_data["email"], TenantAccountJoin.tenant_id == tenant.id) - ).first() - - if not tenant_account: - return None - - account = tenant_account[0] + account = db.session.scalar(select(Account).where(Account.email == invitation_data["email"]).limit(1)) if not account: return None diff --git a/api/tests/test_containers_integration_tests/services/test_account_service.py b/api/tests/test_containers_integration_tests/services/test_account_service.py index 9a53ff087c5..83d2e25d224 100644 --- a/api/tests/test_containers_integration_tests/services/test_account_service.py +++ b/api/tests/test_containers_integration_tests/services/test_account_service.py @@ -2755,7 +2755,7 @@ class TestRegisterService: self, db_session_with_containers: Session, mock_external_service_dependencies ): """ - Test inviting an existing member who is not in the tenant yet. + Test inviting an existing active account who is not in the tenant yet. """ fake = Faker() tenant_name = fake.company() @@ -2791,20 +2791,20 @@ class TestRegisterService: # Mock the email task with patch("services.account_service.send_invite_member_mail_task") as mock_send_mail: mock_send_mail.delay.return_value = None - with pytest.raises(AccountAlreadyInTenantError, match="Account already in tenant."): - # Execute invitation - token = RegisterService.invite_new_member( - tenant=tenant, - email=existing_member_email, - language=language, - role="admin", - inviter=inviter, - ) - # Verify email task was not called - mock_send_mail.delay.assert_not_called() + token = RegisterService.invite_new_member( + tenant=tenant, + email=existing_member_email, + language=language, + role="admin", + inviter=inviter, + ) - # Verify tenant member was created for existing account + assert token is not None + assert len(token) > 0 + mock_send_mail.delay.assert_called_once() + + # Existing active accounts must accept the invite before becoming workspace members. from models.account import TenantAccountJoin tenant_join = ( @@ -2812,8 +2812,13 @@ class TestRegisterService: .filter_by(tenant_id=tenant.id, account_id=existing_account.id) .first() ) - assert tenant_join is not None - assert tenant_join.role == "admin" + assert tenant_join is None + + invitation = RegisterService.get_invitation_if_token_valid(None, None, token) + assert invitation is not None + assert invitation["account"].id == existing_account.id + assert invitation["data"]["role"] == "admin" + assert invitation["data"]["requires_setup"] is False def test_invite_new_member_existing_member( self, db_session_with_containers: Session, mock_external_service_dependencies diff --git a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py index 79169cfce7e..1422afd7524 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py +++ b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py @@ -66,6 +66,20 @@ class TestActivateCheckApi: assert response["data"]["workspace_id"] == "workspace-123" assert response["data"]["email"] == "invitee@example.com" + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + def test_check_valid_invitation_token_includes_account_status(self, mock_get_invitation, app, mock_invitation): + mock_account = MagicMock() + mock_account.status = AccountStatus.ACTIVE + mock_invitation["account"] = mock_account + mock_get_invitation.return_value = mock_invitation + + with app.test_request_context("/activate/check?email=invitee@example.com&token=valid_token"): + response = ActivateCheckApi().get() + + assert response["is_valid"] is True + assert response["data"]["account_status"] == AccountStatus.ACTIVE + assert response["data"]["requires_setup"] is False + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") def test_check_invalid_invitation_token(self, mock_get_invitation, app: Flask): """ @@ -177,6 +191,11 @@ class TestActivateApi: "account": mock_account, } + @pytest.fixture(autouse=True) + def mock_switch_tenant(self): + with patch("controllers.console.auth.activate.TenantService.switch_tenant") as mock: + yield mock + @patch("controllers.console.auth.activate.RegisterService.get_invitation_if_token_valid") @patch("controllers.console.auth.activate.RegisterService.revoke_token") @patch("controllers.console.auth.activate.db") @@ -224,7 +243,40 @@ class TestActivateApi: assert mock_account.status == AccountStatus.ACTIVE assert mock_account.initialized_at is not None mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") - mock_db.session.commit.assert_called_once() + + @patch("controllers.console.auth.activate.TenantService.create_tenant_member") + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + @patch("controllers.console.auth.activate.RegisterService.revoke_token") + @patch("controllers.console.auth.activate.db") + def test_activation_rejects_missing_setup_fields_before_consuming_invitation( + self, + mock_db, + mock_revoke_token, + mock_get_invitation, + mock_create_tenant_member, + app: Flask, + mock_invitation, + mock_switch_tenant, + ): + mock_invitation["data"]["requires_setup"] = True + mock_get_invitation.return_value = mock_invitation + mock_db.session.scalar.return_value = None + + with app.test_request_context( + "/activate", + method="POST", + json={ + "workspace_id": "workspace-123", + "email": "invitee@example.com", + "token": "valid_token", + }, + ): + with pytest.raises(AlreadyActivateError): + ActivateApi().post() + + mock_revoke_token.assert_not_called() + mock_create_tenant_member.assert_not_called() + mock_switch_tenant.assert_not_called() @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") def test_activation_with_invalid_token(self, mock_get_invitation, app: Flask): @@ -504,3 +556,75 @@ class TestActivateApi: assert response["result"] == "success" mock_get_invitation.assert_called_once_with("workspace-123", "Invitee@Example.com", "valid_token") mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") + + @patch("controllers.console.auth.activate.TenantService.create_tenant_member") + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + @patch("controllers.console.auth.activate.RegisterService.revoke_token") + @patch("controllers.console.auth.activate.db") + def test_activation_for_existing_active_account_creates_membership_on_acceptance( + self, + mock_db, + mock_revoke_token, + mock_get_invitation, + mock_create_tenant_member, + app: Flask, + mock_invitation, + mock_account, + mock_switch_tenant, + ): + mock_account.status = AccountStatus.ACTIVE + mock_invitation["data"]["role"] = "admin" + mock_invitation["data"]["requires_setup"] = False + mock_get_invitation.return_value = mock_invitation + mock_db.session.scalar.return_value = None + + with app.test_request_context( + "/activate", + method="POST", + json={ + "workspace_id": "workspace-123", + "email": "invitee@example.com", + "token": "valid_token", + }, + ): + response = ActivateApi().post() + + assert response["result"] == "success" + mock_create_tenant_member.assert_called_once_with(mock_invitation["tenant"], mock_account, "admin") + mock_switch_tenant.assert_called_once_with(mock_account, mock_invitation["tenant"].id) + mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") + + @patch("controllers.console.auth.activate.TenantService.create_tenant_member") + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + @patch("controllers.console.auth.activate.RegisterService.revoke_token") + @patch("controllers.console.auth.activate.db") + def test_activation_legacy_active_member_invitation_does_not_require_setup( + self, + mock_db, + mock_revoke_token, + mock_get_invitation, + mock_create_tenant_member, + app: Flask, + mock_invitation, + mock_account, + mock_switch_tenant, + ): + mock_account.status = AccountStatus.ACTIVE + mock_get_invitation.return_value = mock_invitation + mock_db.session.scalar.return_value = "membership-id" + + with app.test_request_context( + "/activate", + method="POST", + json={ + "workspace_id": "workspace-123", + "email": "invitee@example.com", + "token": "valid_token", + }, + ): + response = ActivateApi().post() + + assert response["result"] == "success" + mock_create_tenant_member.assert_not_called() + mock_switch_tenant.assert_called_once_with(mock_account, mock_invitation["tenant"].id) + mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") diff --git a/api/tests/unit_tests/controllers/console/workspace/test_members.py b/api/tests/unit_tests/controllers/console/workspace/test_members.py index 494cbbf0c37..6c7879c8050 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_members.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_members.py @@ -190,7 +190,9 @@ class TestMemberInviteEmailApi: ): result, status = method(api, user) - assert result["invitation_results"][0]["status"] == "success" + assert status == 201 + assert result["invitation_results"][0]["status"] == "already_member" + assert result["invitation_results"][0]["message"] == "Account already in workspace." def test_invite_invalid_role(self, app: Flask): api = MemberInviteEmailApi() diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index db79cf4cb55..c98b717a105 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -1752,13 +1752,15 @@ class TestRegisterService: mock_check_permission.assert_called_once_with(mock_tenant, mock_inviter, None, "add") mock_create_member.assert_called_once_with(mock_tenant, mock_new_account, "normal") mock_switch_tenant.assert_called_once_with(mock_new_account, mock_tenant.id) - mock_generate_token.assert_called_once_with(mock_tenant, mock_new_account) + mock_generate_token.assert_called_once_with( + mock_tenant, mock_new_account, "normal", requires_setup=True + ) mock_task_dependencies.delay.assert_called_once() def test_invite_new_member_existing_account( self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies ): - """Test inviting a new member who already has an account.""" + """Test inviting a pending account that is not in the tenant yet.""" # Setup test data mock_tenant = MagicMock() mock_tenant.id = "tenant-456" @@ -1796,10 +1798,51 @@ class TestRegisterService: # Verify results assert result == "invite-token-123" mock_create_member.assert_called_once_with(mock_tenant, mock_existing_account, "normal") - mock_generate_token.assert_called_once_with(mock_tenant, mock_existing_account) + mock_generate_token.assert_called_once_with( + mock_tenant, mock_existing_account, "normal", requires_setup=True + ) mock_task_dependencies.delay.assert_called_once() mock_lookup.assert_called_once_with("existing@example.com") + def test_invite_existing_active_account_requires_acceptance_before_joining( + self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies + ): + """Existing active accounts outside the tenant receive an invite without immediate membership.""" + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test Workspace" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + mock_existing_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="existing-user-456", email="existing@example.com", status="active" + ) + + with patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup: + mock_lookup.return_value = mock_existing_account + mock_db_dependencies["db"].session.scalar.return_value = None + + with ( + patch("services.account_service.TenantService.check_member_permission") as mock_check_permission, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.RegisterService.generate_invite_token") as mock_generate_token, + ): + mock_generate_token.return_value = "invite-token-123" + + result = RegisterService.invite_new_member( + tenant=mock_tenant, + email="existing@example.com", + language="en-US", + role="admin", + inviter=mock_inviter, + ) + + assert result == "invite-token-123" + mock_check_permission.assert_called_once_with(mock_tenant, mock_inviter, mock_existing_account, "add") + mock_create_member.assert_not_called() + mock_generate_token.assert_called_once_with( + mock_tenant, mock_existing_account, "admin", requires_setup=False + ) + mock_task_dependencies.delay.assert_called_once() + def test_invite_new_member_already_in_tenant(self, mock_db_dependencies, mock_redis_dependencies): """Test inviting a member who is already in the tenant.""" # Setup test data @@ -1864,7 +1907,7 @@ class TestRegisterService: mock_uuid.return_value = "test-uuid-123" # Execute test - result = RegisterService.generate_invite_token(mock_tenant, mock_account) + result = RegisterService.generate_invite_token(mock_tenant, mock_account, "admin", requires_setup=True) # Verify results assert result == "test-uuid-123" @@ -1877,6 +1920,8 @@ class TestRegisterService: assert stored_data["account_id"] == "user-123" assert stored_data["email"] == "test@example.com" assert stored_data["workspace_id"] == "tenant-456" + assert stored_data["role"] == "admin" + assert stored_data["requires_setup"] is True def test_is_valid_invite_token_valid(self, mock_redis_dependencies): """Test checking valid invite token.""" @@ -1943,9 +1988,8 @@ class TestRegisterService: } mock_get_invitation_by_token.return_value = invitation_data - # Mock scalar for tenant lookup, execute for account+role lookup - mock_db_dependencies["db"].session.scalar.return_value = mock_tenant - mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") + # Mock scalar for tenant lookup, then account lookup. + mock_db_dependencies["db"].session.scalar.side_effect = [mock_tenant, mock_account] # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -2001,9 +2045,8 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock scalar for tenant, execute for account+role - mock_db_dependencies["db"].session.scalar.return_value = mock_tenant - mock_db_dependencies["db"].session.execute.return_value.first.return_value = None # No account found + # Mock scalar for tenant lookup, then account lookup. + mock_db_dependencies["db"].session.scalar.side_effect = [mock_tenant, None] # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -2029,9 +2072,8 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock scalar for tenant, execute for account+role - mock_db_dependencies["db"].session.scalar.return_value = mock_tenant - mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") + # Mock scalar for tenant lookup, then account lookup. + mock_db_dependencies["db"].session.scalar.side_effect = [mock_tenant, mock_account] # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 0641521eb13..263481bc9d1 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -7445,24 +7445,11 @@ "count": 1 } }, - "web/app/signin/invite-settings/page.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/signin/layout.tsx": { "ts/no-explicit-any": { "count": 1 } }, - "web/app/signin/normal-form.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - } - }, "web/app/signin/one-more-step.tsx": { "no-restricted-imports": { "count": 1 @@ -7695,7 +7682,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 27 + "count": 26 } }, "web/service/datasets.ts": { diff --git a/packages/contracts/generated/api/console/activate/types.gen.ts b/packages/contracts/generated/api/console/activate/types.gen.ts index 92370b5a75c..6591aba3996 100644 --- a/packages/contracts/generated/api/console/activate/types.gen.ts +++ b/packages/contracts/generated/api/console/activate/types.gen.ts @@ -6,9 +6,9 @@ export type ClientOptions = { export type ActivatePayload = { email?: string | null - interface_language: string - name: string - timezone: string + interface_language?: string | null + name?: string | null + timezone?: string | null token: string workspace_id?: string | null } @@ -23,7 +23,9 @@ export type ActivationCheckResponse = { } export type ActivationCheckData = { + account_status?: string | null email: string | null + requires_setup?: boolean | null workspace_id: string | null workspace_name: string | null } diff --git a/packages/contracts/generated/api/console/activate/zod.gen.ts b/packages/contracts/generated/api/console/activate/zod.gen.ts index 00f85767b7c..40aff1f934f 100644 --- a/packages/contracts/generated/api/console/activate/zod.gen.ts +++ b/packages/contracts/generated/api/console/activate/zod.gen.ts @@ -7,9 +7,9 @@ import * as z from 'zod' */ export const zActivatePayload = z.object({ email: z.string().nullish(), - interface_language: z.string(), - name: z.string().max(30), - timezone: z.string(), + interface_language: z.string().nullish(), + name: z.string().max(30).nullish(), + timezone: z.string().nullish(), token: z.string(), workspace_id: z.string().nullish(), }) @@ -25,7 +25,9 @@ export const zActivationResponse = z.object({ * ActivationCheckData */ export const zActivationCheckData = z.object({ + account_status: z.string().nullish(), email: z.string().nullable(), + requires_setup: z.boolean().nullish(), workspace_id: z.string().nullable(), workspace_name: z.string().nullable(), }) diff --git a/web/app/components/header/account-setting/members-page/invited-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/invited-modal/__tests__/index.spec.tsx index 32d0bdda50f..27ba8e4b41f 100644 --- a/web/app/components/header/account-setting/members-page/invited-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/invited-modal/__tests__/index.spec.tsx @@ -14,6 +14,7 @@ describe('InvitedModal', () => { const mockOnCancel = vi.fn() const results: InvitationResult[] = [ { email: 'success@example.com', status: 'success', url: 'http://invite.com/1' }, + { email: 'member@example.com', status: 'already_member', message: 'Account already in workspace.' }, { email: 'failed@example.com', status: 'failed', message: 'Error msg' }, ] @@ -28,6 +29,8 @@ describe('InvitedModal', () => { expect(await screen.findByText(/members\.invitationSent$/i)).toBeInTheDocument() expect(await screen.findByText(/members\.invitationLink/i)).toBeInTheDocument() expect(screen.getByText('http://invite.com/1')).toBeInTheDocument() + expect(screen.getByText(/members\.alreadyInTeam$/i)).toBeInTheDocument() + expect(screen.getByText('member@example.com')).toBeInTheDocument() expect(screen.getByText('failed@example.com')).toBeInTheDocument() }) @@ -53,6 +56,19 @@ describe('InvitedModal', () => { expect(screen.queryByText(/members\.failedInvitationEmails/i)).not.toBeInTheDocument() }) + it('should show already-member message without invitation copy when every email is already a member', () => { + const alreadyMembers: InvitationResult[] = [ + { email: 'member@example.com', status: 'already_member' }, + ] + + render() + + expect(screen.getByText(/members\.noNewInvitationsSent/i)).toBeInTheDocument() + expect(screen.getByText(/members\.alreadyInTeamTip/i)).toBeInTheDocument() + expect(screen.getByText('member@example.com')).toBeInTheDocument() + expect(screen.queryByText(/members\.invitationLink/i)).not.toBeInTheDocument() + }) + it('should hide both sections when results are empty', () => { render() @@ -85,4 +101,16 @@ describe('InvitedModal (non-CE edition)', () => { // CE-only content should not be shown expect(screen.queryByText(/members\.invitationLink/i)).not.toBeInTheDocument() }) + + it('should show already-member details when IS_CE_EDITION is false', () => { + const results: InvitationResult[] = [ + { email: 'member@example.com', status: 'already_member' }, + ] + + render() + + expect(screen.getByText(/members\.noNewInvitationsSent/i)).toBeInTheDocument() + expect(screen.getByText(/members\.alreadyInTeam$/i)).toBeInTheDocument() + expect(screen.getByText('member@example.com')).toBeInTheDocument() + }) }) diff --git a/web/app/components/header/account-setting/members-page/invited-modal/index.tsx b/web/app/components/header/account-setting/members-page/invited-modal/index.tsx index 8ac60da1f4f..d03d420a667 100644 --- a/web/app/components/header/account-setting/members-page/invited-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invited-modal/index.tsx @@ -2,12 +2,12 @@ import type { InvitationResult } from '@/models/common' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { IS_CE_EDITION } from '@/config' import InvitationLink from './invitation-link' export type SuccessInvitationResult = Extract +type AlreadyMemberInvitationResult = Extract type FailedInvitationResult = Extract type IInvitedModalProps = { @@ -20,8 +20,17 @@ const InvitedModal = ({ }: IInvitedModalProps) => { const { t } = useTranslation() - const successInvitationResults = useMemo(() => invitationResults?.filter(item => item.status === 'success') as SuccessInvitationResult[], [invitationResults]) - const failedInvitationResults = useMemo(() => invitationResults?.filter(item => item.status !== 'success') as FailedInvitationResult[], [invitationResults]) + const successInvitationResults = invitationResults.filter( + (item): item is SuccessInvitationResult => item.status === 'success', + ) + const alreadyMemberInvitationResults = invitationResults.filter( + (item): item is AlreadyMemberInvitationResult => item.status === 'already_member', + ) + const failedInvitationResults = invitationResults.filter( + (item): item is FailedInvitationResult => item.status === 'failed', + ) + const onlyAlreadyMembers = alreadyMemberInvitationResults.length > 0 && successInvitationResults.length === 0 && failedInvitationResults.length === 0 + const description = t(onlyAlreadyMembers ? 'members.alreadyInTeamTip' : 'members.invitationSentTip', { ns: 'common' }) return ( - {t('members.invitationSent', { ns: 'common' })} + + {t(onlyAlreadyMembers ? 'members.noNewInvitationsSent' : 'members.invitationSent', { ns: 'common' })} + {!IS_CE_EDITION && ( -
{t('members.invitationSentTip', { ns: 'common' })}
+
{description}
)} - {IS_CE_EDITION && ( + {(IS_CE_EDITION || !!alreadyMemberInvitationResults.length) && ( <> -
{t('members.invitationSentTip', { ns: 'common' })}
+ {IS_CE_EDITION && ( +
{description}
+ )}
{ - !!successInvitationResults.length + IS_CE_EDITION && !!successInvitationResults.length && ( <>
{t('members.invitationLink', { ns: 'common' })}
@@ -65,7 +78,30 @@ const InvitedModal = ({ ) } { - !!failedInvitationResults.length + !!alreadyMemberInvitationResults.length + && ( + <> +
{t('members.alreadyInTeam', { ns: 'common' })}
+ {!onlyAlreadyMembers && ( +
{t('members.alreadyInTeamTip', { ns: 'common' })}
+ )} +
+ { + alreadyMemberInvitationResults.map(item => ( +
+ {item.email} +
+ )) + } +
+ + ) + } + { + IS_CE_EDITION && !!failedInvitationResults.length && ( <>
{t('members.failedInvitationEmails', { ns: 'common' })}
diff --git a/web/app/signin/__tests__/normal-form.spec.tsx b/web/app/signin/__tests__/normal-form.spec.tsx new file mode 100644 index 00000000000..6dfdfbb372f --- /dev/null +++ b/web/app/signin/__tests__/normal-form.spec.tsx @@ -0,0 +1,113 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' +import { render, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useRouter, useSearchParams } from '@/next/navigation' +import NormalForm from '../normal-form' + +vi.mock('@tanstack/react-query', async () => { + const actual = await vi.importActual('@tanstack/react-query') + return { + ...actual, + useQuery: vi.fn(), + useSuspenseQuery: vi.fn(), + } +}) + +vi.mock('@/features/account-profile/client', () => ({ + isLegacyBase401: vi.fn(() => false), + userProfileQueryOptions: vi.fn(() => ({ + queryKey: ['account', 'profile'], + queryFn: vi.fn(), + })), +})) + +vi.mock('@/features/system-features/client', () => ({ + systemFeaturesQueryOptions: vi.fn(() => ({ + queryKey: ['system-features'], + queryFn: vi.fn(), + })), +})) + +vi.mock('@/next/navigation', () => ({ + useRouter: vi.fn(), + useSearchParams: vi.fn(), +})) + +vi.mock('@/service/common', async () => { + const actual = await vi.importActual('@/service/common') + return { + ...actual, + invitationCheck: vi.fn(), + } +}) + +vi.mock('./utils/post-login-redirect', () => ({ + resolvePostLoginRedirect: vi.fn(() => null), +})) + +const mockReplace = vi.fn() +const mockUseQuery = vi.mocked(useQuery) +const mockUseSuspenseQuery = vi.mocked(useSuspenseQuery) +const mockUseRouter = useRouter as unknown as ReturnType +const mockUseSearchParams = useSearchParams as unknown as ReturnType + +const loggedInQueryResult = { + isPending: false, + data: { + profile: { + id: 'account-id', + }, + }, + error: null, +} + +const invitationQueryResult = { + isPending: false, + isError: false, + data: { + is_valid: true, + data: { + workspace_name: 'Acme', + workspace_id: 'workspace-id', + email: 'invitee@example.com', + }, + }, +} + +describe('NormalForm', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUseRouter.mockReturnValue({ replace: mockReplace }) + mockUseSearchParams.mockReturnValue(new URLSearchParams('invite_token=invite-token')) + mockUseSuspenseQuery.mockReturnValue({ + data: { + enable_social_oauth_login: false, + sso_enforced_for_signin: false, + enable_email_code_login: false, + enable_email_password_login: true, + is_email_setup: true, + is_allow_register: false, + license: { + status: 'none', + }, + branding: { + enabled: true, + }, + }, + } as unknown as ReturnType) + }) + + describe('Invite Redirects', () => { + it('should send logged-in invite visitors to the invite confirmation page', async () => { + mockUseQuery + .mockReturnValueOnce(loggedInQueryResult as unknown as ReturnType) + .mockReturnValueOnce(invitationQueryResult as unknown as ReturnType) + + render() + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/signin/invite-settings?invite_token=invite-token') + }) + }) + }) +}) diff --git a/web/app/signin/invite-settings/__tests__/page.spec.tsx b/web/app/signin/invite-settings/__tests__/page.spec.tsx index 6990e71a679..4038aff328f 100644 --- a/web/app/signin/invite-settings/__tests__/page.spec.tsx +++ b/web/app/signin/invite-settings/__tests__/page.spec.tsx @@ -86,6 +86,7 @@ describe('InviteSettingsPage', () => { workspace_name: 'Acme', workspace_id: 'workspace-id', email: 'invitee@example.com', + requires_setup: true, }, }, refetch: mockRefetch, @@ -138,5 +139,99 @@ describe('InviteSettingsPage', () => { }) }) }) + + it('should only submit the token when an active account accepts an invitation', async () => { + mockUseInvitationCheck.mockReturnValue({ + data: { + is_valid: true, + data: { + workspace_name: 'Acme', + workspace_id: 'workspace-id', + email: 'invitee@example.com', + account_status: 'active', + requires_setup: false, + }, + }, + refetch: mockRefetch, + } as unknown as ReturnType) + + render() + + expect(screen.queryByLabelText('login.name')).not.toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'login.join Acme' })) + + await waitFor(() => { + expect(mockActivateMember).toHaveBeenCalledWith({ + url: '/activate', + body: { + token: 'invite-token', + }, + }) + }) + }) + + it('should only submit the token when an active account check omits setup state', async () => { + mockUseInvitationCheck.mockReturnValue({ + data: { + is_valid: true, + data: { + workspace_name: 'Acme', + workspace_id: 'workspace-id', + email: 'invitee@example.com', + account_status: 'active', + }, + }, + refetch: mockRefetch, + } as unknown as ReturnType) + + render() + + expect(screen.queryByLabelText('login.name')).not.toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'login.join Acme' })) + + await waitFor(() => { + expect(mockActivateMember).toHaveBeenCalledWith({ + url: '/activate', + body: { + token: 'invite-token', + }, + }) + }) + }) + + it('should submit setup fields when the invitation requires account setup', async () => { + mockUseInvitationCheck.mockReturnValue({ + data: { + is_valid: true, + data: { + workspace_name: 'Acme', + workspace_id: 'workspace-id', + email: 'invitee@example.com', + account_status: 'active', + requires_setup: true, + }, + }, + refetch: mockRefetch, + } as unknown as ReturnType) + + render() + + fireEvent.change(screen.getByLabelText('login.name'), { + target: { value: 'Invitee' }, + }) + fireEvent.click(screen.getByRole('button', { name: 'login.join Acme' })) + + await waitFor(() => { + expect(mockActivateMember).toHaveBeenCalledWith({ + url: '/activate', + body: { + token: 'invite-token', + name: 'Invitee', + interface_language: 'zh-Hans', + timezone: 'Asia/Shanghai', + }, + }) + }) + }) }) }) diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index f4f255bd89b..df48867a9b9 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -1,6 +1,7 @@ 'use client' import type { Locale } from '@/i18n-config' import { Button } from '@langgenius/dify-ui/button' +import { Input } from '@langgenius/dify-ui/input' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import { toast } from '@langgenius/dify-ui/toast' import { RiAccountCircleLine } from '@remixicon/react' @@ -8,7 +9,6 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' import Loading from '@/app/components/base/loading' import { LICENSE_LINK } from '@/constants/link' import { useLocale } from '@/context/i18n' @@ -85,25 +85,32 @@ export default function InviteSettingsPage() { }, } const { data: checkRes, refetch: recheck } = useInvitationCheck(checkParams.params, !!token) + const requiresAccountSetup = checkRes?.data?.requires_setup ?? checkRes?.data?.account_status === 'pending' const handleActivate = useCallback(async () => { try { - if (!name) { + if (requiresAccountSetup && !name) { toast.error(t('enterYourName', { ns: 'login' })) return } + const body = requiresAccountSetup + ? { + token, + name, + interface_language: language, + timezone, + } + : { + token, + } const res = await activateMember({ url: '/activate', - body: { - token, - name, - interface_language: language, - timezone, - }, + body, }) if (res.result === 'success') { // Tokens are now stored in cookies by the backend - await setLocaleOnClient(language!, false) + if (requiresAccountSetup) + await setLocaleOnClient(language!, false) await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() }) const redirectUrl = resolvePostLoginRedirect(searchParams) router.replace(redirectUrl || '/') @@ -112,7 +119,7 @@ export default function InviteSettingsPage() { catch { recheck() } - }, [language, name, queryClient, recheck, searchParams, timezone, token, router, t]) + }, [language, name, queryClient, recheck, requiresAccountSetup, searchParams, timezone, token, router, t]) if (!checkRes) return @@ -138,77 +145,84 @@ export default function InviteSettingsPage() {
-

{t('setYourAccount', { ns: 'login' })}

+

+ {requiresAccountSetup + ? t('setYourAccount', { ns: 'login' }) + : `${t('join', { ns: 'login' })}${checkRes?.data?.workspace_name}`} +

-
- -
- setName(e.target.value)} - placeholder={t('namePlaceholder', { ns: 'login' }) || ''} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - e.stopPropagation() - handleActivate() - } - }} - /> -
-
-
- -
- -
-
- {/* timezone */} -
- -
- -
-
+ {requiresAccountSetup && ( + <> +
+ +
+ setName(e.target.value)} + placeholder={t('namePlaceholder', { ns: 'login' }) || ''} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + e.stopPropagation() + handleActivate() + } + }} + /> +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + )}
+ )} )} @@ -207,9 +216,13 @@ function NormalForm() { <> {hasEmailCodeLogin && ( -
{ setSelectedAuthType('code') }}> +
+ )} )} diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json index 50ad6b445ff..5939821a64f 100644 --- a/web/i18n/ar-TN/common.json +++ b/web/i18n/ar-TN/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "اتصل بخوادم MCP وأدرها لمنح تطبيقاتك إمكانية الوصول إلى الأدوات والخدمات الخارجية.", "members.admin": "المسؤول", "members.adminTip": "يمكنه بناء التطبيقات وإدارة إعدادات الفريق", + "members.alreadyInTeam": "موجود بالفعل في الفريق", + "members.alreadyInTeamTip": "هؤلاء المستخدمون لديهم بالفعل إمكانية الوصول إلى مساحة العمل هذه.", "members.builder": "باني", "members.builderTip": "يمكنه بناء وتعديل تطبيقاته الخاصة", "members.datasetOperator": "مسؤول المعرفة", @@ -273,6 +275,7 @@ "members.invitedAsRole": "تمت الدعوة كمستخدم {{role}}", "members.lastActive": "آخر نشاط", "members.name": "الاسم", + "members.noNewInvitationsSent": "لم يتم إرسال دعوات جديدة", "members.normal": "عادي", "members.normalTip": "يمكنه استخدام التطبيقات فقط، ولا يمكنه بناء التطبيقات", "members.ok": "موافق", diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json index ad337a46eb2..93ba27276d5 100644 --- a/web/i18n/de-DE/common.json +++ b/web/i18n/de-DE/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Verbinde und verwalte MCP-Server, damit deine Apps auf externe Tools und Dienste zugreifen können.", "members.admin": "Admin", "members.adminTip": "Kann Apps erstellen & Team-Einstellungen verwalten", + "members.alreadyInTeam": "Bereits im Team", + "members.alreadyInTeamTip": "Diese Benutzer haben bereits Zugriff auf diesen Arbeitsbereich.", "members.builder": "Bauherr", "members.builderTip": "Kann eigene Apps erstellen und bearbeiten", "members.datasetOperator": "Wissensadministrator", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Eingeladen als {{role}}-Benutzer", "members.lastActive": "ZULETZT AKTIV", "members.name": "NAME", + "members.noNewInvitationsSent": "Keine neuen Einladungen gesendet", "members.normal": "Normal", "members.normalTip": "Kann nur Apps verwenden, kann keine Apps erstellen", "members.ok": "OK", diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 429565887c6..023b35a5ae7 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Connect and manage MCP servers to give your apps access to external tools and services.", "members.admin": "Admin", "members.adminTip": "Can build apps & manage team settings", + "members.alreadyInTeam": "Already in team", + "members.alreadyInTeamTip": "These users already have access to this workspace.", "members.builder": "Builder", "members.builderTip": "Can build & edit own apps", "members.datasetOperator": "Knowledge Admin", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Invited as {{role}} user", "members.lastActive": "LAST ACTIVE", "members.name": "NAME", + "members.noNewInvitationsSent": "No new invitations sent", "members.normal": "Normal", "members.normalTip": "Only can use apps, can not build apps", "members.ok": "OK", diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json index 7d7fb80bfc2..4dfd491cde6 100644 --- a/web/i18n/es-ES/common.json +++ b/web/i18n/es-ES/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Conecta y gestiona servidores MCP para dar a tus apps acceso a herramientas y servicios externos.", "members.admin": "Administrador", "members.adminTip": "Puede crear aplicaciones y administrar configuraciones del equipo", + "members.alreadyInTeam": "Ya está en el equipo", + "members.alreadyInTeamTip": "Estos usuarios ya tienen acceso a este espacio de trabajo.", "members.builder": "Constructor", "members.builderTip": "Puede crear y editar sus propias aplicaciones", "members.datasetOperator": "Administrador de Conocimiento", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Invitado como usuario {{role}}", "members.lastActive": "ÚLTIMA ACTIVIDAD", "members.name": "NOMBRE", + "members.noNewInvitationsSent": "No se han enviado nuevas invitaciones", "members.normal": "Normal", "members.normalTip": "Solo puede usar aplicaciones, no puede crear aplicaciones", "members.ok": "OK", diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json index e461067627a..3fbd841354e 100644 --- a/web/i18n/fa-IR/common.json +++ b/web/i18n/fa-IR/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "سرورهای MCP را وصل و مدیریت کنید تا برنامه‌های شما به ابزارها و سرویس‌های خارجی دسترسی داشته باشند.", "members.admin": "مدیر", "members.adminTip": "می‌تواند برنامه‌ها را بسازد و تنظیمات تیم را مدیریت کند", + "members.alreadyInTeam": "در حال حاضر در تیم است", + "members.alreadyInTeamTip": "این کاربران از قبل به این فضای کاری دسترسی دارند.", "members.builder": "سازنده", "members.builderTip": "می‌تواند برنامه‌های خود را بسازد و ویرایش کند", "members.datasetOperator": "مدیر دانش", @@ -273,6 +275,7 @@ "members.invitedAsRole": "به عنوان کاربر {{role}} دعوت شده", "members.lastActive": "آخرین فعالیت", "members.name": "نام", + "members.noNewInvitationsSent": "هیچ دعوت‌نامه جدیدی ارسال نشد", "members.normal": "عادی", "members.normalTip": "فقط می‌تواند از برنامه‌ها استفاده کند، نمی‌تواند برنامه بسازد", "members.ok": "تایید", diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json index 7de464807c2..b10d5a216b0 100644 --- a/web/i18n/fr-FR/common.json +++ b/web/i18n/fr-FR/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Connectez et gérez des serveurs MCP pour donner à vos apps accès à des outils et services externes.", "members.admin": "Administrateur", "members.adminTip": "Peut construire des applications & gérer les paramètres de l'équipe", + "members.alreadyInTeam": "Déjà dans l’équipe", + "members.alreadyInTeamTip": "Ces utilisateurs ont déjà accès à cet espace de travail.", "members.builder": "Constructeur", "members.builderTip": "Peut créer et modifier ses propres applications", "members.datasetOperator": "Administrateur des connaissances", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Invité en tant qu'utilisateur {{role}}", "members.lastActive": "DERNIÈRE ACTIVITÉ", "members.name": "NOM", + "members.noNewInvitationsSent": "Aucune nouvelle invitation envoyée", "members.normal": "Normal", "members.normalTip": "Peut seulement utiliser des applications, ne peut pas construire des applications", "members.ok": "D'accord", diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json index c678fefa2a6..3031cecc279 100644 --- a/web/i18n/hi-IN/common.json +++ b/web/i18n/hi-IN/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "MCP सर्वर कनेक्ट और प्रबंधित करें ताकि आपके ऐप्स बाहरी टूल और सेवाओं तक पहुँच सकें।", "members.admin": "प्रशासक", "members.adminTip": "ऐप्स बना सकते हैं और टीम सेटिंग्स का प्रबंधन कर सकते हैं", + "members.alreadyInTeam": "पहले से टीम में हैं", + "members.alreadyInTeamTip": "इन उपयोगकर्ताओं के पास पहले से इस वर्कस्पेस की पहुंच है।", "members.builder": "निर्माता", "members.builderTip": "अपने स्वयं के ऐप्स बना और संपादित कर सकते हैं", "members.datasetOperator": "ज्ञान व्यवस्थापक", @@ -273,6 +275,7 @@ "members.invitedAsRole": "{{role}} उपयोगकर्ता के रूप में आमंत्रित किया गया", "members.lastActive": "अंतिम सक्रियता", "members.name": "नाम", + "members.noNewInvitationsSent": "कोई नया आमंत्रण नहीं भेजा गया", "members.normal": "सामान्य", "members.normalTip": "केवल ऐप्स का उपयोग कर सकते हैं, ऐप्स नहीं बना सकते", "members.ok": "ठीक है", diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json index 6a44ac1d148..57939b12a99 100644 --- a/web/i18n/id-ID/common.json +++ b/web/i18n/id-ID/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Hubungkan dan kelola server MCP agar aplikasi Anda dapat mengakses alat dan layanan eksternal.", "members.admin": "Admin", "members.adminTip": "Dapat membangun aplikasi & mengelola pengaturan tim", + "members.alreadyInTeam": "Sudah ada di tim", + "members.alreadyInTeamTip": "Pengguna ini sudah memiliki akses ke ruang kerja ini.", "members.builder": "Pembangun", "members.builderTip": "Dapat membangun & mengedit aplikasi sendiri", "members.datasetOperator": "Admin Pengetahuan", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Diundang sebagai pengguna {{role}}", "members.lastActive": "TERAKHIR AKTIF", "members.name": "NAMA", + "members.noNewInvitationsSent": "Tidak ada undangan baru yang dikirim", "members.normal": "Biasa", "members.normalTip": "Hanya dapat menggunakan aplikasi, tidak dapat membuat aplikasi", "members.ok": "OKE", diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json index aa71b3df53e..fe662ee06c2 100644 --- a/web/i18n/it-IT/common.json +++ b/web/i18n/it-IT/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Connetti e gestisci server MCP per dare alle tue app accesso a strumenti e servizi esterni.", "members.admin": "Admin", "members.adminTip": "Può creare app e gestire le impostazioni del team", + "members.alreadyInTeam": "Già nel team", + "members.alreadyInTeamTip": "Questi utenti hanno già accesso a questo spazio di lavoro.", "members.builder": "Builder", "members.builderTip": "Può creare e modificare le proprie app", "members.datasetOperator": "Admin della Conoscenza", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Invitato come utente {{role}}", "members.lastActive": "ULTIMA ATTIVITÀ", "members.name": "NOME", + "members.noNewInvitationsSent": "Nessun nuovo invito inviato", "members.normal": "Normale", "members.normalTip": "Può solo usare le app, non può crearle", "members.ok": "OK", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index b20b17e27a0..698fa5fc9eb 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "MCP サーバーを接続・管理して、アプリから外部ツールやサービスにアクセスできるようにします。", "members.admin": "管理者", "members.adminTip": "アプリの構築およびチーム設定の管理ができます", + "members.alreadyInTeam": "すでにチームに参加済み", + "members.alreadyInTeamTip": "これらのユーザーはすでにこのワークスペースにアクセスできます。", "members.builder": "ビルダー", "members.builderTip": "独自のアプリを作成・編集できる", "members.datasetOperator": "ナレッジ管理員", @@ -273,6 +275,7 @@ "members.invitedAsRole": "{{role}}ユーザーとして招待されました", "members.lastActive": "最終アクティブ", "members.name": "名前", + "members.noNewInvitationsSent": "新しい招待は送信されませんでした", "members.normal": "通常", "members.normalTip": "アプリの使用のみが可能で、アプリの構築はできません", "members.ok": "OK", diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json index 79e93c93541..cb776d64f9f 100644 --- a/web/i18n/ko-KR/common.json +++ b/web/i18n/ko-KR/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "MCP 서버를 연결하고 관리하여 앱이 외부 도구와 서비스에 접근할 수 있도록 하세요.", "members.admin": "관리자", "members.adminTip": "앱 빌드 및 팀 설정 관리 가능", + "members.alreadyInTeam": "이미 팀에 있습니다", + "members.alreadyInTeamTip": "이 사용자들은 이미 이 작업 공간에 액세스할 수 있습니다.", "members.builder": "빌더", "members.builderTip": "자신의 앱을 구축 및 편집할 수 있습니다.", "members.datasetOperator": "지식 관리자", @@ -273,6 +275,7 @@ "members.invitedAsRole": "{{role}} 사용자로 초대되었습니다", "members.lastActive": "최근 활동", "members.name": "이름", + "members.noNewInvitationsSent": "새 초대가 전송되지 않았습니다", "members.normal": "일반", "members.normalTip": "앱 사용만 가능하고 앱 빌드는 불가능", "members.ok": "확인", diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json index 56a4b69d322..494f92ab164 100644 --- a/web/i18n/nl-NL/common.json +++ b/web/i18n/nl-NL/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Verbind en beheer MCP-servers zodat je apps toegang krijgen tot externe tools en services.", "members.admin": "Admin", "members.adminTip": "Can build apps & manage team settings", + "members.alreadyInTeam": "Al in het team", + "members.alreadyInTeamTip": "Deze gebruikers hebben al toegang tot deze werkruimte.", "members.builder": "Builder", "members.builderTip": "Can build & edit own apps", "members.datasetOperator": "Knowledge Admin", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Invited as {{role}} user", "members.lastActive": "LAST ACTIVE", "members.name": "NAME", + "members.noNewInvitationsSent": "Geen nieuwe uitnodigingen verzonden", "members.normal": "Normal", "members.normalTip": "Only can use apps, can not build apps", "members.ok": "OK", diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json index 1626af3964b..35a30b811e6 100644 --- a/web/i18n/pl-PL/common.json +++ b/web/i18n/pl-PL/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Łącz i zarządzaj serwerami MCP, aby aplikacje mogły korzystać z zewnętrznych narzędzi i usług.", "members.admin": "Admin", "members.adminTip": "Może tworzyć aplikacje i zarządzać ustawieniami zespołu", + "members.alreadyInTeam": "Już w zespole", + "members.alreadyInTeamTip": "Ci użytkownicy mają już dostęp do tego obszaru roboczego.", "members.builder": "Budowniczy", "members.builderTip": "Może tworzyć i edytować własne aplikacje", "members.datasetOperator": "Wiedza Admin", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Zaproszony jako użytkownik typu {{role}}", "members.lastActive": "OSTATNIA AKTYWNOŚĆ", "members.name": "NAZWA", + "members.noNewInvitationsSent": "Nie wysłano nowych zaproszeń", "members.normal": "Normalny", "members.normalTip": "Może tylko korzystać z aplikacji, nie może tworzyć aplikacji", "members.ok": "OK", diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json index 3b6890c26c9..5c640313016 100644 --- a/web/i18n/pt-BR/common.json +++ b/web/i18n/pt-BR/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Conecte e gerencie servidores MCP para dar aos seus apps acesso a ferramentas e serviços externos.", "members.admin": "Admin", "members.adminTip": "Pode criar aplicativos e gerenciar configurações da equipe", + "members.alreadyInTeam": "Já está na equipe", + "members.alreadyInTeamTip": "Estes usuários já têm acesso a este espaço de trabalho.", "members.builder": "Construtor", "members.builderTip": "Pode criar e editar seus próprios aplicativos", "members.datasetOperator": "Administrador de conhecimento", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Convidado como usuário {{role}}", "members.lastActive": "ÚLTIMA ATIVIDADE", "members.name": "NOME", + "members.noNewInvitationsSent": "Nenhum novo convite enviado", "members.normal": "Normal", "members.normalTip": "Só pode usar aplicativos, não pode criar aplicativos", "members.ok": "OK", diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json index 22357b143d3..3a15cf1b603 100644 --- a/web/i18n/ro-RO/common.json +++ b/web/i18n/ro-RO/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Conectează și administrează servere MCP pentru a oferi aplicațiilor acces la instrumente și servicii externe.", "members.admin": "Administrator", "members.adminTip": "Poate construi aplicații și gestiona setările echipei", + "members.alreadyInTeam": "Deja în echipă", + "members.alreadyInTeamTip": "Acești utilizatori au deja acces la acest spațiu de lucru.", "members.builder": "Constructor", "members.builderTip": "Poate construi și edita propriile aplicații", "members.datasetOperator": "Administrator de cunoștințe", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Invitat ca utilizator {{role}}", "members.lastActive": "ULTIMA ACTIVITATE", "members.name": "NUME", + "members.noNewInvitationsSent": "Nu au fost trimise invitații noi", "members.normal": "Normal", "members.normalTip": "Poate doar utiliza aplicații, nu poate construi aplicații", "members.ok": "OK", diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json index ff084881dba..e99d38def6d 100644 --- a/web/i18n/ru-RU/common.json +++ b/web/i18n/ru-RU/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Подключайте и управляйте MCP-серверами, чтобы приложения могли обращаться к внешним инструментам и сервисам.", "members.admin": "Администратор", "members.adminTip": "Может создавать приложения и управлять настройками команды", + "members.alreadyInTeam": "Уже в команде", + "members.alreadyInTeamTip": "Эти пользователи уже имеют доступ к этому рабочему пространству.", "members.builder": "Разработчик", "members.builderTip": "Может создавать и редактировать собственные приложения", "members.datasetOperator": "Администратор знаний", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Приглашен как пользователь с ролью {{role}}", "members.lastActive": "ПОСЛЕДНЯЯ АКТИВНОСТЬ", "members.name": "ИМЯ", + "members.noNewInvitationsSent": "Новые приглашения не отправлены", "members.normal": "Обычный", "members.normalTip": "Может только использовать приложения, не может создавать приложения", "members.ok": "ОК", diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json index f317bcb96db..af24887d274 100644 --- a/web/i18n/sl-SI/common.json +++ b/web/i18n/sl-SI/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Povežite in upravljajte strežnike MCP, da aplikacijam omogočite dostop do zunanjih orodij in storitev.", "members.admin": "Administrator", "members.adminTip": "Lahko ustvarja aplikacije in upravlja nastavitve ekipe", + "members.alreadyInTeam": "Že v ekipi", + "members.alreadyInTeamTip": "Ti uporabniki že imajo dostop do tega delovnega prostora.", "members.builder": "Graditelj", "members.builderTip": "Lahko ustvarja in ureja lastne aplikacije", "members.datasetOperator": "Skrbnik znanja", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Povabljen kot uporabnik {{role}}", "members.lastActive": "NAZADNJE AKTIVEN", "members.name": "IME", + "members.noNewInvitationsSent": "Nova povabila niso bila poslana", "members.normal": "Običajni uporabnik", "members.normalTip": "Lahko uporablja samo aplikacije, ne more ustvarjati aplikacij", "members.ok": "V redu", diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json index 8a1184c0fe1..41c5ed7ad80 100644 --- a/web/i18n/th-TH/common.json +++ b/web/i18n/th-TH/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "เชื่อมต่อและจัดการเซิร์ฟเวอร์ MCP เพื่อให้แอปของคุณเข้าถึงเครื่องมือและบริการภายนอกได้", "members.admin": "ผู้ดูแลระบบ", "members.adminTip": "สามารถสร้างแอพและจัดการการตั้งค่าทีมได้", + "members.alreadyInTeam": "อยู่ในทีมแล้ว", + "members.alreadyInTeamTip": "ผู้ใช้เหล่านี้มีสิทธิ์เข้าถึงพื้นที่ทำงานนี้อยู่แล้ว", "members.builder": "ผู้สร้าง", "members.builderTip": "สามารถสร้างและแก้ไขแอปของตัวเองได้", "members.datasetOperator": "ผู้ดูแลระบบความรู้", @@ -273,6 +275,7 @@ "members.invitedAsRole": "ได้รับเชิญให้เป็นผู้ใช้ {{role}}", "members.lastActive": "ใช้งานล่าสุด", "members.name": "ชื่อ", + "members.noNewInvitationsSent": "ไม่มีการส่งคำเชิญใหม่", "members.normal": "ปกติ", "members.normalTip": "ใช้ได้เฉพาะแอพ สร้างแอพไม่ได้", "members.ok": "ตกลง, ได้", diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json index 7d8e7096feb..93f9bd14f42 100644 --- a/web/i18n/tr-TR/common.json +++ b/web/i18n/tr-TR/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Uygulamalarınıza harici araç ve servislere erişim vermek için MCP sunucularını bağlayın ve yönetin.", "members.admin": "Yönetici", "members.adminTip": "Uygulama oluşturabilir ve takım ayarlarını yönetebilir", + "members.alreadyInTeam": "Zaten ekipte", + "members.alreadyInTeamTip": "Bu kullanıcıların bu çalışma alanına zaten erişimi var.", "members.builder": "Oluşturucu", "members.builderTip": "Kendi uygulamalarını oluşturup düzenleyebilir", "members.datasetOperator": "Bilgi Yöneticisi", @@ -273,6 +275,7 @@ "members.invitedAsRole": "{{role}} kullanıcısı olarak davet edildi", "members.lastActive": "SON AKTİF", "members.name": "İSİM", + "members.noNewInvitationsSent": "Yeni davet gönderilmedi", "members.normal": "Normal", "members.normalTip": "Sadece uygulamaları kullanabilir, uygulama oluşturamaz", "members.ok": "Tamam", diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json index 995d6ef0318..ff458338d3d 100644 --- a/web/i18n/uk-UA/common.json +++ b/web/i18n/uk-UA/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Підключайте й керуйте MCP-серверами, щоб ваші застосунки мали доступ до зовнішніх інструментів і сервісів.", "members.admin": "Адміністратор", "members.adminTip": "Може створювати програми та керувати налаштуваннями команди", + "members.alreadyInTeam": "Уже в команді", + "members.alreadyInTeamTip": "Ці користувачі вже мають доступ до цього робочого простору.", "members.builder": "Будівник", "members.builderTip": "Може створювати та редагувати власні програми", "members.datasetOperator": "Адміністратор знань", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Запрошено як користувача {{role}}", "members.lastActive": "ОСТАННЯ АКТИВНІСТЬ", "members.name": "ІМ'Я", + "members.noNewInvitationsSent": "Нові запрошення не надіслано", "members.normal": "Звичайний", "members.normalTip": "Може лише використовувати програми, не може створювати програми", "members.ok": "ОК", diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json index 2fb270d7260..3d344ce5767 100644 --- a/web/i18n/vi-VN/common.json +++ b/web/i18n/vi-VN/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "Kết nối và quản lý máy chủ MCP để ứng dụng của bạn truy cập các công cụ và dịch vụ bên ngoài.", "members.admin": "Quản trị viên", "members.adminTip": "Có thể xây dựng ứng dụng và quản lý cài đặt nhóm", + "members.alreadyInTeam": "Đã ở trong nhóm", + "members.alreadyInTeamTip": "Những người dùng này đã có quyền truy cập vào không gian làm việc này.", "members.builder": "Chủ thầu", "members.builderTip": "Có thể xây dựng và chỉnh sửa ứng dụng của riêng mình", "members.datasetOperator": "Quản trị viên kiến thức", @@ -273,6 +275,7 @@ "members.invitedAsRole": "Được mời với vai trò {{role}}", "members.lastActive": "HOẠT ĐỘNG GẦN ĐÂY", "members.name": "TÊN", + "members.noNewInvitationsSent": "Không có lời mời mới nào được gửi", "members.normal": "Bình thường", "members.normalTip": "Chỉ có thể sử dụng ứng dụng, không thể xây dựng ứng dụng", "members.ok": "OK", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 0245ca738b9..b7903ed354f 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "连接并管理 MCP 服务器,让你的应用可以访问外部工具与服务。", "members.admin": "管理员", "members.adminTip": "能够建立应用程序和管理团队设置", + "members.alreadyInTeam": "已在团队中", + "members.alreadyInTeamTip": "以下用户已经可以访问此工作空间。", "members.builder": "构建器", "members.builderTip": "可以构建和编辑自己的应用程序", "members.datasetOperator": "知识库管理员", @@ -273,6 +275,7 @@ "members.invitedAsRole": "邀请为{{role}}用户", "members.lastActive": "上次活动时间", "members.name": "姓名", + "members.noNewInvitationsSent": "没有新的邀请发送", "members.normal": "成员", "members.normalTip": "只能使用应用程序,不能建立应用程序", "members.ok": "好的", diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json index 8e586d5640e..0dab2a8b720 100644 --- a/web/i18n/zh-Hant/common.json +++ b/web/i18n/zh-Hant/common.json @@ -251,6 +251,8 @@ "mcpPage.description": "連接並管理 MCP 伺服器,讓你的應用可以存取外部工具和服務。", "members.admin": "管理員", "members.adminTip": "能夠建立應用程式和管理團隊設定", + "members.alreadyInTeam": "已在團隊中", + "members.alreadyInTeamTip": "以下使用者已經可以存取此工作區。", "members.builder": "建築工人", "members.builderTip": "可以構建和編輯自己的應用程式", "members.datasetOperator": "知識管理員", @@ -273,6 +275,7 @@ "members.invitedAsRole": "邀請為{{role}}使用者", "members.lastActive": "上次活動時間", "members.name": "姓名", + "members.noNewInvitationsSent": "沒有新的邀請送出", "members.normal": "成員", "members.normalTip": "只能使用應用程式,不能建立應用程式", "members.ok": "好的", diff --git a/web/models/common.ts b/web/models/common.ts index efc4c11839c..d41de93ab0c 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -171,6 +171,10 @@ export type InvitationResult = { status: 'success' email: string url: string +} | { + status: 'already_member' + email: string + message?: string } | { status: 'failed' email: string diff --git a/web/service/common.ts b/web/service/common.ts index 3f9c707fd02..34216703012 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -167,11 +167,26 @@ export const updatePluginProviderAIKey = ({ url, body }: { url: string, body: { return post(url, { body }) } -export const invitationCheck = ({ url, params }: { url: string, params: { workspace_id?: string, email?: string, token: string } }): Promise => { - return get(url, { params }) +type InvitationCheckData = { + workspace_name: string + email: string + workspace_id: string + account_status?: string + requires_setup?: boolean } -export const activateMember = ({ url, body }: { url: string, body: any }): Promise => { +type ActivateMemberBody = { + token: string + name?: string + interface_language?: string + timezone?: string +} + +export const invitationCheck = ({ url, params }: { url: string, params: { workspace_id?: string, email?: string, token: string } }): Promise => { + return get(url, { params }) +} + +export const activateMember = ({ url, body }: { url: string, body: ActivateMemberBody }): Promise => { return post(url, { body }) } diff --git a/web/service/use-common.ts b/web/service/use-common.ts index d315a876022..4a39c5c8dd4 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -248,7 +248,7 @@ export const useInvitationCheck = (params?: { workspace_id?: string, email?: str queryKey: commonQueryKeys.invitationCheck(params), queryFn: () => get<{ is_valid: boolean - data: { workspace_name: string, email: string, workspace_id: string } + data: { workspace_name: string, email: string, workspace_id: string, account_status?: string, requires_setup?: boolean } result: string }>('/activate/check', { params }), enabled: enabled ?? !!params?.token,