mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add user security audit logs
This commit is contained in:
@ -1,21 +1,25 @@
|
||||
import { compare } from 'bcrypt';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { ErrorCode } from '../../next-auth/error-codes';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { validateTwoFactorAuthentication } from './validate-2fa';
|
||||
|
||||
type DisableTwoFactorAuthenticationOptions = {
|
||||
user: User;
|
||||
backupCode: string;
|
||||
password: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const disableTwoFactorAuthentication = async ({
|
||||
backupCode,
|
||||
user,
|
||||
password,
|
||||
requestMetadata,
|
||||
}: DisableTwoFactorAuthenticationOptions) => {
|
||||
if (!user.password) {
|
||||
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
|
||||
@ -33,15 +37,26 @@ export const disableTwoFactorAuthentication = async ({
|
||||
throw new Error(ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE);
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: false,
|
||||
twoFactorBackupCodes: null,
|
||||
twoFactorSecret: null,
|
||||
},
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: false,
|
||||
twoFactorBackupCodes: null,
|
||||
twoFactorSecret: null,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: UserSecurityAuditLogType.AUTH_2FA_DISABLE,
|
||||
userAgent: requestMetadata?.userAgent,
|
||||
ipAddress: requestMetadata?.ipAddress,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { User } from '@documenso/prisma/client';
|
||||
import { type User, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getBackupCodes } from './get-backup-code';
|
||||
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
||||
|
||||
type EnableTwoFactorAuthenticationOptions = {
|
||||
user: User;
|
||||
code: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const enableTwoFactorAuthentication = async ({
|
||||
user,
|
||||
code,
|
||||
requestMetadata,
|
||||
}: EnableTwoFactorAuthenticationOptions) => {
|
||||
if (user.identityProvider !== 'DOCUMENSO') {
|
||||
throw new Error(ErrorCode.INCORRECT_IDENTITY_PROVIDER);
|
||||
@ -32,13 +35,24 @@ export const enableTwoFactorAuthentication = async ({
|
||||
throw new Error(ErrorCode.INCORRECT_TWO_FACTOR_CODE);
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: true,
|
||||
},
|
||||
const updatedUser = await prisma.$transaction(async (tx) => {
|
||||
await tx.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: UserSecurityAuditLogType.AUTH_2FA_ENABLE,
|
||||
userAgent: requestMetadata?.userAgent,
|
||||
ipAddress: requestMetadata?.ipAddress,
|
||||
},
|
||||
});
|
||||
|
||||
return await tx.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const recoveryCodes = getBackupCodes({ user: updatedUser });
|
||||
|
||||
@ -5,7 +5,7 @@ import { createTOTPKeyURI } from 'oslo/otp';
|
||||
|
||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { User } from '@documenso/prisma/client';
|
||||
import { type User } from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
import { symmetricEncrypt } from '../../universal/crypto';
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { UserSecurityAuditLog, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
export type FindUserSecurityAuditLogsOptions = {
|
||||
userId: number;
|
||||
type?: UserSecurityAuditLogType;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Omit<UserSecurityAuditLog, 'id' | 'userId'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
};
|
||||
|
||||
export const findUserSecurityAuditLogs = async ({
|
||||
userId,
|
||||
type,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
}: FindUserSecurityAuditLogsOptions) => {
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const whereClause = {
|
||||
userId,
|
||||
type,
|
||||
};
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.userSecurityAuditLog.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
}),
|
||||
prisma.userSecurityAuditLog.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
};
|
||||
@ -1,16 +1,19 @@
|
||||
import { compare, hash } from 'bcrypt';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { SALT_ROUNDS } from '../../constants/auth';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { sendResetPassword } from '../auth/send-reset-password';
|
||||
|
||||
export type ResetPasswordOptions = {
|
||||
token: string;
|
||||
password: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const resetPassword = async ({ token, password }: ResetPasswordOptions) => {
|
||||
export const resetPassword = async ({ token, password, requestMetadata }: ResetPasswordOptions) => {
|
||||
if (!token) {
|
||||
throw new Error('Invalid token provided. Please try again.');
|
||||
}
|
||||
@ -56,6 +59,14 @@ export const resetPassword = async ({ token, password }: ResetPasswordOptions) =
|
||||
userId: foundToken.userId,
|
||||
},
|
||||
}),
|
||||
prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: foundToken.userId,
|
||||
type: UserSecurityAuditLogType.PASSWORD_RESET,
|
||||
userAgent: requestMetadata?.userAgent,
|
||||
ipAddress: requestMetadata?.ipAddress,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
await sendResetPassword({ userId: foundToken.userId });
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
import { compare, hash } from 'bcrypt';
|
||||
|
||||
import { SALT_ROUNDS } from '@documenso/lib/constants/auth';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { SALT_ROUNDS } from '../../constants/auth';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdatePasswordOptions = {
|
||||
userId: number;
|
||||
password: string;
|
||||
currentPassword: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updatePassword = async ({
|
||||
userId,
|
||||
password,
|
||||
currentPassword,
|
||||
requestMetadata,
|
||||
}: UpdatePasswordOptions) => {
|
||||
// Existence check
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@ -39,14 +42,23 @@ export const updatePassword = async ({
|
||||
|
||||
const hashedNewPassword = await hash(password, SALT_ROUNDS);
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
password: hashedNewPassword,
|
||||
},
|
||||
});
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
type: UserSecurityAuditLogType.PASSWORD_UPDATE,
|
||||
userAgent: requestMetadata?.userAgent,
|
||||
ipAddress: requestMetadata?.ipAddress,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
return await tx.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
password: hashedNewPassword,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,12 +1,21 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
|
||||
export type UpdateProfileOptions = {
|
||||
userId: number;
|
||||
name: string;
|
||||
signature: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updateProfile = async ({ userId, name, signature }: UpdateProfileOptions) => {
|
||||
export const updateProfile = async ({
|
||||
userId,
|
||||
name,
|
||||
signature,
|
||||
requestMetadata,
|
||||
}: UpdateProfileOptions) => {
|
||||
// Existence check
|
||||
await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@ -14,15 +23,24 @@ export const updateProfile = async ({ userId, name, signature }: UpdateProfileOp
|
||||
},
|
||||
});
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
signature,
|
||||
},
|
||||
});
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
type: UserSecurityAuditLogType.ACCOUNT_PROFILE_UPDATE,
|
||||
userAgent: requestMetadata?.userAgent,
|
||||
ipAddress: requestMetadata?.ipAddress,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
return await tx.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
signature,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user