mirror of
https://github.com/documenso/documenso.git
synced 2026-07-22 16:03:39 +10:00
9ac7b94d9a
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.
164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
import { UserSecurityAuditLogType } from '@prisma/client';
|
|
|
|
import { getOrganisationAuthenticationPortalOptions } from '@documenso/auth/server/lib/utils/organisation-portal';
|
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
|
import {
|
|
ORGANISATION_ACCOUNT_LINK_VERIFICATION_TOKEN_IDENTIFIER,
|
|
ORGANISATION_USER_ACCOUNT_TYPE,
|
|
} from '@documenso/lib/constants/organisations';
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import { addUserToOrganisation } from '@documenso/lib/server-only/organisation/accept-organisation-invitation';
|
|
import { ZOrganisationAccountLinkMetadataSchema } from '@documenso/lib/types/organisation';
|
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
import { prisma } from '@documenso/prisma';
|
|
|
|
export interface LinkOrganisationAccountOptions {
|
|
token: string;
|
|
requestMeta: RequestMetadata;
|
|
}
|
|
|
|
export const linkOrganisationAccount = async ({
|
|
token,
|
|
requestMeta,
|
|
}: LinkOrganisationAccountOptions) => {
|
|
if (!IS_BILLING_ENABLED()) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Billing is not enabled',
|
|
});
|
|
}
|
|
|
|
// Delete the token since it contains unnecessary sensitive data.
|
|
const verificationToken = await prisma.verificationToken.delete({
|
|
where: {
|
|
token,
|
|
identifier: ORGANISATION_ACCOUNT_LINK_VERIFICATION_TOKEN_IDENTIFIER,
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
emailVerified: true,
|
|
accounts: {
|
|
select: {
|
|
provider: true,
|
|
providerAccountId: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!verificationToken) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Verification token not found, used or expired',
|
|
});
|
|
}
|
|
|
|
if (verificationToken.completed) {
|
|
throw new AppError('ALREADY_USED');
|
|
}
|
|
|
|
if (verificationToken.expires < new Date()) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Verification token not found, used or expired',
|
|
});
|
|
}
|
|
|
|
const tokenMetadata = ZOrganisationAccountLinkMetadataSchema.safeParse(
|
|
verificationToken.metadata,
|
|
);
|
|
|
|
if (!tokenMetadata.success) {
|
|
console.error('Invalid token metadata', tokenMetadata.error);
|
|
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Verification token not found, used or expired',
|
|
});
|
|
}
|
|
|
|
const user = verificationToken.user;
|
|
|
|
const { clientOptions, organisation } = await getOrganisationAuthenticationPortalOptions({
|
|
type: 'id',
|
|
organisationId: tokenMetadata.data.organisationId,
|
|
});
|
|
|
|
const organisationMember = await prisma.organisationMember.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
organisationId: tokenMetadata.data.organisationId,
|
|
},
|
|
});
|
|
|
|
const oauthConfig = tokenMetadata.data.oauthConfig;
|
|
|
|
const userAlreadyLinked = user.accounts.find(
|
|
(account) =>
|
|
account.provider === clientOptions.id &&
|
|
account.providerAccountId === oauthConfig.providerAccountId,
|
|
);
|
|
|
|
if (organisationMember && userAlreadyLinked) {
|
|
return;
|
|
}
|
|
|
|
await prisma.$transaction(
|
|
async (tx) => {
|
|
// Link the user if not linked yet.
|
|
if (!userAlreadyLinked) {
|
|
await tx.account.create({
|
|
data: {
|
|
type: ORGANISATION_USER_ACCOUNT_TYPE,
|
|
provider: clientOptions.id,
|
|
providerAccountId: oauthConfig.providerAccountId,
|
|
access_token: oauthConfig.accessToken,
|
|
expires_at: oauthConfig.expiresAt,
|
|
token_type: 'Bearer',
|
|
id_token: oauthConfig.idToken,
|
|
userId: user.id,
|
|
},
|
|
});
|
|
|
|
// Log link event.
|
|
await tx.userSecurityAuditLog.create({
|
|
data: {
|
|
userId: user.id,
|
|
ipAddress: requestMeta.ipAddress,
|
|
userAgent: requestMeta.userAgent,
|
|
type: UserSecurityAuditLogType.ORGANISATION_SSO_LINK,
|
|
},
|
|
});
|
|
|
|
// If account already exists in an unverified state, remove the password to ensure
|
|
// they cannot sign in using that method since we cannot confirm the password
|
|
// was set by the user.
|
|
if (!user.emailVerified) {
|
|
await tx.user.update({
|
|
where: {
|
|
id: user.id,
|
|
},
|
|
data: {
|
|
emailVerified: new Date(),
|
|
password: null,
|
|
// Todo: (RR7) Will need to update the "password" account after the migration.
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// Only add the user to the organisation if they are not already a member.
|
|
if (!organisationMember) {
|
|
await addUserToOrganisation({
|
|
userId: user.id,
|
|
organisationId: tokenMetadata.data.organisationId,
|
|
organisationGroups: organisation.groups,
|
|
organisationMemberRole:
|
|
organisation.organisationAuthenticationPortal.defaultOrganisationRole,
|
|
});
|
|
}
|
|
},
|
|
{ timeout: 30_000 },
|
|
);
|
|
};
|