Merge pull request #3001 from Infisical/fix/address-org-invite-resend

fix: address org invite resend issues
This commit is contained in:
Sheen
2025-01-17 18:53:32 +08:00
committed by GitHub
6 changed files with 151 additions and 40 deletions

View File

@ -73,6 +73,40 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/signup-resend",
config: {
rateLimit: inviteUserRateLimit
},
method: "POST",
schema: {
body: z.object({
membershipId: z.string()
}),
response: {
200: z.object({
signupToken: z
.object({
email: z.string(),
link: z.string()
})
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.org.resendOrgMemberInvitation({
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
membershipId: req.body.membershipId
});
}
});
server.route({
url: "/verify",
method: "POST",

View File

@ -69,6 +69,7 @@ import {
TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
@ -584,6 +585,66 @@ export const orgServiceFactory = ({
});
return membership;
};
const resendOrgMemberInvitation = async ({
orgId,
actorId,
actor,
actorAuthMethod,
actorOrgId,
membershipId
}: TResendOrgMemberInvitationDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const org = await orgDAL.findOrgById(orgId);
const [inviteeOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.id` as "id"]: membershipId
});
if (inviteeOrgMembership.status !== OrgMembershipStatus.Invited) {
throw new BadRequestError({
message: "Organization invitation already accepted"
});
}
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: inviteeOrgMembership.userId,
orgId
});
if (!appCfg.isSmtpConfigured) {
return {
signupToken: {
email: inviteeOrgMembership.email as string,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeOrgMembership.email}&organization_id=${org?.id}`
}
};
}
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [inviteeOrgMembership.email as string],
substitutions: {
inviterFirstName: inviteeOrgMembership.firstName,
inviterUsername: inviteeOrgMembership.email,
organizationName: org?.name,
email: inviteeOrgMembership.email,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
return { signupToken: undefined };
};
/*
* Invite user to organization
*/
@ -627,6 +688,7 @@ export const orgServiceFactory = ({
}
})
: [];
if (projectsToInvite.length !== invitedProjects?.length) {
throw new ForbiddenRequestError({
message: "Access denied to one or more of the specified projects"
@ -1221,6 +1283,7 @@ export const orgServiceFactory = ({
deleteIncidentContact,
getOrgGroups,
listProjectMembershipsByOrgMembershipId,
findOrgBySlug
findOrgBySlug,
resendOrgMemberInvitation
};
};

View File

@ -35,6 +35,10 @@ export type TInviteUserToOrgDTO = {
}[];
} & TOrgPermission;
export type TResendOrgMemberInvitationDTO = {
membershipId: string;
} & TOrgPermission;
export type TVerifyUserToOrgDTO = {
email: string;
orgId: string;

View File

@ -156,3 +156,18 @@ export const useCreateNewTotpRecoveryCodes = () => {
}
});
};
export const useResendOrgMemberInvitation = () => {
return useMutation({
mutationFn: async (dto: { membershipId: string }) => {
const { data } = await apiRequest.post<{
signupToken?: {
email: string;
link: string;
};
}>("/api/v1/invite-org/signup-resend", dto);
return data.signupToken;
}
});
};

View File

@ -44,13 +44,13 @@ import {
} from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import {
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetOrgUsers,
useUpdateOrgMembership
} from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useResendOrgMemberInvitation } from "@app/hooks/api/users/mutation";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@ -83,7 +83,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
const { data: serverDetails } = useFetchServerStatus();
const { data: members = [], isPending: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { mutateAsync: resendOrgMemberInvitation } = useResendOrgMemberInvitation();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const onRoleChange = async (membershipId: string, role: string) => {
@ -119,26 +119,25 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
}
};
const onResendInvite = async (email: string) => {
const onResendInvite = async (membershipId: string) => {
try {
const { data } = await addUsersMutateAsync({
organizationId: orgId,
inviteeEmails: [email],
organizationRoleSlug: "member"
const signupToken = await resendOrgMemberInvitation({
membershipId
});
setCompleteInviteLinks(data?.completeInviteLinks || null);
if (!data.completeInviteLinks) {
createNotification({
text: `Successfully resent invite to ${email}`,
type: "success"
});
if (signupToken) {
setCompleteInviteLinks([signupToken]);
return;
}
createNotification({
text: "Successfully resent org invitation",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to resend invite to ${email}`,
text: "Failed to resend org invitation",
type: "error"
});
}
@ -370,7 +369,10 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
className="w-48"
colorSchema="primary"
variant="outline_bg"
onClick={() => onResendInvite(email)}
onClick={(e) => {
onResendInvite(orgMembershipId);
e.stopPropagation();
}}
>
Resend invite
</Button>

View File

@ -18,13 +18,9 @@ import {
useUser
} from "@app/context";
import { useTimedReset } from "@app/hooks";
import {
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgMembership,
useGetOrgRoles
} from "@app/hooks/api";
import { useFetchServerStatus, useGetOrgMembership, useGetOrgRoles } from "@app/hooks/api";
import { OrgUser } from "@app/hooks/api/types";
import { useResendOrgMemberInvitation } from "@app/hooks/api/users/mutation";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@ -45,28 +41,27 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
const { data: roles } = useGetOrgRoles(orgId);
const { data: serverDetails } = useFetchServerStatus();
const { data: membership } = useGetOrgMembership(orgId, membershipId);
const { mutateAsync: inviteUsers, isPending } = useAddUsersToOrg();
const onResendInvite = async (email: string) => {
const { mutateAsync: resendOrgMemberInvitation, isPending } = useResendOrgMemberInvitation();
const onResendInvite = async () => {
try {
const { data } = await inviteUsers({
organizationId: orgId,
inviteeEmails: [email],
organizationRoleSlug: "member"
const signupToken = await resendOrgMemberInvitation({
membershipId
});
// setCompleteInviteLink(data?.completeInviteLink || "");
if (!data.completeInviteLinks) {
createNotification({
text: `Successfully resent invite to ${email}`,
type: "success"
});
if (signupToken) {
return;
}
createNotification({
text: "Successfully resent org invitation",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to resend invite to ${email}`,
text: "Failed to resend org invitation",
type: "error"
});
}
@ -210,9 +205,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
colorSchema="primary"
type="submit"
isLoading={isPending}
onClick={() => {
onResendInvite(membership.user.email as string);
}}
onClick={onResendInvite}
>
Resend Invite
</Button>