Files
esign/packages/lib/server-only/organisation/link-organisation-account.ts
zooqueen fb4e3df0e3 rename: @hanzo/sign-* → @hanzo/esign-* (full eSign brand consistency)
The product is branded Hanzo eSign (esign.hanzo.ai, repo hanzoai/esign).
Rename all workspace packages @hanzo/sign-* → @hanzo/esign-* (1052 files)
and align docker/Dockerfile turbo --scope/--filter to the actual remix
package name @hanzo/esign (was a pre-existing mismatch with -remix that
broke the build). Protected deps (@hanzo/insights, @hanzo/logo) untouched.
2026-06-21 19:56:09 -07:00

158 lines
4.7 KiB
TypeScript

import { UserSecurityAuditLogType } from '@prisma/client';
import { getOrganisationAuthenticationPortalOptions } from '@hanzo/esign-auth/server/lib/utils/organisation-portal';
import { prisma } from '@hanzo/esign-prisma';
import {
ORGANISATION_ACCOUNT_LINK_VERIFICATION_TOKEN_IDENTIFIER,
ORGANISATION_USER_ACCOUNT_TYPE,
} from '../../constants/organisations';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { ZOrganisationAccountLinkMetadataSchema } from '../../types/organisation';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { addUserToOrganisation } from './accept-organisation-invitation';
export interface LinkOrganisationAccountOptions {
token: string;
requestMeta: RequestMetadata;
}
export const linkOrganisationAccount = async ({
token,
requestMeta,
}: LinkOrganisationAccountOptions) => {
// 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 },
);
};