Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { SharedJobsModule } from './entities/shared-jobs/shared-jobs.module.js';
import { TableCategoriesModule } from './entities/table-categories/table-categories.module.js';
import { SignInAuditModule } from './entities/user-sign-in-audit/sign-in-audit.module.js';

@Module({
imports: [
Expand Down Expand Up @@ -81,6 +82,7 @@ import { TableCategoriesModule } from './entities/table-categories/table-categor
LoggingModule,
SharedJobsModule,
TableCategoriesModule,
SignInAuditModule,
],
controllers: [AppController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import { IDatabaseContext } from '../database-context.interface.js';
import { TableCategoriesEntity } from '../../entities/table-categories/table-categories.entity.js';
import { ITableCategoriesCustomRepository } from '../../entities/table-categories/repository/table-categories-repository.interface.js';
import { ConnectionPropertiesEntity } from '../../entities/connection-properties/connection-properties.entity.js';
import { SignInAuditEntity } from '../../entities/user-sign-in-audit/sign-in-audit.entity.js';
import { ISignInAuditRepository } from '../../entities/user-sign-in-audit/repository/sign-in-audit-repository.interface.js';

export interface IGlobalDatabaseContext extends IDatabaseContext {
userRepository: Repository<UserEntity> & IUserRepository;
Expand Down Expand Up @@ -83,4 +85,5 @@ export interface IGlobalDatabaseContext extends IDatabaseContext {
tableFiltersRepository: Repository<TableFiltersEntity> & ITableFiltersCustomRepository;
aiResponsesToUserRepository: Repository<AiResponsesToUserEntity> & IAiResponsesToUserRepository;
tableCategoriesRepository: Repository<TableCategoriesEntity> & ITableCategoriesCustomRepository;
signInAuditRepository: Repository<SignInAuditEntity> & ISignInAuditRepository;
}
11 changes: 11 additions & 0 deletions backend/src/common/application/global-database-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ import { aiResponsesToUserRepositoryExtension } from '../../entities/ai/ai-data-
import { TableCategoriesEntity } from '../../entities/table-categories/table-categories.entity.js';
import { ITableCategoriesCustomRepository } from '../../entities/table-categories/repository/table-categories-repository.interface.js';
import { tableCategoriesCustomRepositoryExtension } from '../../entities/table-categories/repository/table-categories-repository.extension.js';
import { SignInAuditEntity } from '../../entities/user-sign-in-audit/sign-in-audit.entity.js';
import { ISignInAuditRepository } from '../../entities/user-sign-in-audit/repository/sign-in-audit-repository.interface.js';
import { signInAuditCustomRepositoryExtension } from '../../entities/user-sign-in-audit/repository/sign-in-audit-custom-repository-extension.js';

@Injectable({ scope: Scope.REQUEST })
export class GlobalDatabaseContext implements IGlobalDatabaseContext {
Expand Down Expand Up @@ -128,6 +131,7 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
private _tableFiltersRepository: Repository<TableFiltersEntity> & ITableFiltersCustomRepository;
private _aiResponsesToUserRepository: Repository<AiResponsesToUserEntity> & IAiResponsesToUserRepository;
private _tableCategoriesRepository: Repository<TableCategoriesEntity> & ITableCategoriesCustomRepository;
private _signInAuditRepository: Repository<SignInAuditEntity> & ISignInAuditRepository;

public constructor(
@Inject(BaseType.DATA_SOURCE)
Expand Down Expand Up @@ -216,6 +220,9 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
this._tableCategoriesRepository = this.appDataSource
.getRepository(TableCategoriesEntity)
.extend(tableCategoriesCustomRepositoryExtension);
this._signInAuditRepository = this.appDataSource
.getRepository(SignInAuditEntity)
.extend(signInAuditCustomRepositoryExtension);
}

public get userRepository(): Repository<UserEntity> & IUserRepository {
Expand Down Expand Up @@ -350,6 +357,10 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
return this._tableCategoriesRepository;
}

public get signInAuditRepository(): Repository<SignInAuditEntity> & ISignInAuditRepository {
return this._signInAuditRepository;
}

public startTransaction(): Promise<void> {
this._queryRunner = this.appDataSource.createQueryRunner();
this._queryRunner.startTransaction();
Expand Down
2 changes: 2 additions & 0 deletions backend/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,6 @@ export enum UseCaseType {

CREATE_UPDATE_TABLE_CATEGORIES = 'CREATE_UPDATE_TABLE_CATEGORIES',
FIND_TABLE_CATEGORIES = 'FIND_TABLE_CATEGORIES',

FIND_SIGN_IN_AUDIT_LOGS = 'FIND_SIGN_IN_AUDIT_LOGS',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SignInMethodEnum } from '../enums/sign-in-method.enum.js';
import { SignInStatusEnum } from '../enums/sign-in-status.enum.js';

export class CreateSignInAuditRecordDs {
email: string;
userId?: string;
status: SignInStatusEnum;
signInMethod: SignInMethodEnum;
ipAddress?: string;
userAgent?: string;
failureReason?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SignInMethodEnum } from '../enums/sign-in-method.enum.js';
import { SignInStatusEnum } from '../enums/sign-in-status.enum.js';

export class FindSignInAuditLogsDs {
userId: string;
companyId: string;
query: {
order?: string;
page?: number;
perPage?: number;
dateFrom?: string;
dateTo?: string;
email?: string;
status?: SignInStatusEnum;
signInMethod?: SignInMethodEnum;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { PaginationDs } from '../../table/application/data-structures/pagination.ds.js';
import { FoundSignInAuditRecordDs } from './found-sign-in-audit-record.ds.js';

export class FoundSignInAuditLogsDs {
@ApiProperty({ isArray: true, type: FoundSignInAuditRecordDs })
logs: Array<FoundSignInAuditRecordDs>;

@ApiProperty({ type: PaginationDs })
pagination: PaginationDs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
import { SignInMethodEnum } from '../enums/sign-in-method.enum.js';
import { SignInStatusEnum } from '../enums/sign-in-status.enum.js';

export class FoundSignInAuditRecordDs {
@ApiProperty()
id: string;

@ApiProperty()
email: string;

@ApiProperty({ enum: SignInStatusEnum })
status: SignInStatusEnum;

@ApiProperty({ enum: SignInMethodEnum })
signInMethod: SignInMethodEnum;

@ApiProperty()
ipAddress: string;

@ApiProperty()
userAgent: string;

@ApiProperty()
failureReason: string;

@ApiProperty()
createdAt: Date;

@ApiProperty()
userId: string;
}
4 changes: 4 additions & 0 deletions backend/src/entities/user-sign-in-audit/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './create-sign-in-audit-record.ds.js';
export * from './found-sign-in-audit-record.ds.js';
export * from './found-sign-in-audit-logs.ds.js';
export * from './find-sign-in-audit-logs.ds.js';
2 changes: 2 additions & 0 deletions backend/src/entities/user-sign-in-audit/enums/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './sign-in-status.enum.js';
export * from './sign-in-method.enum.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum SignInMethodEnum {
EMAIL = 'email',
GOOGLE = 'google',
GITHUB = 'github',
SAML = 'saml',
OTP = 'otp',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum SignInStatusEnum {
SUCCESS = 'success',
FAILED = 'failed',
BLOCKED = 'blocked',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { UserEntity } from '../../user/user.entity.js';
import { CreateSignInAuditRecordDs } from '../dto/create-sign-in-audit-record.ds.js';
import { SignInAuditEntity } from '../sign-in-audit.entity.js';
import {
IFindSignInAuditLogsOptions,
IFoundSignInAuditLogsResult,
ISignInAuditRepository,
} from './sign-in-audit-repository.interface.js';

export const signInAuditCustomRepositoryExtension: ISignInAuditRepository = {
async createSignInAuditRecord(data: CreateSignInAuditRecordDs): Promise<SignInAuditEntity> {
const { email, userId, status, signInMethod, ipAddress, userAgent, failureReason } = data;

const newRecord = new SignInAuditEntity();
newRecord.email = email?.toLowerCase();
newRecord.status = status;
newRecord.signInMethod = signInMethod;
newRecord.ipAddress = ipAddress || null;
newRecord.userAgent = userAgent || null;
newRecord.failureReason = failureReason || null;

if (userId) {
const userRepository = this.manager.getRepository(UserEntity);
const user = await userRepository.findOne({ where: { id: userId } });
if (user) {
newRecord.user = user;
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency: In the service (line 34), newRecord.userId = user.id is set, but in the repository extension (line 26), this assignment is missing. This could lead to the userId column not being properly populated when using the repository method directly. Add newRecord.userId = user.id; after line 26 in the repository extension to maintain consistency.

Suggested change
newRecord.user = user;
newRecord.user = user;
newRecord.userId = user.id;

Copilot uses AI. Check for mistakes.
}
}

return await this.save(newRecord);
},

async findSignInAuditLogs(options: IFindSignInAuditLogsOptions): Promise<IFoundSignInAuditLogsResult> {
const { companyId, order, page, perPage, dateFrom, dateTo, searchedEmail, status, signInMethod, userId } = options;

const qb = this.createQueryBuilder('signInAudit')
.leftJoinAndSelect('signInAudit.user', 'user')
.leftJoin('user.company', 'company')
.where('company.id = :companyId', { companyId });

if (userId) {
qb.andWhere('user.id = :userId', { userId });
}

if (searchedEmail) {
qb.andWhere('signInAudit.email = :email', { email: searchedEmail.toLowerCase() });
}

if (status) {
qb.andWhere('signInAudit.status = :status', { status });
}

if (signInMethod) {
qb.andWhere('signInAudit.signInMethod = :signInMethod', { signInMethod });
}

if (dateFrom && dateTo) {
qb.andWhere('signInAudit.createdAt >= :dateFrom', { dateFrom });
qb.andWhere('signInAudit.createdAt <= :dateTo', { dateTo });
}

qb.orderBy('signInAudit.createdAt', order);

const rowsCount = await qb.getCount();
const lastPage = Math.ceil(rowsCount / perPage);
const offset = (page - 1) * perPage;

qb.limit(perPage);
qb.offset(offset);

const logs = await qb.getMany();

return {
logs,
pagination: {
currentPage: page,
lastPage,
perPage,
total: rowsCount,
},
};
},

async findSignInAuditLogsByUserId(
userId: string,
options: IFindSignInAuditLogsOptions,
): Promise<IFoundSignInAuditLogsResult> {
const { order, page, perPage, dateFrom, dateTo, status, signInMethod } = options;

const qb = this.createQueryBuilder('signInAudit')
.leftJoinAndSelect('signInAudit.user', 'user')
.where('user.id = :userId', { userId });

if (status) {
qb.andWhere('signInAudit.status = :status', { status });
}

if (signInMethod) {
qb.andWhere('signInAudit.signInMethod = :signInMethod', { signInMethod });
}

if (dateFrom && dateTo) {
qb.andWhere('signInAudit.createdAt >= :dateFrom', { dateFrom });
qb.andWhere('signInAudit.createdAt <= :dateTo', { dateTo });
}

qb.orderBy('signInAudit.createdAt', order);

const rowsCount = await qb.getCount();
const lastPage = Math.ceil(rowsCount / perPage);
const offset = (page - 1) * perPage;

qb.limit(perPage);
qb.offset(offset);

const logs = await qb.getMany();

return {
logs,
pagination: {
currentPage: page,
lastPage,
perPage,
total: rowsCount,
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { QueryOrderingEnum } from '../../../enums/query-ordering.enum.js';
import { SignInMethodEnum } from '../enums/sign-in-method.enum.js';
import { SignInStatusEnum } from '../enums/sign-in-status.enum.js';
import { CreateSignInAuditRecordDs } from '../dto/create-sign-in-audit-record.ds.js';
import { SignInAuditEntity } from '../sign-in-audit.entity.js';

export interface ISignInAuditRepository {
createSignInAuditRecord(data: CreateSignInAuditRecordDs): Promise<SignInAuditEntity>;

findSignInAuditLogs(options: IFindSignInAuditLogsOptions): Promise<IFoundSignInAuditLogsResult>;

findSignInAuditLogsByUserId(
userId: string,
options: IFindSignInAuditLogsOptions,
): Promise<IFoundSignInAuditLogsResult>;
}

export interface IFindSignInAuditLogsOptions {
companyId: string;
order: QueryOrderingEnum;
page: number;
perPage: number;
dateFrom?: Date;
dateTo?: Date;
searchedEmail?: string;
status?: SignInStatusEnum;
signInMethod?: SignInMethodEnum;
userId?: string;
}

export interface IFoundSignInAuditLogsResult {
logs: Array<SignInAuditEntity>;
pagination: {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
};
}
Loading
Loading