diff --git a/messages/en-US.json b/messages/en-US.json index 7630612ca..660ffa5c9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -43,6 +43,8 @@ "inviteLoginUser": "Please make sure you're logged in as the correct user.", "inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.", "inviteCreateUser": "Please create an account first.", + "inviteErrorOidcNotAllowed": "Invites can only be accepted by internal accounts. Sign out and log in with your password for this email.", + "inviteLoginInternalOnly": "Invites require an internal account with a password. Create an account or sign in with your password.", "goHome": "Go Home", "inviteLogInOtherUser": "Log In as a Different User", "createAnAccount": "Create an Account", diff --git a/server/lib/idp/idpExistsForOrg.ts b/server/lib/idp/idpExistsForOrg.ts index 52530e7ba..772e69bcf 100644 --- a/server/lib/idp/idpExistsForOrg.ts +++ b/server/lib/idp/idpExistsForOrg.ts @@ -1,8 +1,9 @@ import { db, idp, idpOrg, Transaction } from "@server/db"; import { and, eq } from "drizzle-orm"; +import { build } from "@server/build"; export function isOrgIdentityProviderMode(): boolean { - return process.env.IDENTITY_PROVIDER_MODE === "org"; + return build === "saas" || process.env.IDENTITY_PROVIDER_MODE === "org"; } /** diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index c912fea0e..acd9cd011 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -22,6 +22,7 @@ import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsFor import { build } from "@server/build"; import { assignUserToOrg } from "@server/lib/userOrg"; import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations"; +import { UserType } from "@server/types/UserTypes"; const acceptInviteBodySchema = z.strictObject({ token: z.string(), @@ -66,12 +67,17 @@ export async function acceptInvite( ); } - const existingUser = await db + const [existingInternalUser] = await db .select() .from(users) - .where(eq(users.email, existingInvite.email)) + .where( + and( + eq(users.email, existingInvite.email), + eq(users.type, UserType.Internal) + ) + ) .limit(1); - if (!existingUser.length) { + if (!existingInternalUser) { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -80,9 +86,8 @@ export async function acceptInvite( ); } - const { user, session } = await verifySession(req); + const { user } = await verifySession(req); - // at this point we know the user exists if (!user) { return next( createHttpError( @@ -92,7 +97,7 @@ export async function acceptInvite( ); } - if (user && user.email !== existingInvite.email) { + if (user.email !== existingInvite.email) { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -101,6 +106,15 @@ export async function acceptInvite( ); } + if (user.type !== UserType.Internal) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invites can only be accepted by internal users." + ) + ); + } + if (build == "saas") { const usage = await usageService.getUsage( existingInvite.orgId, @@ -195,7 +209,7 @@ export async function acceptInvite( await assignUserToOrg( org, { - userId: existingUser[0].userId, + userId: user.userId, orgId: existingInvite.orgId }, inviteRoleIds, @@ -208,13 +222,13 @@ export async function acceptInvite( .where(eq(userInvites.inviteId, inviteId)); logger.debug( - `User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}` + `User ${user.userId} accepted invite to org ${existingInvite.orgId}` ); }); - calculateUserClientsForOrgs(existingUser[0].userId).catch((e) => { + calculateUserClientsForOrgs(user.userId).catch((e) => { logger.error( - `Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}` + `Failed to calculate user clients after accepting invite for user ${user.userId}: ${e}` ); }); diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index d80d85999..ea02884fd 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -20,6 +20,7 @@ import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { assignUserToOrg } from "@server/lib/userOrg"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations"; +import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -239,6 +240,16 @@ export async function createOrgUser( ); } + const providerExists = await idpExistsForOrg(idpId, orgId); + if (!providerExists) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Identity provider not found in this organization" + ) + ); + } + const [idpRes] = await db .select() .from(idp) diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index cafbca55f..4f6a2b974 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -237,10 +237,13 @@ export default function Page() { return; } + const useOrgIdps = + build === "saas" || env.app.identityProviderMode === "org"; + const res = await api .get< AxiosResponse - >(build === "saas" ? `/org/${orgId}/idp` : "/idp") + >(useOrgIdps ? `/org/${orgId}/idp` : "/idp") .catch((e) => { console.error(e); toast({ @@ -301,8 +304,7 @@ export default function Page() { ); const [isSubmittingExternal, setIsSubmittingExternal] = useState(false); - const loading = - isSubmittingInternal || isSubmittingExternal; + const loading = isSubmittingInternal || isSubmittingExternal; async function onSubmitInternal() { const isValid = await internalForm.trigger(); diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 31a626281..db523f650 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -193,7 +193,10 @@ export default async function Page(props: { redirect={redirectUrl} forceLogin={forceLogin} defaultUser={defaultUser} - lastUsedIdp={lastUsedIdpForSmartLogin} + inviteMode={isInvite} + lastUsedIdp={ + isInvite ? null : lastUsedIdpForSmartLogin + } orgSignIn={ !isInvite && (build === "saas" || @@ -213,7 +216,7 @@ export default async function Page(props: { ) : ( ("rejected"); useEffect(() => { @@ -69,6 +70,12 @@ export default function InviteStatusCard({ function cardType() { if (error.includes("Invite is not for this user")) { return "wrong_user"; + } else if ( + error.includes( + "Invites can only be accepted by internal users." + ) + ) { + return "oidc_not_allowed"; } else if ( error.includes( "User does not exist. Please create an account first." @@ -166,6 +173,14 @@ export default function InviteStatusCard({

{t("inviteCreateUser")}

); + } else if (type === "oidc_not_allowed") { + return ( +
+

+ {t("inviteErrorOidcNotAllowed")} +

+
+ ); } else if (type === "user_limit_exceeded") { return (
@@ -199,6 +214,10 @@ export default function InviteStatusCard({ ); } else if (type === "user_does_not_exist") { return ; + } else if (type === "oidc_not_allowed") { + return ( + + ); } else if (type === "user_limit_exceeded") { return ( +
+ )} );