Skip to content

Commit

Permalink
fix: address org invite resend
Browse files Browse the repository at this point in the history
  • Loading branch information
sheensantoscapadngan committed Jan 16, 2025
1 parent b440e91 commit c592ff0
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 40 deletions.
34 changes: 34 additions & 0 deletions backend/src/server/routes/v1/invite-org-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
65 changes: 64 additions & 1 deletion backend/src/services/org/org-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -1221,6 +1283,7 @@ export const orgServiceFactory = ({
deleteIncidentContact,
getOrgGroups,
listProjectMembershipsByOrgMembershipId,
findOrgBySlug
findOrgBySlug,
resendOrgMemberInvitation
};
};
4 changes: 4 additions & 0 deletions backend/src/services/org/org-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export type TInviteUserToOrgDTO = {
}[];
} & TOrgPermission;

export type TResendOrgMemberInvitationDTO = {
membershipId: string;
} & TOrgPermission;

export type TVerifyUserToOrgDTO = {
email: string;
orgId: string;
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/hooks/api/users/mutation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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, isPending } = useResendOrgMemberInvitation();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();

const onRoleChange = async (membershipId: string, role: string) => {
Expand Down Expand Up @@ -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"
});
}
Expand Down Expand Up @@ -370,7 +369,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
className="w-48"
colorSchema="primary"
variant="outline_bg"
onClick={() => onResendInvite(email)}
isLoading={isPending}
onClick={() => onResendInvite(orgMembershipId)}
>
Resend invite
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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"
});
}
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit c592ff0

Please sign in to comment.