mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 10:13:52 +10:00
Selecting password auth failed with a generic "Unauthorized" for users who signed up via OAuth or passkey, with no way to set one. Detect the missing password and email the existing reset link from the signing dialog and security settings. Require a 2FA code and rate limit update-password.
93 lines
2.1 KiB
TypeScript
93 lines
2.1 KiB
TypeScript
import { prisma } from '@documenso/prisma';
|
|
import { compare, hash } from '@node-rs/bcrypt';
|
|
import { UserSecurityAuditLogType } from '@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';
|
|
|
|
export type ResetPasswordOptions = {
|
|
token: string;
|
|
password: string;
|
|
requestMetadata?: RequestMetadata;
|
|
};
|
|
|
|
export const resetPassword = async ({ token, password, requestMetadata }: ResetPasswordOptions) => {
|
|
if (!token) {
|
|
throw new AppError('INVALID_TOKEN');
|
|
}
|
|
|
|
const foundToken = await prisma.passwordResetToken.findFirst({
|
|
where: {
|
|
token,
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
password: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!foundToken) {
|
|
throw new AppError('INVALID_TOKEN');
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
if (now > foundToken.expiry) {
|
|
throw new AppError(AppErrorCode.EXPIRED_CODE);
|
|
}
|
|
|
|
const isSamePassword = await compare(password, foundToken.user.password || '');
|
|
|
|
if (isSamePassword) {
|
|
throw new AppError(AppErrorCode.SAME_PASSWORD);
|
|
}
|
|
|
|
const hashedPassword = await hash(password, SALT_ROUNDS);
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.user.update({
|
|
where: {
|
|
id: foundToken.userId,
|
|
},
|
|
data: {
|
|
password: hashedPassword,
|
|
},
|
|
});
|
|
|
|
await tx.passwordResetToken.deleteMany({
|
|
where: {
|
|
userId: foundToken.userId,
|
|
},
|
|
});
|
|
|
|
await tx.userSecurityAuditLog.create({
|
|
data: {
|
|
userId: foundToken.userId,
|
|
type: UserSecurityAuditLogType.PASSWORD_RESET,
|
|
userAgent: requestMetadata?.userAgent,
|
|
ipAddress: requestMetadata?.ipAddress,
|
|
},
|
|
});
|
|
});
|
|
|
|
await jobsClient.triggerJob({
|
|
name: 'send.password.reset.success.email',
|
|
payload: {
|
|
userId: foundToken.userId,
|
|
source: 'RESET',
|
|
},
|
|
});
|
|
|
|
return {
|
|
userId: foundToken.userId,
|
|
};
|
|
};
|