feat(ee): audit logs (#1977)

feat: clickhouse driver
* sync
* updates
This commit is contained in:
Philip Okugbe
2026-03-01 01:29:03 +00:00
committed by GitHub
parent 85ce0d32bf
commit 69d7532c6c
62 changed files with 2600 additions and 191 deletions
+17 -1
View File
@@ -3,6 +3,7 @@ import {
Controller,
HttpCode,
HttpStatus,
Inject,
Post,
Res,
UseGuards,
@@ -24,6 +25,11 @@ import { VerifyUserTokenDto } from './dto/verify-user-token.dto';
import { FastifyReply } from 'fastify';
import { validateSsoEnforcement } from './auth.util';
import { ModuleRef } from '@nestjs/core';
import { AuditEvent, AuditResource } from '../../common/events/audit-events';
import {
AUDIT_SERVICE,
IAuditService,
} from '../../integrations/audit/audit.service';
@Controller('auth')
export class AuthController {
@@ -33,6 +39,7 @@ export class AuthController {
private authService: AuthService,
private environmentService: EnvironmentService,
private moduleRef: ModuleRef,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
@HttpCode(HttpStatus.OK)
@@ -169,8 +176,17 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('logout')
async logout(@Res({ passthrough: true }) res: FastifyReply) {
async logout(
@AuthUser() user: User,
@Res({ passthrough: true }) res: FastifyReply,
) {
res.clearCookie('authToken');
this.auditService.log({
event: AuditEvent.USER_LOGOUT,
resourceType: AuditResource.USER,
resourceId: user.id,
});
}
setAuthCookie(res: FastifyReply, token: string) {
@@ -1,5 +1,6 @@
import {
BadRequestException,
Inject,
Injectable,
NotFoundException,
UnauthorizedException,
@@ -29,6 +30,11 @@ import { InjectKysely } from 'nestjs-kysely';
import { executeTx } from '@docmost/db/utils';
import { VerifyUserTokenDto } from '../dto/verify-user-token.dto';
import { DomainService } from '../../../integrations/environment/domain.service';
import { AuditEvent, AuditResource } from '../../../common/events/audit-events';
import {
AUDIT_SERVICE,
IAuditService,
} from '../../../integrations/audit/audit.service';
@Injectable()
export class AuthService {
@@ -40,6 +46,7 @@ export class AuthService {
private mailService: MailService,
private domainService: DomainService,
@InjectKysely() private readonly db: KyselyDB,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
async login(loginDto: LoginDto, workspaceId: string) {
@@ -64,6 +71,13 @@ export class AuthService {
user.lastLoginAt = new Date();
await this.userRepo.updateLastLogin(user.id, workspaceId);
this.auditService.log({
event: AuditEvent.USER_LOGIN,
resourceType: AuditResource.USER,
resourceId: user.id,
metadata: { source: 'password' },
});
return this.tokenService.generateAccessToken(user);
}
@@ -112,6 +126,12 @@ export class AuthService {
workspaceId,
);
this.auditService.log({
event: AuditEvent.USER_PASSWORD_CHANGED,
resourceType: AuditResource.USER,
resourceId: userId,
});
const emailTemplate = ChangePasswordEmail({ username: user.name });
await this.mailService.sendToQueue({
to: user.email,
@@ -135,16 +155,27 @@ export class AuthService {
const token = nanoIdGen(16);
const resetLink = `${this.domainService.getUrl(workspace.hostname)}/password-reset?token=${token}`;
await executeTx(this.db, async (trx) => {
await trx
.deleteFrom('userTokens')
.where('userId', '=', user.id)
.where('type', '=', UserTokenType.FORGOT_PASSWORD)
.execute();
await this.userTokenRepo.insertUserToken({
token: token,
userId: user.id,
workspaceId: user.workspaceId,
expiresAt: new Date(new Date().getTime() + 60 * 60 * 1000), // 1 hour
type: UserTokenType.FORGOT_PASSWORD,
await this.userTokenRepo.insertUserToken(
{
token,
userId: user.id,
workspaceId: user.workspaceId,
expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 minutes
type: UserTokenType.FORGOT_PASSWORD,
},
{ trx },
);
});
const resetLink = `${this.domainService.getUrl(workspace.hostname)}/password-reset?token=${token}`;
const emailTemplate = ForgotPasswordEmail({
username: user.name,
resetLink: resetLink,
@@ -201,6 +232,13 @@ export class AuthService {
.execute();
});
this.auditService.setActorId(user.id);
this.auditService.log({
event: AuditEvent.USER_PASSWORD_RESET,
resourceType: AuditResource.USER,
resourceId: user.id,
});
const emailTemplate = ChangePasswordEmail({ username: user.name });
await this.mailService.sendToQueue({
to: user.email,
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { CreateUserDto } from '../dto/create-user.dto';
import { WorkspaceService } from '../../workspace/services/workspace.service';
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
@@ -10,6 +10,11 @@ import { InjectKysely } from 'nestjs-kysely';
import { User, Workspace } from '@docmost/db/types/entity.types';
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
import { UserRole } from '../../../common/helpers/types/permission';
import { AuditEvent, AuditResource } from '../../../common/events/audit-events';
import {
AUDIT_SERVICE,
IAuditService,
} from '../../../integrations/audit/audit.service';
@Injectable()
export class SignupService {
@@ -18,6 +23,7 @@ export class SignupService {
private workspaceService: WorkspaceService,
private groupUserRepo: GroupUserRepo,
@InjectKysely() private readonly db: KyselyDB,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
async signup(
@@ -36,7 +42,7 @@ export class SignupService {
);
}
return await executeTx(
const user = await executeTx(
this.db,
async (trx) => {
// create user
@@ -66,6 +72,24 @@ export class SignupService {
},
trx,
);
this.auditService.log({
event: AuditEvent.USER_CREATED,
resourceType: AuditResource.USER,
resourceId: user.id,
changes: {
after: {
name: user.name,
email: user.email,
role: user.role,
},
},
metadata: {
source: 'signup',
},
});
return user;
}
async initialSetup(