Files
documenso/packages/lib/server-only/organisation/create-organisation.ts
David Nguyen 9ac7b94d9a feat: add organisation sso portal (#1946)
Allow organisations to manage an SSO OIDC compliant portal. This method
is intended to streamline the onboarding process and paves the way to
allow organisations to manage their members in a more strict way.
2025-09-09 17:14:07 +10:00

216 lines
5.5 KiB
TypeScript

import type { Prisma } from '@prisma/client';
import { OrganisationType } from '@prisma/client';
import { OrganisationMemberRole } from '@prisma/client';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { prisma } from '@documenso/prisma';
import { IS_BILLING_ENABLED } from '../../constants/app';
import { ORGANISATION_INTERNAL_GROUPS } from '../../constants/organisations';
import { AppErrorCode } from '../../errors/app-error';
import { AppError } from '../../errors/app-error';
import type { InternalClaim } from '../../types/subscription';
import { INTERNAL_CLAIM_ID, internalClaims } from '../../types/subscription';
import { generateDatabaseId, prefixedId } from '../../universal/id';
import { generateDefaultOrganisationSettings } from '../../utils/organisations';
import { createTeam } from '../team/create-team';
type CreateOrganisationOptions = {
userId: number;
name: string;
type: OrganisationType;
url?: string;
customerId?: string;
claim: InternalClaim;
};
export const createOrganisation = async ({
name,
url,
type,
userId,
customerId,
claim,
}: CreateOrganisationOptions) => {
let customerIdToUse = customerId;
if (!customerId && IS_BILLING_ENABLED()) {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
customerIdToUse = await createCustomer({
name: user.name || user.email,
email: user.email,
})
.then((customer) => customer.id)
.catch((err) => {
console.error(err);
return undefined;
});
}
return await prisma.$transaction(async (tx) => {
const organisationSetting = await tx.organisationGlobalSettings.create({
data: {
...generateDefaultOrganisationSettings(),
id: generateDatabaseId('org_setting'),
},
});
const organisationClaim = await tx.organisationClaim.create({
data: {
id: generateDatabaseId('org_claim'),
originalSubscriptionClaimId: claim.id,
...createOrganisationClaimUpsertData(claim),
},
});
const organisationAuthenticationPortal = await tx.organisationAuthenticationPortal.create({
data: {
id: generateDatabaseId('org_sso'),
enabled: false,
clientId: '',
clientSecret: '',
wellKnownUrl: '',
},
});
const orgIdAndUrl = prefixedId('org');
const organisation = await tx.organisation
.create({
data: {
id: orgIdAndUrl,
name,
type,
url: url || orgIdAndUrl,
ownerUserId: userId,
organisationGlobalSettingsId: organisationSetting.id,
organisationClaimId: organisationClaim.id,
organisationAuthenticationPortalId: organisationAuthenticationPortal.id,
groups: {
create: ORGANISATION_INTERNAL_GROUPS.map((group) => ({
...group,
id: generateDatabaseId('org_group'),
})),
},
customerId: customerIdToUse,
},
include: {
groups: true,
},
})
.catch((err) => {
if (err.code === 'P2002') {
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Organisation URL already exists',
});
}
throw err;
});
const adminGroup = organisation.groups.find(
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
);
if (!adminGroup) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Admin group not found',
});
}
await tx.organisationMember.create({
data: {
id: generateDatabaseId('member'),
userId,
organisationId: organisation.id,
organisationGroupMembers: {
create: {
id: generateDatabaseId('group_member'),
groupId: adminGroup.id,
},
},
},
});
return organisation;
});
};
type CreatePersonalOrganisationOptions = {
userId: number;
orgUrl?: string;
throwErrorOnOrganisationCreationFailure?: boolean;
inheritMembers?: boolean;
type?: OrganisationType;
};
export const createPersonalOrganisation = async ({
userId,
orgUrl,
throwErrorOnOrganisationCreationFailure = false,
inheritMembers = true,
type = OrganisationType.PERSONAL,
}: CreatePersonalOrganisationOptions) => {
const organisation = await createOrganisation({
name: 'Personal Organisation',
userId,
url: orgUrl,
type,
claim: internalClaims[INTERNAL_CLAIM_ID.FREE],
}).catch((err) => {
console.error(err);
if (throwErrorOnOrganisationCreationFailure) {
throw err;
}
// Todo: (LOGS)
});
if (organisation) {
await createTeam({
userId,
teamName: 'Personal Team',
teamUrl: prefixedId('personal'),
organisationId: organisation.id,
inheritMembers,
}).catch((err) => {
console.error(err);
// Todo: (LOGS)
});
}
return organisation;
};
export const createOrganisationClaimUpsertData = (subscriptionClaim: InternalClaim) => {
// Done like this to ensure type errors are thrown if items are added.
const data: Omit<
Prisma.SubscriptionClaimCreateInput,
'id' | 'createdAt' | 'updatedAt' | 'locked' | 'name'
> = {
flags: {
...subscriptionClaim.flags,
},
teamCount: subscriptionClaim.teamCount,
memberCount: subscriptionClaim.memberCount,
};
return {
...data,
};
};