mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat: allow admins to create users (#2082)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-admin-user-created-email';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||
import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email';
|
||||
@@ -29,6 +30,7 @@ import { SYNC_EMAIL_DOMAINS_JOB_DEFINITION } from './definitions/internal/sync-e
|
||||
* triggering jobs.
|
||||
*/
|
||||
export const jobsClient = new JobClient([
|
||||
SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION,
|
||||
SEND_SIGNING_EMAIL_JOB_DEFINITION,
|
||||
SEND_CONFIRMATION_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { AdminUserCreatedTemplate } from '@documenso/email/templates/admin-user-created';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import crypto from 'crypto';
|
||||
import { createElement } from 'react';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { DOCUMENSO_INTERNAL_EMAIL } from '../../../constants/email';
|
||||
import { ONE_DAY } from '../../../constants/time';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendAdminUserCreatedEmailJobDefinition } from './send-admin-user-created-email';
|
||||
|
||||
/**
|
||||
* Send notification email for admin-created users with password reset link.
|
||||
*
|
||||
* Creates a password reset token and sends an email explaining:
|
||||
* - An administrator created their account
|
||||
* - They need to set their password
|
||||
* - Support contact if they didn't expect this
|
||||
*/
|
||||
export const run = async ({ payload, io }: { payload: TSendAdminUserCreatedEmailJobDefinition; io: JobRunIO }) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.userId,
|
||||
},
|
||||
});
|
||||
|
||||
const token = await io.runTask(`create-password-reset-token`, async () => {
|
||||
const passwordResetToken = await prisma.passwordResetToken.create({
|
||||
data: {
|
||||
token: crypto.randomBytes(18).toString('hex'),
|
||||
expiry: new Date(Date.now() + ONE_DAY),
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return passwordResetToken.token;
|
||||
});
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const resetPasswordLink = `${assetBaseUrl}/reset-password/${token}`;
|
||||
|
||||
const emailTemplate = createElement(AdminUserCreatedTemplate, {
|
||||
assetBaseUrl,
|
||||
resetPasswordLink,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailTemplate),
|
||||
renderEmailWithI18N(emailTemplate, { plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
return mailer.sendMail({
|
||||
to: {
|
||||
address: user.email,
|
||||
name: user.name || '',
|
||||
},
|
||||
from: DOCUMENSO_INTERNAL_EMAIL,
|
||||
subject: i18n._(msg`Welcome to Documenso`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID = 'send.admin.user.created.email';
|
||||
|
||||
const SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendAdminUserCreatedEmailJobDefinition = z.infer<
|
||||
typeof SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Admin User Created Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-admin-user-created-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendAdminUserCreatedEmailJobDefinition
|
||||
>;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface CreateAdminUserOptions {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user for admin-initiated flows.
|
||||
*
|
||||
* Unlike normal signup, this function:
|
||||
* - Leaves the password unset (`null`); the user must set it later via a password reset/onboarding link
|
||||
* - Marks the email as verified immediately because this route is only called by admins
|
||||
* - Does NOT create a personal organisation (user will be added to real org)
|
||||
* - Returns the user immediately without side effects
|
||||
*/
|
||||
export const createAdminUser = async ({ name, email }: CreateAdminUserOptions) => {
|
||||
const userExists = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (userExists) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'User with this email already exists',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email: email.toLowerCase(),
|
||||
password: null,
|
||||
// Verifying the email here instead of the password reset flow to reduce the
|
||||
// attack surface. This route is only called by admins.
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
@@ -26,30 +26,41 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
const user = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({
|
||||
data: {
|
||||
name,
|
||||
email: email.toLowerCase(),
|
||||
password: hashedPassword, // Todo: (RR7) Drop password.
|
||||
signature,
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: (RR7) Migrate to use this after RR7.
|
||||
// await tx.account.create({
|
||||
// data: {
|
||||
// userId: user.id,
|
||||
// type: 'emailPassword', // Todo: (RR7)
|
||||
// provider: 'DOCUMENSO', // Todo: (RR7) Enums
|
||||
// providerAccountId: user.id.toString(),
|
||||
// password: hashedPassword,
|
||||
// },
|
||||
// });
|
||||
|
||||
return user;
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email: email.toLowerCase(),
|
||||
password: hashedPassword, // Todo: (RR7) Drop password.
|
||||
signature,
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: (RR7) Migrate to use this after RR7.
|
||||
// Note: If we actually ever proceed with this, there are multiple
|
||||
// locations where we will need to update this.
|
||||
// const user = await prisma.$transaction(async (tx) => {
|
||||
// const user = await tx.user.create({
|
||||
// data: {
|
||||
// name,
|
||||
// email: email.toLowerCase(),
|
||||
// password: hashedPassword, // Todo: (RR7) Drop password.
|
||||
// signature,
|
||||
// },
|
||||
// });
|
||||
|
||||
// await tx.account.create({
|
||||
// data: {
|
||||
// userId: user.id,
|
||||
// type: 'emailPassword', // Todo: (RR7)
|
||||
// provider: 'DOCUMENSO', // Todo: (RR7) Enums
|
||||
// providerAccountId: user.id.toString(),
|
||||
// password: hashedPassword,
|
||||
// },
|
||||
// });
|
||||
|
||||
// return user;
|
||||
// });
|
||||
|
||||
// Not used at the moment, uncomment if required.
|
||||
await onCreateUserHook(user).catch((err) => {
|
||||
// Todo: (RR7) Add logging.
|
||||
|
||||
Reference in New Issue
Block a user