fix: address org invite resend

This commit is contained in:
Sheen Capadngan
2025-01-17 01:42:35 +08:00
parent b440e918ac
commit c592ff00a6
6 changed files with 149 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({ server.route({
url: "/verify", url: "/verify",
method: "POST", method: "POST",

View File

@@ -69,6 +69,7 @@ import {
TGetOrgMembershipDTO, TGetOrgMembershipDTO,
TInviteUserToOrgDTO, TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO, TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO, TUpdateOrgDTO,
TUpdateOrgMembershipDTO, TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO TVerifyUserToOrgDTO
@@ -584,6 +585,66 @@ export const orgServiceFactory = ({
}); });
return membership; 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 * Invite user to organization
*/ */
@@ -627,6 +688,7 @@ export const orgServiceFactory = ({
} }
}) })
: []; : [];
if (projectsToInvite.length !== invitedProjects?.length) { if (projectsToInvite.length !== invitedProjects?.length) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Access denied to one or more of the specified projects" message: "Access denied to one or more of the specified projects"
@@ -1221,6 +1283,7 @@ export const orgServiceFactory = ({
deleteIncidentContact, deleteIncidentContact,
getOrgGroups, getOrgGroups,
listProjectMembershipsByOrgMembershipId, listProjectMembershipsByOrgMembershipId,
findOrgBySlug findOrgBySlug,
resendOrgMemberInvitation
}; };
}; };

View File

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

View File

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