feat: migrate nextjs to rr7

This commit is contained in:
David Nguyen
2025-01-02 15:33:37 +11:00
committed by Mythie
parent 9183f668d3
commit 75d7336763
1021 changed files with 60930 additions and 40839 deletions

View File

@ -0,0 +1,22 @@
import type { User } from '@prisma/client';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
export type CreateBillingPortalOptions = {
user: Pick<User, 'id' | 'customerId' | 'email' | 'name'>;
};
export const createBillingPortal = async ({ user }: CreateBillingPortalOptions) => {
if (!IS_BILLING_ENABLED()) {
throw new Error('Billing is not enabled');
}
const { stripeCustomer } = await getStripeCustomerByUser(user);
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
});
};

View File

@ -0,0 +1,39 @@
import type { User } from '@prisma/client';
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getSubscriptionsByUserId } from '../subscription/get-subscriptions-by-user-id';
export type CreateCheckoutSession = {
user: Pick<User, 'id' | 'customerId' | 'email' | 'name'>;
priceId: string;
};
export const createCheckoutSession = async ({ user, priceId }: CreateCheckoutSession) => {
const { stripeCustomer } = await getStripeCustomerByUser(user);
const existingSubscriptions = await getSubscriptionsByUserId({ userId: user.id });
const foundSubscription = existingSubscriptions.find(
(subscription) =>
subscription.priceId === priceId &&
subscription.periodEnd &&
subscription.periodEnd >= new Date(),
);
if (foundSubscription) {
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
});
}
return getCheckoutSession({
customerId: stripeCustomer.id,
priceId,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
});
};

View File

