feat: added change in user invitation mail for suborg and updated list operation for sub org

This commit is contained in:
=
2025-10-20 23:01:59 +05:30
parent eec2b30666
commit 0ecca6a312
10 changed files with 136 additions and 65 deletions
@@ -93,10 +93,10 @@ export const subOrgServiceFactory = ({
await permissionService.getOrgPermission({ await permissionService.getOrgPermission({
actorId: permissionActor.id, actorId: permissionActor.id,
actor: permissionActor.type, actor: permissionActor.type,
orgId: permissionActor.parentOrgId, orgId: permissionActor.rootOrgId,
actorOrgId: permissionActor.rootOrgId, actorOrgId: permissionActor.rootOrgId,
actorAuthMethod: permissionActor.authMethod, actorAuthMethod: permissionActor.authMethod,
scope: OrganizationActionScope.ParentOrganization scope: OrganizationActionScope.Any
}); });
const organizations = await orgDAL.listSubOrganizations({ const organizations = await orgDAL.listSubOrganizations({
@@ -84,6 +84,7 @@ export const newOrgMembershipUserFactory = ({
message: "Failed to invite user due to org-level auth enforced for organization" message: "Failed to invite user due to org-level auth enforced for organization"
}); });
} }
if (org.rootOrgId) { if (org.rootOrgId) {
const rootOrgMembership = await membershipUserDAL.find({ const rootOrgMembership = await membershipUserDAL.find({
scope: AccessScope.Organization, scope: AccessScope.Organization,
@@ -120,7 +121,18 @@ export const newOrgMembershipUserFactory = ({
const signUpTokens: { email: string; link: string }[] = []; const signUpTokens: { email: string; link: string }[] = [];
const orgDetails = await orgDAL.findById(dto.permission.orgId); const orgDetails = await orgDAL.findById(dto.permission.orgId);
if (orgDetails.rootOrgId) {
const emails = newUsers.map((el) => el.email).filter(Boolean);
await smtpService.sendMail({
template: SmtpTemplates.SubOrgInvite,
subjectLine: "Infisical sub-organization invitation",
recipients: emails as string[],
substitutions: {
subOrganizationName: orgDetails.slug,
callback_url: `${appCfg.SITE_URL}/organization/projects?${orgDetails.slug}`
}
});
} else {
await Promise.allSettled( await Promise.allSettled(
newUsers.map(async (el) => { newUsers.map(async (el) => {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
@@ -154,6 +166,7 @@ export const newOrgMembershipUserFactory = ({
} }
}) })
); );
}
return { signUpTokens }; return { signUpTokens };
}; };
+5 -5
View File
@@ -346,7 +346,7 @@ export const orgServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.ParentOrganization
}); });
if (!hasRole(OrgMembershipRole.Admin)) { if (!hasRole(OrgMembershipRole.Admin)) {
@@ -418,7 +418,7 @@ export const orgServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.ParentOrganization
}); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
@@ -878,7 +878,7 @@ export const orgServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.ParentOrganization
}); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
@@ -1172,7 +1172,7 @@ export const orgServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.ParentOrganization
}); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
@@ -1200,7 +1200,7 @@ export const orgServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.ParentOrganization
}); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
@@ -0,0 +1,50 @@
import { Heading, Section, Text } from "@react-email/components";
import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface SubOrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
callback_url: string;
subOrganizationName: string;
}
export const SubOrganizationInvitationTemplate = ({
callback_url,
subOrganizationName,
siteUrl
}: SubOrganizationInvitationTemplateProps) => {
return (
<BaseEmailWrapper
title="Sub-Organization Invitation"
preview="You've been invited to join a suborganization on Infisical."
siteUrl={siteUrl}
>
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You've been invited to join a suborganization on Infisical
</Heading>
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
You've been invited to join the suborganization <strong>{subOrganizationName}</strong>.
</Text>
</Section>
<Section className="text-center">
<BaseButton href={callback_url}>Join Suborganization</BaseButton>
</Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]">
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
certificates, SSH keys, and configurations across your team and infrastructure.
</Text>
</Section>
</BaseEmailWrapper>
);
};
export default SubOrganizationInvitationTemplate;
SubOrganizationInvitationTemplate.PreviewProps = {
subOrganizationName: "Example Project",
siteUrl: "https://infisical.com",
callback_url: "https://app.infisical.com"
} as SubOrganizationInvitationTemplateProps;
@@ -32,3 +32,4 @@ export * from "./SecretSyncFailedTemplate";
export * from "./ServiceTokenExpiryNoticeTemplate"; export * from "./ServiceTokenExpiryNoticeTemplate";
export * from "./SignupEmailVerificationTemplate"; export * from "./SignupEmailVerificationTemplate";
export * from "./UnlockAccountTemplate"; export * from "./UnlockAccountTemplate";
export * from "./SubOrganizationInvitationTemplate";
+4 -1
View File
@@ -40,7 +40,8 @@ import {
SecretSyncFailedTemplate, SecretSyncFailedTemplate,
ServiceTokenExpiryNoticeTemplate, ServiceTokenExpiryNoticeTemplate,
SignupEmailVerificationTemplate, SignupEmailVerificationTemplate,
UnlockAccountTemplate UnlockAccountTemplate,
SubOrganizationInvitationTemplate
} from "./emails"; } from "./emails";
export type TSmtpConfig = SMTPTransport.Options; export type TSmtpConfig = SMTPTransport.Options;
@@ -65,6 +66,7 @@ export enum SmtpTemplates {
// HistoricalSecretList = "historicalSecretLeakIncident", not used anymore? // HistoricalSecretList = "historicalSecretLeakIncident", not used anymore?
NewDeviceJoin = "newDevice", NewDeviceJoin = "newDevice",
OrgInvite = "organizationInvitation", OrgInvite = "organizationInvitation",
SubOrgInvite = "subOrganizationInvitation",
OrgAssignment = "organizationAssignment", OrgAssignment = "organizationAssignment",
OAuthPasswordReset = "oAuthPasswordReset", OAuthPasswordReset = "oAuthPasswordReset",
ResetPassword = "passwordReset", ResetPassword = "passwordReset",
@@ -102,6 +104,7 @@ export enum SmtpHost {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = { const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
[SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate, [SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate,
[SmtpTemplates.SubOrgInvite]: SubOrganizationInvitationTemplate,
[SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate, [SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate,
[SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate, [SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate,
[SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate, [SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate,
@@ -2,6 +2,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
import { useRouteContext, useSearch } from "@tanstack/react-router"; import { useRouteContext, useSearch } from "@tanstack/react-router";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
import { useMemo } from "react";
export const useOrganization = () => { export const useOrganization = () => {
const organizationId = useRouteContext({ const organizationId = useRouteContext({
@@ -20,7 +21,8 @@ export const useOrganization = () => {
staleTime: Infinity staleTime: Infinity
}); });
return { const org = useMemo(
() => ({
currentOrg: { currentOrg: {
...currentOrg, ...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id, id: currentOrg?.subOrganization?.id || currentOrg?.id,
@@ -28,5 +30,9 @@ export const useOrganization = () => {
}, },
isSubOrganization: Boolean(currentOrg.subOrganization), isSubOrganization: Boolean(currentOrg.subOrganization),
isRootOrganization: !currentOrg.subOrganization isRootOrganization: !currentOrg.subOrganization
}; }),
[currentOrg]
);
return org;
}; };
@@ -131,14 +131,14 @@ export const INFISICAL_SUPPORT_OPTIONS = [
export const Navbar = () => { export const Navbar = () => {
const { user } = useUser(); const { user } = useUser();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg, isSubOrganization } = useOrganization(); const { currentOrg } = useOrganization();
const [showAdminsModal, setShowAdminsModal] = useState(false); const [showAdminsModal, setShowAdminsModal] = useState(false);
const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showSubOrgForm, setShowSubOrgForm] = useState(false);
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
const { data: subOrganizations = [] } = useQuery({ const { data: subOrganizations = [] } = useQuery({
...subOrganizationsQuery.list({ limit: 500 }), ...subOrganizationsQuery.list({ limit: 500 }),
enabled: Boolean(subscription.subOrganization) && !isSubOrganization enabled: Boolean(subscription.subOrganization)
}); });
useEffect(() => { useEffect(() => {
@@ -41,7 +41,6 @@ export const OrgNameChangeSection = (): JSX.Element => {
const [isFormInitialized, setIsFormInitialized] = useState(false); const [isFormInitialized, setIsFormInitialized] = useState(false);
useEffect(() => { useEffect(() => {
if (currentOrg) {
reset({ reset({
name: currentOrg.name, name: currentOrg.name,
slug: currentOrg.slug, slug: currentOrg.slug,
@@ -54,8 +53,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
}) })
}); });
setIsFormInitialized(true); setIsFormInitialized(true);
} }, [roles]);
}, [currentOrg, roles]);
const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => { const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => {
try { try {
@@ -50,7 +50,7 @@ export const OrgProductSelectSection = () => {
})); }));
} }
}); });
}, [currentOrg]); }, [currentOrg?.id]);
const onProductToggle = async (value: boolean, key: string) => { const onProductToggle = async (value: boolean, key: string) => {
setIsLoading(true); setIsLoading(true);