diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index 72330aba5f0..b9ed4bf67e2 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -1,4 +1,5 @@ from http import HTTPStatus +from typing import Annotated, Literal from urllib import parse from uuid import UUID @@ -43,7 +44,7 @@ from services.feature_service import FeatureService class MemberInvitePayload(BaseModel): - emails: list[str] = Field(default_factory=list) + emails: list[str] = Field(min_length=1) role: str language: str | None = None @@ -70,11 +71,28 @@ class OwnerTransferPayload(BaseModel): token: str -class MemberInviteResultResponse(ResponseModel): - status: str +class MemberInviteSuccessResponse(ResponseModel): + status: Literal["success"] email: str - url: str | None = None - message: str | None = None + url: str + + +class MemberInviteAlreadyMemberResponse(ResponseModel): + status: Literal["already_member"] + email: str + message: str + + +class MemberInviteFailedResponse(ResponseModel): + status: Literal["failed"] + email: str + message: str + + +MemberInviteResultResponse = Annotated[ + MemberInviteSuccessResponse | MemberInviteAlreadyMemberResponse | MemberInviteFailedResponse, + Field(discriminator="status"), +] class MemberActionResponse(ResponseModel): @@ -83,7 +101,7 @@ class MemberActionResponse(ResponseModel): class MemberInviteResponse(ResponseModel): - result: str + result: Literal["success"] invitation_results: list[MemberInviteResultResponse] tenant_id: str @@ -103,7 +121,9 @@ register_response_schema_models( AccountWithRoleListResponse, MemberActionResponse, MemberInviteResponse, - MemberInviteResultResponse, + MemberInviteSuccessResponse, + MemberInviteAlreadyMemberResponse, + MemberInviteFailedResponse, SimpleResultDataResponse, SimpleResultResponse, VerificationTokenResponse, @@ -279,7 +299,7 @@ class MemberInviteEmailApi(Resource): ) encoded_invitee_email = parse.quote(invitee_email) invitation_results.append( - MemberInviteResultResponse( + MemberInviteSuccessResponse( status="success", email=invitee_email, url=f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}", @@ -287,7 +307,7 @@ class MemberInviteEmailApi(Resource): ) except AccountAlreadyInTenantError: invitation_results.append( - MemberInviteResultResponse( + MemberInviteAlreadyMemberResponse( status="already_member", email=invitee_email, message="Account already in workspace.", @@ -295,14 +315,20 @@ class MemberInviteEmailApi(Resource): ) except Exception as e: invitation_results.append( - MemberInviteResultResponse(status="failed", email=invitee_email, message=str(e)) + MemberInviteFailedResponse(status="failed", email=invitee_email, message=str(e)) ) - return MemberInviteResponse( - result="success", - invitation_results=invitation_results, - tenant_id=inviter.current_tenant.id if inviter.current_tenant else "", - ).model_dump(mode="json"), HTTPStatus.CREATED + return ( + dump_response( + MemberInviteResponse, + { + "result": "success", + "invitation_results": invitation_results, + "tenant_id": inviter.current_tenant.id if inviter.current_tenant else "", + }, + ), + HTTPStatus.CREATED, + ) @console_ns.route("/workspaces/current/members/") diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 06fccf2e9fb..d94b018f894 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -161,14 +161,24 @@ export type EndpointUpdatePayload = { } export type MemberInvitePayload = { - emails?: Array + emails: Array language?: string | null role: string } export type MemberInviteResponse = { - invitation_results: Array - result: string + invitation_results: Array< + | ({ + status: 'success' + } & MemberInviteSuccessResponse) + | ({ + status: 'already_member' + } & MemberInviteAlreadyMemberResponse) + | ({ + status: 'failed' + } & MemberInviteFailedResponse) + > + result: 'success' tenant_id: string } @@ -1128,11 +1138,22 @@ export type EndpointListItemResponse = { url: string } -export type MemberInviteResultResponse = { +export type MemberInviteSuccessResponse = { email: string - message?: string | null - status: string - url?: string | null + status: 'success' + url: string +} + +export type MemberInviteAlreadyMemberResponse = { + email: string + message: string + status: 'already_member' +} + +export type MemberInviteFailedResponse = { + email: string + message: string + status: 'failed' } export type ProviderResponse = { diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index 4c61e8b52b4..098e77d9f19 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -90,7 +90,7 @@ export const zEndpointUpdatePayload = z.object({ * MemberInvitePayload */ export const zMemberInvitePayload = z.object({ - emails: z.array(z.string()).optional(), + emails: z.array(z.string()).min(1), language: z.string().nullish(), role: z.string(), }) @@ -816,21 +816,56 @@ export const zAccountWithRoleListResponse = z.object({ }) /** - * MemberInviteResultResponse + * MemberInviteSuccessResponse */ -export const zMemberInviteResultResponse = z.object({ +export const zMemberInviteSuccessResponse = z.object({ email: z.string(), - message: z.string().nullish(), - status: z.string(), - url: z.string().nullish(), + status: z.literal('success'), + url: z.string(), +}) + +/** + * MemberInviteAlreadyMemberResponse + */ +export const zMemberInviteAlreadyMemberResponse = z.object({ + email: z.string(), + message: z.string(), + status: z.literal('already_member'), +}) + +/** + * MemberInviteFailedResponse + */ +export const zMemberInviteFailedResponse = z.object({ + email: z.string(), + message: z.string(), + status: z.literal('failed'), }) /** * MemberInviteResponse */ export const zMemberInviteResponse = z.object({ - invitation_results: z.array(zMemberInviteResultResponse), - result: z.string(), + invitation_results: z.array( + z.union([ + z + .object({ + status: z.literal('success'), + }) + .and(zMemberInviteSuccessResponse), + z + .object({ + status: z.literal('already_member'), + }) + .and(zMemberInviteAlreadyMemberResponse), + z + .object({ + status: z.literal('failed'), + }) + .and(zMemberInviteFailedResponse), + ]), + ), + result: z.literal('success'), tenant_id: z.string(), })