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
80 changes: 80 additions & 0 deletions backend/src/admin-verifications/admin-verifications.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between, Like } from 'typeorm';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/guards/roles.decorator';
import { VerificationRecord } from '../verification/entities/verification-record.entity';
import { IsOptional, IsString, IsDateString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';

class QueryVerificationsDto {
@IsOptional()
@IsString()
status?: string;

@IsOptional()
@IsString()
documentId?: string;

@IsOptional()
@IsString()
stellarTxHash?: string;

@IsOptional()
@IsDateString()
startDate?: string;

@IsOptional()
@IsDateString()
endDate?: string;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 20;
}

@Controller('admin/verifications')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
export class AdminVerificationsController {
constructor(
@InjectRepository(VerificationRecord)
private readonly verificationRepo: Repository<VerificationRecord>,
) {}

@Get()
async findAll(@Query() query: QueryVerificationsDto) {
const { status, documentId, stellarTxHash, startDate, endDate, page = 1, limit = 20 } = query;
const where: Record<string, unknown> = {};

if (status) where.status = status;
if (documentId) where.documentId = documentId;
if (stellarTxHash) where.stellarTxHash = Like(`%${stellarTxHash}%`);
if (startDate && endDate) {
where.createdAt = Between(new Date(startDate), new Date(endDate));
} else if (startDate) {
where.createdAt = Between(new Date(startDate), new Date('2100-01-01'));
} else if (endDate) {
where.createdAt = Between(new Date('1970-01-01'), new Date(endDate));
}

const [data, total] = await this.verificationRepo.findAndCount({
where,
take: limit,
skip: (page - 1) * limit,
order: { createdAt: 'DESC' },
relations: ['document'],
});

return { data, total, page, limit };
}
}
10 changes: 10 additions & 0 deletions backend/src/admin-verifications/admin-verifications.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { VerificationRecord } from '../verification/entities/verification-record.entity';
import { AdminVerificationsController } from './admin-verifications.controller';

@Module({
imports: [TypeOrmModule.forFeature([VerificationRecord])],
controllers: [AdminVerificationsController],
})
export class AdminVerificationsModule {}
6 changes: 6 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WinstonModule } from 'nest-winston';

import { AdminVerificationsModule } from './admin-verifications/admin-verifications.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuditModule } from './audit/audit.module';
import { AuthModule } from './auth/auth.module';
import { HealthModule } from './health/health.module';
import { buildWinstonOptions } from './common/logger.config';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { DocumentsModule } from './documents/documents.module';
Expand Down Expand Up @@ -48,6 +51,9 @@ import { ConfigValidationSchema } from './config/config.validation';
synchronize: true,
}),
}),
AuditModule,
AdminVerificationsModule,
HealthModule,
UsersModule,
AuthModule,
DocumentsModule,
Expand Down
52 changes: 52 additions & 0 deletions backend/src/audit/audit-log.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';

export enum AuditAction {
CREATE = 'CREATE',
UPDATE = 'UPDATE',
DELETE = 'DELETE',
VERIFY = 'VERIFY',
EXPORT = 'EXPORT',
SHARE = 'SHARE',
REVOKE = 'REVOKE',
LOGIN = 'LOGIN',
LOGOUT = 'LOGOUT',
APPROVE = 'APPROVE',
REJECT = 'REJECT',
}

@Entity('audit_logs')
@Index('IDX_AUDIT_USER_ID', ['userId'])
@Index('IDX_AUDIT_ENTITY', ['entity'])
@Index('IDX_AUDIT_ACTION', ['action'])
@Index('IDX_AUDIT_CREATED_AT', ['createdAt'])
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ name: 'user_id' })
userId: string;

@Column({ type: 'enum', enum: AuditAction })
action: AuditAction;

@Column()
entity: string;

@Column({ name: 'entity_id' })
entityId: string;

@Column({ name: 'metadata', type: 'json', nullable: true })
metadata?: Record<string, unknown>;

@Column({ name: 'ip_address', nullable: true })
ipAddress?: string;

@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
55 changes: 55 additions & 0 deletions backend/src/audit/audit-log.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between, Like } from 'typeorm';
import { AuditLog, AuditAction } from './audit-log.entity';
import { CreateAuditLogDto } from './dto/create-audit-log.dto';
import { QueryAuditLogDto } from './dto/query-audit-log.dto';

@Injectable()
export class AuditService {
constructor(
@InjectRepository(AuditLog)
private readonly auditLogRepo: Repository<AuditLog>,
) {}

async create(dto: CreateAuditLogDto): Promise<AuditLog> {
const log = this.auditLogRepo.create({
userId: dto.userId,
action: dto.action as AuditAction,
entity: dto.entity,
entityId: dto.entityId,
metadata: dto.metadata,
ipAddress: dto.ipAddress,
});
return this.auditLogRepo.save(log);
}

async findAll(query: QueryAuditLogDto) {
const { userId, action, entity, startDate, endDate, page = 1, limit = 20 } = query;
const where: Record<string, unknown> = {};

if (userId) where.userId = userId;
if (action) where.action = action;
if (entity) where.entity = Like(`%${entity}%`);
if (startDate && endDate) {
where.createdAt = Between(new Date(startDate), new Date(endDate));
} else if (startDate) {
where.createdAt = Between(new Date(startDate), new Date('2100-01-01'));
} else if (endDate) {
where.createdAt = Between(new Date('1970-01-01'), new Date(endDate));
}

const [data, total] = await this.auditLogRepo.findAndCount({
where,
take: limit,
skip: (page - 1) * limit,
order: { createdAt: 'DESC' },
});

return { data, total, page, limit };
}

async findOne(id: string): Promise<AuditLog | null> {
return this.auditLogRepo.findOne({ where: { id } });
}
}
23 changes: 23 additions & 0 deletions backend/src/audit/audit.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Controller, Get, Query, Param, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/guards/roles.decorator';
import { AuditService } from './audit-log.service';
import { QueryAuditLogDto } from './dto/query-audit-log.dto';

@Controller('audit')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
export class AuditController {
constructor(private readonly auditService: AuditService) {}

@Get()
findAll(@Query() query: QueryAuditLogDto) {
return this.auditService.findAll(query);
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.auditService.findOne(id);
}
}
13 changes: 13 additions & 0 deletions backend/src/audit/audit.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditLog } from './audit-log.entity';
import { AuditService } from './audit-log.service';
import { AuditController } from './audit.controller';

@Module({
imports: [TypeOrmModule.forFeature([AuditLog])],
controllers: [AuditController],
providers: [AuditService],
exports: [AuditService],
})
export class AuditModule {}
24 changes: 24 additions & 0 deletions backend/src/audit/dto/create-audit-log.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IsString, IsOptional, IsObject } from 'class-validator';
import { AuditAction } from '../audit-log.entity';

export class CreateAuditLogDto {
@IsString()
userId: string;

@IsString()
action: AuditAction;

@IsString()
entity: string;

@IsString()
entityId: string;

@IsOptional()
@IsObject()
metadata?: Record<string, unknown>;

@IsOptional()
@IsString()
ipAddress?: string;
}
36 changes: 36 additions & 0 deletions backend/src/audit/dto/query-audit-log.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IsOptional, IsString, IsDateString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class QueryAuditLogDto {
@IsOptional()
@IsString()
userId?: string;

@IsOptional()
@IsString()
action?: string;

@IsOptional()
@IsString()
entity?: string;

@IsOptional()
@IsDateString()
startDate?: string;

@IsOptional()
@IsDateString()
endDate?: string;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 20;
}
39 changes: 39 additions & 0 deletions backend/src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
TypeOrmHealthIndicator,
HealthCheck,
DiskHealthIndicator,
} from '@nestjs/terminus';
import { ConfigService } from '@nestjs/config';
import { RedisHealthIndicator } from './redis.health';

@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
private readonly disk: DiskHealthIndicator,
private readonly config: ConfigService,
private readonly redisHealth: RedisHealthIndicator,
) {}

@Get()
@HealthCheck()
check() {
const checks = [
() => this.db.pingCheck('database', { timeout: 5000 }),
() =>
this.disk.checkStorage('disk', {
path: '/',
thresholdPercent: 0.9,
}),
];

if (this.config.get<string>('REDIS_HOST')) {
checks.push(() => this.redisHealth.isHealthy('redis'));
}

return this.health.check(checks);
}
}
12 changes: 12 additions & 0 deletions backend/src/health/health.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { ConfigModule } from '@nestjs/config';
import { HealthController } from './health.controller';
import { RedisHealthIndicator } from './redis.health';

@Module({
imports: [TerminusModule, ConfigModule],
controllers: [HealthController],
providers: [RedisHealthIndicator],
})
export class HealthModule {}
Loading
Loading