diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx index 660728b97..2f56a38b9 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx @@ -51,6 +51,7 @@ const ZProviderFormSchema = ZUpdateOrganisationAuthenticationPortalRequestSchema clientId: true, autoProvisionUsers: true, defaultOrganisationRole: true, + allowPersonalOrganisations: true, }) .extend({ clientSecret: z.string().nullable(), @@ -120,6 +121,7 @@ const SSOProviderForm = ({ authenticationPortal }: SSOProviderFormProps) => { autoProvisionUsers: authenticationPortal.autoProvisionUsers, defaultOrganisationRole: authenticationPortal.defaultOrganisationRole, allowedDomains: authenticationPortal.allowedDomains.join(' '), + allowPersonalOrganisations: authenticationPortal.allowPersonalOrganisations, }, }); @@ -161,6 +163,7 @@ const SSOProviderForm = ({ authenticationPortal }: SSOProviderFormProps) => { autoProvisionUsers: values.autoProvisionUsers, defaultOrganisationRole: values.defaultOrganisationRole, allowedDomains: values.allowedDomains.split(' ').filter(Boolean), + allowPersonalOrganisations: values.allowPersonalOrganisations, }, }); @@ -390,6 +393,30 @@ const SSOProviderForm = ({ authenticationPortal }: SSOProviderFormProps) => { )} /> */} + ( + +
+ + Allow Personal Organisations + +

+ + When enabled, users signing in via SSO for the first time will also receive + their own personal organisation. + +

+
+ + + + +
+ )} + /> + { + await onCreateUserHook(userToLink, { + skipPersonalOrganisation: + !organisation.organisationAuthenticationPortal.allowPersonalOrganisations, + }).catch((err) => { // Todo: (RR7) Add logging. console.error(err); }); diff --git a/packages/lib/server-only/user/create-user.ts b/packages/lib/server-only/user/create-user.ts index 53f813c82..7a815a777 100644 --- a/packages/lib/server-only/user/create-user.ts +++ b/packages/lib/server-only/user/create-user.ts @@ -60,13 +60,27 @@ export const createUser = async ({ name, email, password, signature }: CreateUse return user; }; +export type OnCreateUserHookOptions = { + /** + * When true, do not create a "Personal Organisation" for the new user. + * Used by the Organisation SSO signup path, where the user is intended + * to operate inside the SSO organisation rather than a personal space. + * + * Defaults to false — preserves the historical behaviour of creating a + * personal organisation for every new user. + */ + skipPersonalOrganisation?: boolean; +}; + /** * Should be run after a user is created, example during email password signup or google sign in. * * @returns User */ -export const onCreateUserHook = async (user: User) => { - await createPersonalOrganisation({ userId: user.id }); +export const onCreateUserHook = async (user: User, options: OnCreateUserHookOptions = {}) => { + if (!options.skipPersonalOrganisation) { + await createPersonalOrganisation({ userId: user.id }); + } return user; }; diff --git a/packages/prisma/migrations/20260504000000_add_allow_personal_organisations_to_auth_portal/migration.sql b/packages/prisma/migrations/20260504000000_add_allow_personal_organisations_to_auth_portal/migration.sql new file mode 100644 index 000000000..c353f063a --- /dev/null +++ b/packages/prisma/migrations/20260504000000_add_allow_personal_organisations_to_auth_portal/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable +-- Add the column with a temporary default of `true` so that all existing rows +-- (representing organisations created before this feature) are backfilled to +-- `true` — preserving the historical behaviour of creating personal +-- organisations for SSO-provisioned users. +ALTER TABLE "OrganisationAuthenticationPortal" ADD COLUMN "allowPersonalOrganisations" BOOLEAN NOT NULL DEFAULT true; + +-- Switch the column default to `false` so that any organisations created from +-- now on opt out of personal-organisation creation by default. +ALTER TABLE "OrganisationAuthenticationPortal" ALTER COLUMN "allowPersonalOrganisations" SET DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index f43a7e149..6b546c59a 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -1100,9 +1100,10 @@ model OrganisationAuthenticationPortal { clientSecret String @default("") wellKnownUrl String @default("") - defaultOrganisationRole OrganisationMemberRole @default(MEMBER) - autoProvisionUsers Boolean @default(true) - allowedDomains String[] @default([]) + defaultOrganisationRole OrganisationMemberRole @default(MEMBER) + autoProvisionUsers Boolean @default(true) + allowedDomains String[] @default([]) + allowPersonalOrganisations Boolean @default(false) } model Counter { diff --git a/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.ts b/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.ts index a57887a44..650611aa6 100644 --- a/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.ts +++ b/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.ts @@ -52,6 +52,7 @@ export const getOrganisationAuthenticationPortal = async ({ wellKnownUrl: true, autoProvisionUsers: true, allowedDomains: true, + allowPersonalOrganisations: true, clientSecret: true, }, }, @@ -79,6 +80,7 @@ export const getOrganisationAuthenticationPortal = async ({ wellKnownUrl: portal.wellKnownUrl, autoProvisionUsers: portal.autoProvisionUsers, allowedDomains: portal.allowedDomains, + allowPersonalOrganisations: portal.allowPersonalOrganisations, clientSecretProvided: Boolean(portal.clientSecret), }; }; diff --git a/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.types.ts b/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.types.ts index b4565ca0d..635f9aeb5 100644 --- a/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.types.ts +++ b/packages/trpc/server/enterprise-router/get-organisation-authentication-portal.types.ts @@ -14,6 +14,7 @@ export const ZGetOrganisationAuthenticationPortalResponseSchema = wellKnownUrl: true, autoProvisionUsers: true, allowedDomains: true, + allowPersonalOrganisations: true, }).extend({ /** * Whether we have the client secret in the database. diff --git a/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.ts b/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.ts index 8d8842c37..9709f8800 100644 --- a/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.ts +++ b/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.ts @@ -61,6 +61,7 @@ export const updateOrganisationAuthenticationPortalRoute = authenticatedProcedur wellKnownUrl, autoProvisionUsers, allowedDomains, + allowPersonalOrganisations, } = data; if ( @@ -104,6 +105,7 @@ export const updateOrganisationAuthenticationPortalRoute = authenticatedProcedur wellKnownUrl, autoProvisionUsers, allowedDomains, + allowPersonalOrganisations, }, }); }); diff --git a/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.types.ts b/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.types.ts index ebe5858ee..7c7b4beb6 100644 --- a/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.types.ts +++ b/packages/trpc/server/enterprise-router/update-organisation-authentication-portal.types.ts @@ -14,6 +14,7 @@ export const ZUpdateOrganisationAuthenticationPortalRequestSchema = z.object({ wellKnownUrl: z.union([z.string().url(), z.literal('')]), autoProvisionUsers: z.boolean(), allowedDomains: z.array(z.string().regex(domainRegex)), + allowPersonalOrganisations: z.boolean(), }), });