@ -1,9 +1,10 @@
import { hash } from '@node-rs/bcrypt';
import type { User } from '@prisma/client';
import { TeamMemberInviteStatus } from '@prisma/client';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { prisma } from '@documenso/prisma';
import { IdentityProvider, TeamMemberInviteStatus } from '@documenso/prisma/client';
import { IS_BILLING_ENABLED } from '../../constants/app';
import { SALT_ROUNDS } from '../../constants/auth';
@ -39,24 +40,54 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
});
if (urlExists) {
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
throw new AppError('PROFILE_URL_TAKEN', {
message: 'Profile username is taken',
userMessage: 'The profile username is already taken',
});
}
}
const user = await prisma.user.create({
data: {
name,
email: email.toLowerCase(),
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
url,
},
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,
url,
},
});
// 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;
});
await onCreateUserHook(user).catch((err) => {
// Todo: (RR7) Add logging.
console.error(err);
});
return user;
};
/**
* Should be run after a user is created.
*
* @returns User
*/
export const onCreateUserHook = async (user: User) => {
const { email } = user;
const acceptedTeamInvites = await prisma.teamMemberInvite.findMany({
where: {
status: TeamMemberInviteStatus.ACCEPTED,

View File

@ -1,5 +1,6 @@
import { DocumentStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { deletedAccountServiceAccount } from './service-accounts/deleted-account';
@ -28,7 +29,7 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
where: {
userId: user.id,
status: {
in: [DocumentStatus.PENDING, DocumentStatus.COMPLETED],
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
},
},
data: {

View File

@ -1,5 +1,6 @@
import type { UserSecurityAuditLog, UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { UserSecurityAuditLog, UserSecurityAuditLogType } from '@documenso/prisma/client';
import type { FindResultResponse } from '../../types/search-params';

View File

@ -1,12 +1,11 @@
import crypto from 'crypto';
import { prisma } from '@documenso/prisma';
import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
import { ONE_DAY, ONE_HOUR } from '../../constants/time';
import { ONE_DAY } from '../../constants/time';
import { sendForgotPassword } from '../auth/send-forgot-password';
export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => {
export const forgotPassword = async ({ email }: { email: string }) => {
const user = await prisma.user.findFirst({
where: {
email: {
@ -21,21 +20,21 @@ export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => {
}
// Find a token that was created in the last hour and hasn't expired
const existingToken = await prisma.passwordResetToken.findFirst({
where: {
userId: user.id,
expiry: {
gt: new Date(),
},
createdAt: {
gt: new Date(Date.now() - ONE_HOUR),
},
},
});
// const existingToken = await prisma.passwordResetToken.findFirst({
// where: {
// userId: user.id,
// expiry: {
// gt: new Date(),
// },
// createdAt: {
// gt: new Date(Date.now() - ONE_HOUR),
// },
// },
// });
if (existingToken) {
return;
}
// if (existingToken) {
// return;
// }
const token = crypto.randomBytes(18).toString('hex');

View File

@ -1,5 +1,6 @@
import { Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { Prisma } from '@documenso/prisma/client';
type GetAllUsersProps = {
username: string;

View File

@ -1,7 +1,7 @@
import { DocumentStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
export const getCompletedDocumentsMonthly = async () => {
const qb = kyselyPrisma.$kysely

View File

@ -1,5 +1,6 @@
import type { UserProfile } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { UserProfile } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { updatePublicProfile } from './update-public-profile';

View File

@ -1,12 +1,12 @@
import { compare, hash } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobsClient } from '../../jobs/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { sendResetPassword } from '../auth/send-reset-password';
export type ResetPasswordOptions = {
token: string;
@ -46,29 +46,36 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
const hashedPassword = await hash(password, SALT_ROUNDS);
await prisma.$transaction([
prisma.user.update({
await prisma.$transaction(async (tx) => {
await tx.user.update({
where: {
id: foundToken.userId,
},
data: {
password: hashedPassword,
},
}),
prisma.passwordResetToken.deleteMany({
});
await tx.passwordResetToken.deleteMany({
where: {
userId: foundToken.userId,
},
}),
prisma.userSecurityAuditLog.create({
});
await tx.userSecurityAuditLog.create({
data: {
userId: foundToken.userId,
type: UserSecurityAuditLogType.PASSWORD_RESET,
userAgent: requestMetadata?.userAgent,
ipAddress: requestMetadata?.ipAddress,
},
}),
]);
});
await sendResetPassword({ userId: foundToken.userId });
await jobsClient.triggerJob({
name: 'send.password.reset.success.email',
payload: {
userId: foundToken.userId,
},
});
});
};

View File

@ -1,9 +1,9 @@
import { compare, hash } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { SALT_ROUNDS } from '@documenso/lib/constants/auth';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AppError } from '../../errors/app-error';

View File

@ -1,5 +1,6 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';

View File

@ -59,7 +59,7 @@ export const updatePublicProfile = async ({ userId, data }: UpdatePublicProfileO
});
if (isUrlTakenByAnotherUser || isUrlTakenByAnotherTeam) {
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
throw new AppError('PROFILE_URL_TAKEN', {
message: 'The profile username is already taken',
});
}

View File

@ -2,15 +2,9 @@ import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { EMAIL_VERIFICATION_STATE } from '../../constants/email';
import { jobsClient } from '../../jobs/client';
export const EMAIL_VERIFICATION_STATE = {
NOT_FOUND: 'NOT_FOUND',
VERIFIED: 'VERIFIED',
EXPIRED: 'EXPIRED',
ALREADY_VERIFIED: 'ALREADY_VERIFIED',
} as const;
export type VerifyEmailProps = {
token: string;
};
@ -26,7 +20,10 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
if (!verificationToken) {
return EMAIL_VERIFICATION_STATE.NOT_FOUND;
return {
state: EMAIL_VERIFICATION_STATE.NOT_FOUND,
userId: null,
};
}
// check if the token is valid or expired
@ -55,11 +52,17 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
}
return EMAIL_VERIFICATION_STATE.EXPIRED;
return {
state: EMAIL_VERIFICATION_STATE.EXPIRED,
userId: null,
};
}
if (verificationToken.completed) {
return EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED;
return {
state: EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED,
userId: null,
};
}
const [updatedUser] = await prisma.$transaction([
@ -94,5 +97,8 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
throw new Error('Something went wrong while verifying your email. Please try again.');
}
return EMAIL_VERIFICATION_STATE.VERIFIED;
return {
state: EMAIL_VERIFICATION_STATE.VERIFIED,
userId: updatedUser.id,
};
};