diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2fb7e9ff7..c9e460e1e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -785,6 +785,7 @@ export const registerRoutes = async ( smtpService, authDAL, userDAL, + orgMembershipDAL, totpConfigDAL }); diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index efc8b3cc0..7c8bd5262 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -6,6 +6,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; @@ -22,6 +23,7 @@ import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; userDAL: TUserDALFactory; + orgMembershipDAL: Pick; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; totpConfigDAL: Pick; @@ -31,6 +33,7 @@ export type TAuthPasswordFactory = ReturnType; export const authPaswordServiceFactory = ({ authDAL, userDAL, + orgMembershipDAL, tokenService, smtpService, totpConfigDAL @@ -47,21 +50,46 @@ export const authPaswordServiceFactory = ({ if (user && user.isAccepted) { const cfg = getConfig(); - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, - userId: user.id - }); - await smtpService.sendMail({ - template: SmtpTemplates.ResetPassword, - recipients: [email], - subjectLine: "Infisical password reset", - substitutions: { + const hasEmailAuth = user.authMethods?.includes(AuthMethod.EMAIL); + + if (!hasEmailAuth) { + const orgMemberships = await orgMembershipDAL.find({ userId: user.id }); + const lastLoginMethod = + orgMemberships.length > 0 + ? orgMemberships.find((membership) => membership.lastLoginAuthMethod)?.lastLoginAuthMethod || null + : null; + + const substitutions = { email, - token, - callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" - } - }); + lastLoginMethod, + isCloud: cfg.isCloud, + siteUrl: cfg.SITE_URL || "" + }; + + await smtpService.sendMail({ + template: SmtpTemplates.OAuthPasswordReset, + recipients: [email], + subjectLine: "Password reset not available", + substitutions + }); + } else { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.ResetPassword, + recipients: [email], + subjectLine: "Infisical password reset", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + } + }); + } } }; diff --git a/backend/src/services/smtp/emails/OAuthPasswordResetTemplate.tsx b/backend/src/services/smtp/emails/OAuthPasswordResetTemplate.tsx new file mode 100644 index 000000000..a63ea1273 --- /dev/null +++ b/backend/src/services/smtp/emails/OAuthPasswordResetTemplate.tsx @@ -0,0 +1,84 @@ +import { Heading, Section, Text } from "@react-email/components"; + +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; + +interface OAuthPasswordResetTemplateProps extends Omit { + email: string; + lastLoginMethod?: string | null; + isCloud: boolean; +} + +export const OAuthPasswordResetTemplate = ({ + email, + lastLoginMethod, + isCloud, + siteUrl +}: OAuthPasswordResetTemplateProps) => { + const getAuthMethodDisplayName = (method: string) => { + return method + .split("-") + .map((word) => { + const upperWord = word.toUpperCase(); + if (["SAML", "LDAP", "OIDC", "SSO"].includes(upperWord)) { + return upperWord; + } + return word.charAt(0).toUpperCase() + word.slice(1); + }) + .join(" "); + }; + + const getAuthMethodMessage = () => { + if (lastLoginMethod) { + const displayName = getAuthMethodDisplayName(lastLoginMethod); + return `Please continue signing in with ${displayName}, which aligns with your account's configured authentication method.`; + } + return "Please continue using the same authentication method you previously used to sign in (e.g., SSO, SAML, OAuth, or another configured provider)."; + }; + return ( + + + Password Reset Not Available + +
+ + A password reset was requested for your Infisical account ({email}), but your account doesn't have password + login enabled. + + + Password reset is not available for this account. + + {getAuthMethodMessage()} +
+
+ + Need help? + + + If you're having trouble accessing your account or need to change your authentication method, please contact{" "} + {isCloud ? ( + <> + our support team at support@infisical.com + + ) : ( + "your instance administrator" + )} + . + +
+
+ ); +}; + +export default OAuthPasswordResetTemplate; + +OAuthPasswordResetTemplate.PreviewProps = { + email: "user@example.com", + lastLoginMethod: "github", + isCloud: true, + siteUrl: "https://infisical.com" +} as OAuthPasswordResetTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 71c338def..066744596 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -7,6 +7,7 @@ export * from "./ExternalImportStartedTemplate"; export * from "./ExternalImportSucceededTemplate"; export * from "./IntegrationSyncFailedTemplate"; export * from "./NewDeviceLoginTemplate"; +export * from "./OAuthPasswordResetTemplate"; export * from "./OrgAdminBreakglassAccessTemplate"; export * from "./OrgAdminProjectGrantAccessTemplate"; export * from "./OrganizationAssignmentTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 224f78265..d64582fe0 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -16,6 +16,7 @@ import { ExternalImportSucceededTemplate, IntegrationSyncFailedTemplate, NewDeviceLoginTemplate, + OAuthPasswordResetTemplate, OrgAdminBreakglassAccessTemplate, OrgAdminProjectGrantAccessTemplate, OrganizationAssignmentTemplate, @@ -63,6 +64,7 @@ export enum SmtpTemplates { NewDeviceJoin = "newDevice", OrgInvite = "organizationInvitation", OrgAssignment = "organizationAssignment", + OAuthPasswordReset = "oAuthPasswordReset", ResetPassword = "passwordReset", SetupPassword = "passwordSetup", SecretLeakIncident = "secretLeakIncident", @@ -121,6 +123,7 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.OrgAdminProjectDirectAccess]: OrgAdminProjectGrantAccessTemplate, [SmtpTemplates.ProjectAccessRequest]: ProjectAccessRequestTemplate, [SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate, + [SmtpTemplates.OAuthPasswordReset]: OAuthPasswordResetTemplate, [SmtpTemplates.ResetPassword]: PasswordResetTemplate, [SmtpTemplates.SetupPassword]: PasswordSetupTemplate, [SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate,