chore: merge main

This commit is contained in:
Catalin Pit
2025-09-11 17:20:24 +03:00
100 changed files with 6161 additions and 513 deletions

View File

@ -8,7 +8,10 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import {
DOCUMENSO_INTERNAL_EMAIL,
USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
} from '../../constants/email';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps {
@ -16,15 +19,15 @@ export interface SendConfirmationEmailProps {
}
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
const NEXT_PRIVATE_SMTP_FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME');
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS');
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
include: {
verificationTokens: {
where: {
identifier: USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
},
orderBy: {
createdAt: 'desc',
},
@ -41,8 +44,6 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const confirmationLink = `${assetBaseUrl}/verify-email/${verificationToken.token}`;
const senderName = NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
const senderAddress = NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com';
const confirmationTemplate = createElement(ConfirmEmailTemplate, {
assetBaseUrl,
@ -61,10 +62,7 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
address: user.email,
name: user.name || '',
},
from: {
name: senderName,
address: senderAddress,
},
from: DOCUMENSO_INTERNAL_EMAIL,
subject: i18n._(msg`Please confirm your email`),
html,
text,

View File

@ -0,0 +1,21 @@
import * as fs from 'node:fs';
import { env } from '@documenso/lib/utils/env';
export type CertificateStatus = {
isAvailable: boolean;
};
export const getCertificateStatus = (): CertificateStatus => {
const defaultPath =
env('NODE_ENV') === 'production' ? '/opt/documenso/cert.p12' : './example/cert.p12';
const filePath = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH') || defaultPath;
try {
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
const stats = fs.statSync(filePath);
return { isAvailable: stats.size > 0 };
} catch {
return { isAvailable: false };
}
};

View File

@ -1,3 +1,4 @@
import type { OrganisationGroup, OrganisationMemberRole } from '@prisma/client';
import { OrganisationGroupType, OrganisationMemberInviteStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
@ -23,11 +24,7 @@ export const acceptOrganisationInvitation = async ({
include: {
organisation: {
include: {
groups: {
include: {
teamGroups: true,
},
},
groups: true,
},
},
},
@ -45,6 +42,9 @@ export const acceptOrganisationInvitation = async ({
where: {
email: organisationMemberInvite.email,
},
select: {
id: true,
},
});
if (!user) {
@ -55,10 +55,49 @@ export const acceptOrganisationInvitation = async ({
const { organisation } = organisationMemberInvite;
const organisationGroupToUse = organisation.groups.find(
const isUserPartOfOrganisation = await prisma.organisationMember.findFirst({
where: {
userId: user.id,
organisationId: organisation.id,
},
});
if (isUserPartOfOrganisation) {
return;
}
await addUserToOrganisation({
userId: user.id,
organisationId: organisation.id,
organisationGroups: organisation.groups,
organisationMemberRole: organisationMemberInvite.organisationRole,
});
await prisma.organisationMemberInvite.update({
where: {
id: organisationMemberInvite.id,
},
data: {
status: OrganisationMemberInviteStatus.ACCEPTED,
},
});
};
export const addUserToOrganisation = async ({
userId,
organisationId,
organisationGroups,
organisationMemberRole,
}: {
userId: number;
organisationId: string;
organisationGroups: OrganisationGroup[];
organisationMemberRole: OrganisationMemberRole;
}) => {
const organisationGroupToUse = organisationGroups.find(
(group) =>
group.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
group.organisationRole === organisationMemberInvite.organisationRole,
group.organisationRole === organisationMemberRole,
);
if (!organisationGroupToUse) {
@ -72,8 +111,8 @@ export const acceptOrganisationInvitation = async ({
await tx.organisationMember.create({
data: {
id: generateDatabaseId('member'),
userId: user.id,
organisationId: organisation.id,
userId,
organisationId,
organisationGroupMembers: {
create: {
id: generateDatabaseId('group_member'),
@ -83,20 +122,11 @@ export const acceptOrganisationInvitation = async ({
},
});
await tx.organisationMemberInvite.update({
where: {
id: organisationMemberInvite.id,
},
data: {
status: OrganisationMemberInviteStatus.ACCEPTED,
},
});
await jobs.triggerJob({
name: 'send.organisation-member-joined.email',
payload: {
organisationId: organisation.id,
memberUserId: user.id,
organisationId,
memberUserId: userId,
},
});
},

View File

@ -75,6 +75,16 @@ export const createOrganisation = async ({
},
});
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
@ -87,6 +97,7 @@ export const createOrganisation = async ({
ownerUserId: userId,
organisationGlobalSettingsId: organisationSetting.id,
organisationClaimId: organisationClaim.id,
organisationAuthenticationPortalId: organisationAuthenticationPortal.id,
groups: {
create: ORGANISATION_INTERNAL_GROUPS.map((group) => ({
...group,

View File

@ -0,0 +1,108 @@
import { Prisma } from '@prisma/client';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
export type GetRecipientSuggestionsOptions = {
userId: number;
teamId?: number;
query: string;
};
export const getRecipientSuggestions = async ({
userId,
teamId,
query,
}: GetRecipientSuggestionsOptions) => {
const trimmedQuery = query.trim();
const nameEmailFilter = trimmedQuery
? {
OR: [
{
name: {
contains: trimmedQuery,
mode: Prisma.QueryMode.insensitive,
},
},
{
email: {
contains: trimmedQuery,
mode: Prisma.QueryMode.insensitive,
},
},
],
}
: {};
const recipients = await prisma.recipient.findMany({
where: {
document: {
team: buildTeamWhereQuery({ teamId, userId }),
},
...nameEmailFilter,
},
select: {
name: true,
email: true,
document: {
select: {
createdAt: true,
},
},
},
distinct: ['email'],
orderBy: {
document: {
createdAt: 'desc',
},
},
take: 5,
});
if (teamId) {
const teamMembers = await prisma.organisationMember.findMany({
where: {
user: {
...nameEmailFilter,
NOT: { id: userId },
},
organisationGroupMembers: {
some: {
group: {
teamGroups: {
some: { teamId },
},
},
},
},
},
include: {
user: {
select: {
email: true,
name: true,
},
},
},
take: 5,
});
const uniqueTeamMember = teamMembers.find(
(member) => !recipients.some((r) => r.email === member.user.email),
);
if (uniqueTeamMember) {
const teamMemberSuggestion = {
email: uniqueTeamMember.user.email,
name: uniqueTeamMember.user.name,
};
const allSuggestions = [...recipients.slice(0, 4), teamMemberSuggestion];
return allSuggestions;
}
}
return recipients;
};

View File

@ -1,5 +1,7 @@
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface GetRecipientsForTemplateOptions {
templateId: number;
userId: number;
@ -14,21 +16,12 @@ export const getRecipientsForTemplate = async ({
const recipients = await prisma.recipient.findMany({
where: {
templateId,
template: teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
},
template: {
team: buildTeamWhereQuery({
teamId,
userId,
}),
},
},
orderBy: {
id: 'asc',

View File

@ -1,41 +0,0 @@
import crypto from 'crypto';
import { prisma } from '@documenso/prisma';
import { ONE_HOUR } from '../../constants/time';
import { sendConfirmationEmail } from '../auth/send-confirmation-email';
const IDENTIFIER = 'confirmation-email';
export const generateConfirmationToken = async ({ email }: { email: string }) => {
const token = crypto.randomBytes(20).toString('hex');
const user = await prisma.user.findFirst({
where: {
email: email,
},
});
if (!user) {
throw new Error('User not found');
}
const createdToken = await prisma.verificationToken.create({
data: {
identifier: IDENTIFIER,
token: token,
expires: new Date(Date.now() + ONE_HOUR),
user: {
connect: {
id: user.id,
},
},
},
});
if (!createdToken) {
throw new Error(`Failed to create the verification token`);
}
return sendConfirmationEmail({ userId: user.id });
};

View File

@ -0,0 +1,21 @@
import { prisma } from '@documenso/prisma';
import { USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER } from '../../constants/email';
export type getMostRecentEmailVerificationTokenOptions = {
userId: number;
};
export const getMostRecentEmailVerificationToken = async ({
userId,
}: getMostRecentEmailVerificationTokenOptions) => {
return await prisma.verificationToken.findFirst({
where: {
userId,
identifier: USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
},
orderBy: {
createdAt: 'desc',
},
});
};

View File

@ -1,18 +0,0 @@
import { prisma } from '@documenso/prisma';
export type GetMostRecentVerificationTokenByUserIdOptions = {
userId: number;
};
export const getMostRecentVerificationTokenByUserId = async ({
userId,
}: GetMostRecentVerificationTokenByUserIdOptions) => {
return await prisma.verificationToken.findFirst({
where: {
userId,
},
orderBy: {
createdAt: 'desc',
},
});
};

View File

@ -3,11 +3,10 @@ import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER } from '../../constants/email';
import { ONE_HOUR } from '../../constants/time';
import { sendConfirmationEmail } from '../auth/send-confirmation-email';
import { getMostRecentVerificationTokenByUserId } from './get-most-recent-verification-token-by-user-id';
const IDENTIFIER = 'confirmation-email';
import { getMostRecentEmailVerificationToken } from './get-most-recent-email-verification-token';
type SendConfirmationTokenOptions = { email: string; force?: boolean };
@ -31,7 +30,7 @@ export const sendConfirmationToken = async ({
throw new Error('Email verified');
}
const mostRecentToken = await getMostRecentVerificationTokenByUserId({ userId: user.id });
const mostRecentToken = await getMostRecentEmailVerificationToken({ userId: user.id });
// If we've sent a token in the last 5 minutes, don't send another one
if (
@ -44,7 +43,7 @@ export const sendConfirmationToken = async ({
const createdToken = await prisma.verificationToken.create({
data: {
identifier: IDENTIFIER,
identifier: USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
token: token,
expires: new Date(Date.now() + ONE_HOUR),
user: {

View File

@ -2,7 +2,10 @@ import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { EMAIL_VERIFICATION_STATE } from '../../constants/email';
import {
EMAIL_VERIFICATION_STATE,
USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
} from '../../constants/email';
import { jobsClient } from '../../jobs/client';
export type VerifyEmailProps = {
@ -22,6 +25,7 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
},
where: {
token,
identifier: USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
},
});