diff --git a/backend/package-lock.json b/backend/package-lock.json index 125fda8..9ba55a7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2334,7 +2334,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.19.tgz", "integrity": "sha512-qeiTt2tv+e5QyDKqG8HlVZb2wx64FEaSGFJouqTSRs+kG44iTfl3xlz1XqVped+rihx4hmjWgL5gkhtdK3E6+Q==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -2382,7 +2381,6 @@ "integrity": "sha512-6nJkWa2efrYi+XlU686J9y5L7OvxpLVjT0T/sxRKE7Jvpffiihelup4WSvLvRhdHDjj/5SuoWEwqReXAaaeHmw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2466,7 +2464,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.19.tgz", "integrity": "sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -2488,7 +2485,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.19.tgz", "integrity": "sha512-gu1nPIEaP5Qjjg/Cl8wXyvwGpdZGzgbtK4KcH65YRAA+GTKUkIHb4BNpLJ27Ymq/wqLJKNEbCjajfzD0BEjMGA==", "license": "MIT", - "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -2686,7 +2682,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -2700,7 +2695,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.19.tgz", "integrity": "sha512-2qo8jtIwwwgkqAI1BtnJ02EaFLrRkKA39eYXS8IhZCHilhBHCWdjnJ5cLcFq4oF+s+KZ7LcLGD/3stxJy8ijzg==", "license": "MIT", - "peer": true, "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2854,7 +2848,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -3204,7 +3197,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3359,7 +3351,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3610,7 +3601,6 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -4010,7 +4000,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4060,7 +4049,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4841,7 +4829,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5042,7 +5029,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -5308,15 +5294,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.4", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -6369,7 +6353,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6426,7 +6409,6 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7425,6 +7407,7 @@ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" }, @@ -8121,7 +8104,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9655,6 +9637,7 @@ "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", "license": "MIT", + "peer": true, "optionalDependencies": { "msgpackr-extract": "^3.0.2" } @@ -10109,7 +10092,6 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10307,7 +10289,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -10573,7 +10554,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10839,7 +10819,6 @@ "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", "license": "MIT", - "peer": true, "dependencies": { "@redis/bloom": "5.12.1", "@redis/client": "5.12.1", @@ -11204,7 +11183,6 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -12390,7 +12368,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12556,7 +12533,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -12782,7 +12758,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13161,6 +13136,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -13179,6 +13155,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -13193,6 +13170,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -13203,6 +13181,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -13270,7 +13249,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", diff --git a/backend/src/access-logs/access-logs.service.ts b/backend/src/access-logs/access-logs.service.ts index cbf1a7b..0c1bdf3 100644 --- a/backend/src/access-logs/access-logs.service.ts +++ b/backend/src/access-logs/access-logs.service.ts @@ -1,70 +1,77 @@ -import { Injectable, Logger } from "@nestjs/common" -import { type Repository, Between, type FindOptionsWhere } from "typeorm" -import type { AccessLog } from "./entities/access-log.entity" -import type { CreateAccessLogDto } from "./dto/create-access-log.dto" -import type { FilterAccessLogsDto } from "./dto/filter-access-logs.dto" +import { Injectable, Logger } from '@nestjs/common'; +import { type Repository, Between, type FindOptionsWhere } from 'typeorm'; +import type { AccessLog } from './entities/access-log.entity'; +import type { CreateAccessLogDto } from './dto/create-access-log.dto'; +import type { FilterAccessLogsDto } from './dto/filter-access-logs.dto'; export interface PaginatedAccessLogs { - data: AccessLog[] - total: number - page: number - limit: number - totalPages: number + data: AccessLog[]; + total: number; + page: number; + limit: number; + totalPages: number; } @Injectable() export class AccessLogsService { - private readonly logger = new Logger(AccessLogsService.name) + private readonly logger = new Logger(AccessLogsService.name); constructor(private readonly accessLogRepository: Repository) {} async create(createAccessLogDto: CreateAccessLogDto): Promise { try { - const accessLog = this.accessLogRepository.create(createAccessLogDto) - return await this.accessLogRepository.save(accessLog) + const accessLog = this.accessLogRepository.create(createAccessLogDto); + return await this.accessLogRepository.save(accessLog); } catch (error) { - this.logger.error("Failed to create access log", error.stack) - throw error + this.logger.error('Failed to create access log', error.stack); + throw error; } } async findAll(filterDto: FilterAccessLogsDto): Promise { - const { page = 1, limit = 50, sortByDateDesc = true, ...filters } = filterDto + const { + page = 1, + limit = 50, + sortByDateDesc = true, + ...filters + } = filterDto; - const where: FindOptionsWhere = {} + const where: FindOptionsWhere = {}; // Apply filters if (filters.userId) { - where.userId = filters.userId + where.userId = filters.userId; } if (filters.routePath) { - where.routePath = filters.routePath + where.routePath = filters.routePath; } if (filters.httpMethod) { - where.httpMethod = filters.httpMethod + where.httpMethod = filters.httpMethod; } if (filters.ipAddress) { - where.ipAddress = filters.ipAddress + where.ipAddress = filters.ipAddress; } // Date range filter if (filters.startDate || filters.endDate) { - const startDate = filters.startDate ? new Date(filters.startDate) : new Date("1970-01-01") - const endDate = filters.endDate ? new Date(filters.endDate) : new Date() - where.createdAt = Between(startDate, endDate) + const startDate = filters.startDate + ? new Date(filters.startDate) + : new Date('1970-01-01'); + const endDate = filters.endDate ? new Date(filters.endDate) : new Date(); + where.createdAt = Between(startDate, endDate); } const [data, total] = await this.accessLogRepository.findAndCount({ where, order: { - createdAt: sortByDateDesc ? "DESC" : "ASC", + createdAt: sortByDateDesc ? 'DESC' : 'ASC', }, skip: (page - 1) * limit, take: limit, - }) + }); return { data, @@ -72,15 +79,15 @@ export class AccessLogsService { page, limit, totalPages: Math.ceil(total / limit), - } + }; } async findByUser(userId: string, limit = 100): Promise { return this.accessLogRepository.find({ where: { userId }, - order: { createdAt: "DESC" }, + order: { createdAt: 'DESC' }, take: limit, - }) + }); } async findByTimeRange(startDate: Date, endDate: Date): Promise { @@ -88,32 +95,34 @@ export class AccessLogsService { where: { createdAt: Between(startDate, endDate), }, - order: { createdAt: "DESC" }, - }) + order: { createdAt: 'DESC' }, + }); } async getAccessLogStats(userId?: string): Promise<{ - totalRequests: number - uniqueIPs: number - topRoutes: { routePath: string; count: number }[] + totalRequests: number; + uniqueIPs: number; + topRoutes: { routePath: string; count: number }[]; }> { - const queryBuilder = this.accessLogRepository.createQueryBuilder("log") + const queryBuilder = this.accessLogRepository.createQueryBuilder('log'); if (userId) { - queryBuilder.where("log.userId = :userId", { userId }) + queryBuilder.where('log.userId = :userId', { userId }); } - const totalRequests = await queryBuilder.getCount() + const totalRequests = await queryBuilder.getCount(); - const uniqueIPsResult = await queryBuilder.select("COUNT(DISTINCT log.ipAddress)", "count").getRawOne() + const uniqueIPsResult = await queryBuilder + .select('COUNT(DISTINCT log.ipAddress)', 'count') + .getRawOne(); const topRoutesResult = await queryBuilder - .select("log.routePath", "routePath") - .addSelect("COUNT(*)", "count") - .groupBy("log.routePath") - .orderBy("count", "DESC") + .select('log.routePath', 'routePath') + .addSelect('COUNT(*)', 'count') + .groupBy('log.routePath') + .orderBy('count', 'DESC') .limit(10) - .getRawMany() + .getRawMany(); return { totalRequests, @@ -122,12 +131,12 @@ export class AccessLogsService { routePath: route.routePath, count: Number.parseInt(route.count), })), - } + }; } async deleteOldLogs(olderThan: Date): Promise { await this.accessLogRepository.delete({ - createdAt: Between(new Date("1970-01-01"), olderThan), - }) - } + createdAt: Between(new Date('1970-01-01'), olderThan), + }); + } } diff --git a/backend/src/access-logs/entities/access-log.entity.ts b/backend/src/access-logs/entities/access-log.entity.ts index 2bdee2e..6702abe 100644 --- a/backend/src/access-logs/entities/access-log.entity.ts +++ b/backend/src/access-logs/entities/access-log.entity.ts @@ -1,4 +1,9 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, +} from 'typeorm'; @Entity('access_logs') export class AccessLog { diff --git a/backend/src/activity-tracker/activity-tracker.service.ts b/backend/src/activity-tracker/activity-tracker.service.ts index 91db870..6f16332 100644 --- a/backend/src/activity-tracker/activity-tracker.service.ts +++ b/backend/src/activity-tracker/activity-tracker.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from "@nestjs/common" -import type { Repository } from "typeorm" -import type { Activity } from "./entities/activity.entity" -import type { CreateActivityDto } from "./dto/create-activity.dto" -import type { FilterActivityDto } from "./dto/filter-activity.dto" +import { Injectable } from '@nestjs/common'; +import type { Repository } from 'typeorm'; +import type { Activity } from './entities/activity.entity'; +import type { CreateActivityDto } from './dto/create-activity.dto'; +import type { FilterActivityDto } from './dto/filter-activity.dto'; @Injectable() export class ActivityTrackerService { @@ -14,8 +14,8 @@ export class ActivityTrackerService { * @returns The created activity entity. */ async logActivity(createActivityDto: CreateActivityDto): Promise { - const activity = this.activityRepository.create(createActivityDto) - return this.activityRepository.save(activity) + const activity = this.activityRepository.create(createActivityDto); + return this.activityRepository.save(activity); } /** @@ -23,7 +23,9 @@ export class ActivityTrackerService { * @param filterDto The DTO containing filter, pagination, and sort parameters. * @returns An object containing activity entries and total count. */ - async findActivities(filterDto: FilterActivityDto): Promise<{ data: Activity[]; total: number }> { + async findActivities( + filterDto: FilterActivityDto, + ): Promise<{ data: Activity[]; total: number }> { const { userId, actionType, @@ -31,34 +33,40 @@ export class ActivityTrackerService { endDate, page = 1, limit = 10, - sortBy = "timestamp", - sortOrder = "DESC", - } = filterDto + sortBy = 'timestamp', + sortOrder = 'DESC', + } = filterDto; - const queryBuilder = this.activityRepository.createQueryBuilder("activity") + const queryBuilder = this.activityRepository.createQueryBuilder('activity'); // Apply filters if (userId) { - queryBuilder.andWhere("activity.userId = :userId", { userId }) + queryBuilder.andWhere('activity.userId = :userId', { userId }); } if (actionType) { - queryBuilder.andWhere("activity.actionType = :actionType", { actionType }) + queryBuilder.andWhere('activity.actionType = :actionType', { + actionType, + }); } if (startDate) { - queryBuilder.andWhere("activity.timestamp >= :startDate", { startDate: new Date(startDate) }) + queryBuilder.andWhere('activity.timestamp >= :startDate', { + startDate: new Date(startDate), + }); } if (endDate) { - queryBuilder.andWhere("activity.timestamp <= :endDate", { endDate: new Date(endDate) }) + queryBuilder.andWhere('activity.timestamp <= :endDate', { + endDate: new Date(endDate), + }); } // Order by - queryBuilder.orderBy(`activity.${sortBy}`, sortOrder) + queryBuilder.orderBy(`activity.${sortBy}`, sortOrder); // Pagination - queryBuilder.skip((page - 1) * limit).take(limit) + queryBuilder.skip((page - 1) * limit).take(limit); - const [data, total] = await queryBuilder.getManyAndCount() - return { data, total } + const [data, total] = await queryBuilder.getManyAndCount(); + return { data, total }; } /** @@ -69,14 +77,14 @@ export class ActivityTrackerService { async findActivitiesByUserId(userId: string): Promise { return this.activityRepository.find({ where: { userId }, - order: { timestamp: "DESC" }, - }) + order: { timestamp: 'DESC' }, + }); } async findActivitiesByActionType(actionType: string): Promise { return this.activityRepository.find({ where: { actionType }, - order: { timestamp: "DESC" }, - }) + order: { timestamp: 'DESC' }, + }); } } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 49cbfd6..1e0ade7 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,6 +9,7 @@ import { AuthModule } from './auth/auth.module'; import { buildWinstonOptions } from './common/logger.config'; import { LoggerMiddleware } from './common/middleware/logger.middleware'; import { DocumentsModule } from './documents/documents.module'; +import { ExternalValidationModule } from './external-validation/external-validation.module'; import { MailModule } from './mail/mail.module'; import { QueueModule } from './queue/queue.module'; import { RiskAssessmentModule } from './risk-assessment/risk-assessment.module'; @@ -51,6 +52,7 @@ import { ConfigValidationSchema } from './config/config.validation'; UsersModule, AuthModule, DocumentsModule, + ExternalValidationModule, RiskAssessmentModule, StellarModule, VerificationModule, diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 4bfe553..7754af3 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -7,6 +7,7 @@ Res, UseGuards, BadRequestException, + Headers, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AuthGuard } from '@nestjs/passport'; @@ -18,6 +19,7 @@ import { AuthService } from './auth.service'; import { RegisterAuthDto } from './dto/register-auth.dto'; import { LoginAuthDto } from './dto/login-auth.dto'; import { RefreshAuthDto } from './dto/refresh-auth.dto'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; @Controller('auth') export class AuthController { @@ -41,6 +43,21 @@ export class AuthController { return this.authService.refreshToken(dto); } + @Post('logout') + @UseGuards(JwtAuthGuard) + async logout( + @Req() req: Request & { user?: any }, + @Headers('authorization') authorization: string, + @Body() body?: { refreshToken?: string }, + ) { + // Extract access token from Authorization header + const accessToken = authorization?.startsWith('Bearer ') + ? authorization.slice(7) + : null; + + return this.authService.logout(accessToken, body?.refreshToken); + } + @Get('google') @UseGuards(AuthGuard('google')) googleAuth() { diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 033d5b9..800a095 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -9,6 +9,7 @@ import { UsersModule } from '../users/users.module'; import { JwtStrategy } from './strategies/jwt.strategy'; import { GoogleStrategy } from './strategies/google.strategy'; import { GithubStrategy } from './strategies/github.strategy'; +import { RedisService } from './redis.service'; @Module({ imports: [ @@ -19,12 +20,21 @@ import { GithubStrategy } from './strategies/github.strategy'; inject: [ConfigService], useFactory: (config: ConfigService) => ({ secret: config.get('JWT_SECRET'), - signOptions: { expiresIn: (config.get('JWT_EXPIRATION') || '1h') as unknown as number }, + signOptions: { + expiresIn: (config.get('JWT_EXPIRATION') || + '1h') as unknown as number, + }, }), }), ], controllers: [AuthController], - providers: [AuthService, JwtStrategy, GoogleStrategy, GithubStrategy], + providers: [ + AuthService, + JwtStrategy, + GoogleStrategy, + GithubStrategy, + RedisService, + ], exports: [AuthService], }) export class AuthModule {} diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 50f5de3..95c23a2 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -7,6 +7,7 @@ import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; +import * as crypto from 'crypto'; import { UsersService } from '../users/users.service'; import { RegisterAuthDto } from './dto/register-auth.dto'; @@ -14,6 +15,7 @@ import { LoginAuthDto } from './dto/login-auth.dto'; import { RefreshAuthDto } from './dto/refresh-auth.dto'; import { JwtPayload } from './interfaces/jwt-payload.interface'; import { User, UserRole } from '../users/entities/user.entity'; +import { RedisService } from './redis.service'; @Injectable() export class AuthService { @@ -21,6 +23,7 @@ export class AuthService { private readonly usersService: UsersService, private readonly jwtService: JwtService, private readonly configService: ConfigService, + private readonly redisService: RedisService, ) {} async register(dto: RegisterAuthDto) { @@ -51,7 +54,9 @@ export class AuthService { async handleOAuthLogin(email: string, fullName?: string) { if (!email) { - throw new BadRequestException('Email is required from the OAuth provider'); + throw new BadRequestException( + 'Email is required from the OAuth provider', + ); } let user = await this.usersService.findByEmail(email); @@ -75,10 +80,20 @@ export class AuthService { throw new BadRequestException('Refresh token is required'); } + // Check if the refresh token has been revoked + const tokenHash = this.hashToken(refreshToken); + const isRevoked = await this.redisService.isTokenRevoked(tokenHash); + if (isRevoked) { + throw new UnauthorizedException('Token has been revoked'); + } + try { - const payload = await this.jwtService.verifyAsync(refreshToken, { - secret: this.getRefreshSecret(), - }); + const payload = await this.jwtService.verifyAsync( + refreshToken, + { + secret: this.getRefreshSecret(), + }, + ); const user = await this.usersService.findById(payload.sub); if (!user) { @@ -92,6 +107,33 @@ export class AuthService { } } + async logout( + accessToken: string, + refreshToken?: string, + ): Promise<{ message: string }> { + if (!accessToken) { + throw new BadRequestException('Access token is required'); + } + + // Add access token to blocklist + const accessTtl = this.redisService.getTokenRemainingTtl(accessToken); + const accessHash = this.hashToken(accessToken); + await this.redisService.addToBlocklist(accessHash, accessTtl); + + // If refresh token is provided, add it to blocklist as well + if (refreshToken) { + const refreshTtl = this.redisService.getTokenRemainingTtl(refreshToken); + const refreshHash = this.hashToken(refreshToken); + await this.redisService.addToBlocklist(refreshHash, refreshTtl); + } + + return { message: 'Successfully logged out' }; + } + + private hashToken(token: string): string { + return crypto.createHash('sha256').update(token).digest('hex'); + } + private async validateCredentials(email: string, password: string) { const user = await this.usersService.findByEmail(email); if (!user) { @@ -135,8 +177,10 @@ export class AuthService { } private getRefreshSecret() { - return this.configService.get('JWT_REFRESH_SECRET') ?? - this.configService.get('JWT_SECRET'); + return ( + this.configService.get('JWT_REFRESH_SECRET') ?? + this.configService.get('JWT_SECRET') + ); } private getRefreshExpiration() { diff --git a/backend/src/auth/guards/roles.guard.ts b/backend/src/auth/guards/roles.guard.ts index bf8d508..db37e4d 100644 --- a/backend/src/auth/guards/roles.guard.ts +++ b/backend/src/auth/guards/roles.guard.ts @@ -1,4 +1,9 @@ -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; diff --git a/backend/src/auth/redis.service.ts b/backend/src/auth/redis.service.ts new file mode 100644 index 0000000..2265cb5 --- /dev/null +++ b/backend/src/auth/redis.service.ts @@ -0,0 +1,101 @@ +import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisService implements OnModuleDestroy { + private readonly logger = new Logger(RedisService.name); + private readonly client: Redis; + + // Key prefix for the token blocklist + private readonly BLOCKLIST_PREFIX = 'auth:blocklist:'; + + constructor(private readonly configService: ConfigService) { + this.client = new Redis({ + host: this.configService.get('REDIS_HOST') || 'localhost', + port: this.configService.get('REDIS_PORT') || 6379, + password: this.configService.get('REDIS_PASSWORD') || undefined, + retryStrategy: (times) => { + if (times > 3) { + return null; // Stop retrying after 3 attempts + } + return Math.min(times * 1000, 3000); // Exponential backoff + }, + }); + + this.client.on('error', (err) => { + this.logger.error(`Redis client error: ${err.message}`); + }); + + this.client.on('connect', () => { + this.logger.log('Redis client connected'); + }); + } + + /** + * Add a token to the blocklist with a TTL (in seconds) + * @param tokenIdentifier - Unique identifier for the token (e.g., JTI or hash) + * @param ttlSeconds - Time-to-live in seconds + */ + async addToBlocklist( + tokenIdentifier: string, + ttlSeconds: number, + ): Promise { + const key = `${this.BLOCKLIST_PREFIX}${tokenIdentifier}`; + await this.client.set(key, '1', 'EX', ttlSeconds); + this.logger.debug( + `Token added to blocklist: ${tokenIdentifier} (TTL: ${ttlSeconds}s)`, + ); + } + + /** + * Check if a token is in the blocklist + * @param tokenIdentifier - Unique identifier for the token + * @returns true if the token is revoked, false otherwise + */ + async isTokenRevoked(tokenIdentifier: string): Promise { + const key = `${this.BLOCKLIST_PREFIX}${tokenIdentifier}`; + const result = await this.client.get(key); + return result !== null; + } + + /** + * Calculate the remaining TTL for a JWT token + * @param token - The JWT token + * @returns TTL in seconds based on token expiration + */ + getTokenRemainingTtl(token: string): number { + try { + // Decode JWT payload (second part) to get expiration time + const payload = JSON.parse( + Buffer.from(token.split('.')[1], 'base64').toString('utf-8'), + ); + + if (!payload.exp) { + // Default to 15 minutes if no expiration found + return 900; + } + + const now = Math.floor(Date.now() / 1000); + const remaining = payload.exp - now; + + // Return at least 1 second, or 0 if already expired + return Math.max(0, remaining); + } catch (error) { + this.logger.error('Failed to decode JWT for TTL calculation', error); + return 900; // Default to 15 minutes + } + } + + async onModuleDestroy(): Promise { + await this.client.quit(); + this.logger.log('Redis client disconnected'); + } + + /** + * Get the Redis client for advanced operations + */ + getClient(): Redis { + return this.client; + } +} diff --git a/backend/src/auth/strategies/github.strategy.ts b/backend/src/auth/strategies/github.strategy.ts index 98304c5..3ee665b 100644 --- a/backend/src/auth/strategies/github.strategy.ts +++ b/backend/src/auth/strategies/github.strategy.ts @@ -23,7 +23,11 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github') { }); } - async validate(_accessToken: string, _refreshToken: string, profile: Profile) { + async validate( + _accessToken: string, + _refreshToken: string, + profile: Profile, + ) { return profile; } } diff --git a/backend/src/auth/strategies/google.strategy.ts b/backend/src/auth/strategies/google.strategy.ts index 9c0da93..49bf060 100644 --- a/backend/src/auth/strategies/google.strategy.ts +++ b/backend/src/auth/strategies/google.strategy.ts @@ -23,7 +23,11 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { }); } - async validate(_accessToken: string, _refreshToken: string, profile: Profile) { + async validate( + _accessToken: string, + _refreshToken: string, + profile: Profile, + ) { return profile; } } diff --git a/backend/src/auth/strategies/jwt.strategy.ts b/backend/src/auth/strategies/jwt.strategy.ts index 3dc5393..5576e3c 100644 --- a/backend/src/auth/strategies/jwt.strategy.ts +++ b/backend/src/auth/strategies/jwt.strategy.ts @@ -2,28 +2,44 @@ import { PassportStrategy } from '@nestjs/passport'; import { ConfigService } from '@nestjs/config'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Request } from 'express'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; import { UsersService } from '../../users/users.service'; +import { RedisService } from '../redis.service'; +import * as crypto from 'crypto'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private readonly usersService: UsersService, + private readonly redisService: RedisService, configService: ConfigService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: configService.get('JWT_SECRET'), ignoreExpiration: false, + passReqToCallback: true, }); } - async validate(payload: JwtPayload) { + async validate(req: Request, payload: JwtPayload) { const user = await this.usersService.findById(payload.sub); if (!user) { throw new UnauthorizedException('User not found'); } + + // Check if the token has been revoked + const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req); + if (token) { + const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); + const isRevoked = await this.redisService.isTokenRevoked(tokenHash); + if (isRevoked) { + throw new UnauthorizedException('Token has been revoked'); + } + } + return user; } } diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index b1c67c4..84ea3dc 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -38,10 +38,7 @@ export class HttpExceptionFilter implements ExceptionFilter { path: request.url, }; - this.logger.error( - `${status} -> `, - (exception as Error)?.stack, - ); + this.logger.error(`${status} -> `, (exception as Error)?.stack); if (!this.isProduction && exception instanceof Error) { Object.assign(payload, { stack: exception.stack }); diff --git a/backend/src/common/logger.config.ts b/backend/src/common/logger.config.ts index 4982e68..79f3c58 100644 --- a/backend/src/common/logger.config.ts +++ b/backend/src/common/logger.config.ts @@ -1,7 +1,11 @@ import { WinstonModuleOptions } from 'nest-winston'; import { format, transports } from 'winston'; -const baseFormat = format.combine(format.timestamp(), format.errors({ stack: true }), format.json()); +const baseFormat = format.combine( + format.timestamp(), + format.errors({ stack: true }), + format.json(), +); export function buildWinstonOptions(level?: string): WinstonModuleOptions { return { diff --git a/backend/src/common/middleware/logger.middleware.ts b/backend/src/common/middleware/logger.middleware.ts index 33434f4..33fe79b 100644 --- a/backend/src/common/middleware/logger.middleware.ts +++ b/backend/src/common/middleware/logger.middleware.ts @@ -14,9 +14,10 @@ export class LoggerMiddleware implements NestMiddleware { const start = Date.now(); const userAgent = req.headers['user-agent'] || 'unknown'; const forwarded = req.headers['x-forwarded-for']; - const ip = typeof forwarded === 'string' - ? forwarded.split(',')[0].trim() - : forwarded?.[0] ?? req.ip; + const ip = + typeof forwarded === 'string' + ? forwarded.split(',')[0].trim() + : (forwarded?.[0] ?? req.ip); res.on('finish', () => { const duration = Date.now() - start; diff --git a/backend/src/config/config.validation.ts b/backend/src/config/config.validation.ts index e74d31b..6b070ef 100644 --- a/backend/src/config/config.validation.ts +++ b/backend/src/config/config.validation.ts @@ -46,6 +46,10 @@ export const ConfigValidationSchema = Joi.object({ 'string.min': 'JWT_SECRET must be at least 32 characters.', 'any.required': 'JWT_SECRET is required.', }), + JWT_REFRESH_SECRET: Joi.string().min(32).required().messages({ + 'string.min': 'JWT_REFRESH_SECRET must be at least 32 characters.', + 'any.required': 'JWT_REFRESH_SECRET is required.', + }), // ── Logging ──────────────────────────────────────────────────────────────── // Defaults to "debug" in development and "warn" in production. diff --git a/backend/src/documents/documents.controller.ts b/backend/src/documents/documents.controller.ts index ae9e6e8..819d34f 100644 --- a/backend/src/documents/documents.controller.ts +++ b/backend/src/documents/documents.controller.ts @@ -37,7 +37,9 @@ const fileFilter: multer.Options['fileFilter'] = (_req, file, callback) => { return callback(null, true); } - return callback(new BadRequestException('Only PDF, PNG, or JPEG files are allowed')); + return callback( + new BadRequestException('Only PDF, PNG, or JPEG files are allowed'), + ); }; @Controller('documents') @@ -78,7 +80,8 @@ export class DocumentsController { return res.status(200).send(existing); } - const uploadDir = this.configService.get('UPLOAD_DIR') || './uploads'; + const uploadDir = + this.configService.get('UPLOAD_DIR') || './uploads'; await fs.mkdir(uploadDir, { recursive: true }); const extension = extname(file.originalname) || ''; @@ -130,7 +133,9 @@ export class DocumentsController { const record = await this.verificationService.findLatestByDocument(id); if (!record) { - throw new NotFoundException('No verification record found for this document'); + throw new NotFoundException( + 'No verification record found for this document', + ); } return record; diff --git a/backend/src/documents/documents.module.ts b/backend/src/documents/documents.module.ts index fe752f1..43e558b 100644 --- a/backend/src/documents/documents.module.ts +++ b/backend/src/documents/documents.module.ts @@ -9,7 +9,6 @@ import { VerificationModule } from '../verification/verification.module'; import { QueueModule } from '../queue/queue.module'; @Module({ - imports: [ ConfigModule, TypeOrmModule.forFeature([Document]), diff --git a/backend/src/documents/documents.service.spec.ts b/backend/src/documents/documents.service.spec.ts index de86779..810adc8 100644 --- a/backend/src/documents/documents.service.spec.ts +++ b/backend/src/documents/documents.service.spec.ts @@ -86,7 +86,10 @@ describe('DocumentsService', () => { ...mockDocument, status: DocumentStatus.VERIFIED, }); - const result = await service.updateStatus('doc-123', DocumentStatus.VERIFIED); + const result = await service.updateStatus( + 'doc-123', + DocumentStatus.VERIFIED, + ); expect(mockRepository.update).toHaveBeenCalledWith('doc-123', { status: DocumentStatus.VERIFIED, }); diff --git a/backend/src/documents/documents.service.ts b/backend/src/documents/documents.service.ts index fbcd509..882f3f3 100644 --- a/backend/src/documents/documents.service.ts +++ b/backend/src/documents/documents.service.ts @@ -27,7 +27,10 @@ export class DocumentsService { return this.documentRepository.findOne({ where: { fileHash } }); } - async updateStatus(id: string, status: DocumentStatus): Promise { + async updateStatus( + id: string, + status: DocumentStatus, + ): Promise { await this.documentRepository.update(id, { status }); return this.findById(id); } diff --git a/backend/src/external-validation/dto/validation-request.dto.ts b/backend/src/external-validation/dto/validation-request.dto.ts index 09c3540..7470a32 100644 --- a/backend/src/external-validation/dto/validation-request.dto.ts +++ b/backend/src/external-validation/dto/validation-request.dto.ts @@ -1,4 +1,8 @@ -import { ValidationType, ValidationStatus, ValidationResult } from '../entities/validation-request.entity'; +import { + ValidationType, + ValidationStatus, + ValidationResult, +} from '../entities/validation-request.entity'; export class CreateValidationRequestDto { documentId: string; diff --git a/backend/src/external-validation/entities/validation-request.entity.ts b/backend/src/external-validation/entities/validation-request.entity.ts index 364337e..e18d719 100644 --- a/backend/src/external-validation/entities/validation-request.entity.ts +++ b/backend/src/external-validation/entities/validation-request.entity.ts @@ -1,4 +1,9 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, +} from 'typeorm'; export enum ValidationStatus { PENDING = 'PENDING', @@ -40,7 +45,11 @@ export class ValidationRequest { @Column({ type: 'jsonb', nullable: true }) metadata: Record; - @Column({ type: 'enum', enum: ValidationStatus, default: ValidationStatus.PENDING }) + @Column({ + type: 'enum', + enum: ValidationStatus, + default: ValidationStatus.PENDING, + }) status: ValidationStatus; @Column({ type: 'enum', enum: ValidationResult, nullable: true }) diff --git a/backend/src/external-validation/external-validation.module.ts b/backend/src/external-validation/external-validation.module.ts new file mode 100644 index 0000000..6fb6871 --- /dev/null +++ b/backend/src/external-validation/external-validation.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExternalValidationService } from './external-validation.service'; +import { + ValidationRequest, + ValidationProvider, +} from './entities/validation-request.entity'; +import { LandRegistryProvider } from './providers/land-registry.provider'; +import { GovernmentIdProvider } from './providers/government-id.provider'; +import { BusinessRegistrationProvider } from './providers/business-registration.provider'; + +@Module({ + imports: [TypeOrmModule.forFeature([ValidationRequest, ValidationProvider])], + providers: [ + ExternalValidationService, + LandRegistryProvider, + GovernmentIdProvider, + BusinessRegistrationProvider, + ], + exports: [ExternalValidationService], +}) +export class ExternalValidationModule {} diff --git a/backend/src/external-validation/external-validation.service.ts b/backend/src/external-validation/external-validation.service.ts index 0ac9002..401062b 100644 --- a/backend/src/external-validation/external-validation.service.ts +++ b/backend/src/external-validation/external-validation.service.ts @@ -1,26 +1,31 @@ -import { Injectable, Logger, NotFoundException, BadRequestException } from "@nestjs/common" -import type { Repository } from "typeorm" +import { + Injectable, + Logger, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import type { Repository } from 'typeorm'; import { ValidationRequest, type ValidationProvider, ValidationStatus, ValidationResult, ValidationType, -} from "./entities/validation-request.entity" +} from './entities/validation-request.entity'; import type { CreateValidationRequestDto, QueryValidationRequestDto, RetryValidationDto, -} from "./dto/validation-request.dto" -import type { IValidationProvider } from "./interfaces/validation-provider.interface" -import type { LandRegistryProvider } from "./providers/land-registry.provider" -import type { GovernmentIdProvider } from "./providers/government-id.provider" -import type { BusinessRegistrationProvider } from "./providers/business-registration.provider" +} from './dto/validation-request.dto'; +import type { IValidationProvider } from './interfaces/validation-provider.interface'; +import type { LandRegistryProvider } from './providers/land-registry.provider'; +import type { GovernmentIdProvider } from './providers/government-id.provider'; +import type { BusinessRegistrationProvider } from './providers/business-registration.provider'; @Injectable() export class ExternalValidationService { - private readonly logger = new Logger(ExternalValidationService.name) - private readonly providers = new Map() + private readonly logger = new Logger(ExternalValidationService.name); + private readonly providers = new Map(); constructor( private validationRequestRepository: Repository, @@ -29,24 +34,33 @@ export class ExternalValidationService { private governmentIdProvider: GovernmentIdProvider, private businessRegistrationProvider: BusinessRegistrationProvider, ) { - this.initializeProviders() + this.initializeProviders(); } private initializeProviders() { - this.providers.set(ValidationType.LAND_REGISTRY, this.landRegistryProvider) - this.providers.set(ValidationType.GOVERNMENT_ID, this.governmentIdProvider) - this.providers.set(ValidationType.BUSINESS_REGISTRATION, this.businessRegistrationProvider) - - this.logger.log(`Initialized ${this.providers.size} validation providers`) + this.providers.set(ValidationType.LAND_REGISTRY, this.landRegistryProvider); + this.providers.set(ValidationType.GOVERNMENT_ID, this.governmentIdProvider); + this.providers.set( + ValidationType.BUSINESS_REGISTRATION, + this.businessRegistrationProvider, + ); + + this.logger.log(`Initialized ${this.providers.size} validation providers`); } - async createValidationRequest(createDto: CreateValidationRequestDto): Promise { - this.logger.log(`Creating validation request for document: ${createDto.documentId}`) + async createValidationRequest( + createDto: CreateValidationRequestDto, + ): Promise { + this.logger.log( + `Creating validation request for document: ${createDto.documentId}`, + ); // Check if provider is available - const provider = this.providers.get(createDto.validationType) + const provider = this.providers.get(createDto.validationType); if (!provider) { - throw new BadRequestException(`No provider available for validation type: ${createDto.validationType}`) + throw new BadRequestException( + `No provider available for validation type: ${createDto.validationType}`, + ); } // Create validation request record @@ -57,44 +71,54 @@ export class ExternalValidationService { requestedBy: createDto.requestedBy, metadata: createDto.metadata, status: ValidationStatus.PENDING, - }) + }); - const savedRequest = await this.validationRequestRepository.save(validationRequest) + const savedRequest = + await this.validationRequestRepository.save(validationRequest); // Process validation asynchronously - this.processValidationAsync(savedRequest.id) + this.processValidationAsync(savedRequest.id); - return savedRequest + return savedRequest; } private async processValidationAsync(requestId: string) { try { - await this.processValidation(requestId) + await this.processValidation(requestId); } catch (error) { - this.logger.error(`Async validation processing failed for request ${requestId}: ${error.message}`, error.stack) + this.logger.error( + `Async validation processing failed for request ${requestId}: ${error.message}`, + error.stack, + ); } } async processValidation(requestId: string): Promise { - const request = await this.validationRequestRepository.findOne({ where: { id: requestId } }) + const request = await this.validationRequestRepository.findOne({ + where: { id: requestId }, + }); if (!request) { - throw new NotFoundException(`Validation request ${requestId} not found`) + throw new NotFoundException(`Validation request ${requestId} not found`); } if (request.status !== ValidationStatus.PENDING) { - throw new BadRequestException(`Validation request ${requestId} is not in pending status`) + throw new BadRequestException( + `Validation request ${requestId} is not in pending status`, + ); } - this.logger.log(`Processing validation request: ${requestId}`) + this.logger.log(`Processing validation request: ${requestId}`); // Update status to in progress - request.status = ValidationStatus.IN_PROGRESS - await this.validationRequestRepository.save(request) + request.status = ValidationStatus.IN_PROGRESS; + await this.validationRequestRepository.save(request); try { - const provider = this.providers.get(request.validationType) + const provider = this.providers.get(request.validationType); if (!provider) { - throw new Error(`No provider available for validation type: ${request.validationType}`) + throw new Error( + `No provider available for validation type: ${request.validationType}`, + ); } // Perform validation @@ -103,116 +127,151 @@ export class ExternalValidationService { validationType: request.validationType, payload: request.requestPayload, metadata: request.metadata, - }) + }); // Update request with response - request.status = ValidationStatus.COMPLETED - request.result = validationResponse.result - request.responsePayload = validationResponse.data + request.status = ValidationStatus.COMPLETED; + request.result = validationResponse.result; + request.responsePayload = validationResponse.data; request.validationDetails = { confidenceScore: validationResponse.confidenceScore, externalReferenceId: validationResponse.externalReferenceId, validatedAt: validationResponse.validatedAt, expiresAt: validationResponse.expiresAt, metadata: validationResponse.metadata, - } - request.validatedAt = validationResponse.validatedAt - request.expiresAt = validationResponse.expiresAt - request.confidenceScore = validationResponse.confidenceScore - request.externalReferenceId = validationResponse.externalReferenceId + }; + request.validatedAt = validationResponse.validatedAt; + request.expiresAt = validationResponse.expiresAt; + request.confidenceScore = validationResponse.confidenceScore; + request.externalReferenceId = validationResponse.externalReferenceId; if (!validationResponse.success) { - request.errorMessage = validationResponse.errorMessage + request.errorMessage = validationResponse.errorMessage; } - const updatedRequest = await this.validationRequestRepository.save(request) + const updatedRequest = + await this.validationRequestRepository.save(request); - this.logger.log(`Validation completed for request ${requestId}: ${request.result}`) - return updatedRequest + this.logger.log( + `Validation completed for request ${requestId}: ${request.result}`, + ); + return updatedRequest; } catch (error) { - this.logger.error(`Validation failed for request ${requestId}: ${error.message}`, error.stack) + this.logger.error( + `Validation failed for request ${requestId}: ${error.message}`, + error.stack, + ); // Update request with error - request.status = ValidationStatus.FAILED - request.result = ValidationResult.ERROR - request.errorMessage = error.message - request.responsePayload = { error: error.message } + request.status = ValidationStatus.FAILED; + request.result = ValidationResult.ERROR; + request.errorMessage = error.message; + request.responsePayload = { error: error.message }; - return this.validationRequestRepository.save(request) + return this.validationRequestRepository.save(request); } } - async findAll(queryDto: QueryValidationRequestDto): Promise<{ requests: ValidationRequest[]; total: number }> { - const { documentId, validationType, status, result, requestedBy, limit, offset } = queryDto - - const queryBuilder = this.validationRequestRepository.createQueryBuilder("validation_request") + async findAll( + queryDto: QueryValidationRequestDto, + ): Promise<{ requests: ValidationRequest[]; total: number }> { + const { + documentId, + validationType, + status, + result, + requestedBy, + limit, + offset, + } = queryDto; + + const queryBuilder = + this.validationRequestRepository.createQueryBuilder('validation_request'); if (documentId) { - queryBuilder.andWhere("validation_request.documentId = :documentId", { documentId }) + queryBuilder.andWhere('validation_request.documentId = :documentId', { + documentId, + }); } if (validationType) { - queryBuilder.andWhere("validation_request.validationType = :validationType", { validationType }) + queryBuilder.andWhere( + 'validation_request.validationType = :validationType', + { validationType }, + ); } if (status) { - queryBuilder.andWhere("validation_request.status = :status", { status }) + queryBuilder.andWhere('validation_request.status = :status', { status }); } if (result) { - queryBuilder.andWhere("validation_request.result = :result", { result }) + queryBuilder.andWhere('validation_request.result = :result', { result }); } if (requestedBy) { - queryBuilder.andWhere("validation_request.requestedBy = :requestedBy", { requestedBy }) + queryBuilder.andWhere('validation_request.requestedBy = :requestedBy', { + requestedBy, + }); } - queryBuilder.orderBy("validation_request.createdAt", "DESC").skip(offset).take(limit) + queryBuilder + .orderBy('validation_request.createdAt', 'DESC') + .skip(offset) + .take(limit); - const [requests, total] = await queryBuilder.getManyAndCount() + const [requests, total] = await queryBuilder.getManyAndCount(); - return { requests, total } + return { requests, total }; } async findOne(id: string): Promise { - const request = await this.validationRequestRepository.findOne({ where: { id } }) + const request = await this.validationRequestRepository.findOne({ + where: { id }, + }); if (!request) { - throw new NotFoundException(`Validation request with ID ${id} not found`) + throw new NotFoundException(`Validation request with ID ${id} not found`); } - return request + return request; } async findByDocument(documentId: string): Promise { return this.validationRequestRepository.find({ where: { documentId }, - order: { createdAt: "DESC" }, - }) + order: { createdAt: 'DESC' }, + }); } - async retryValidation(requestId: string, retryDto: RetryValidationDto): Promise { - const request = await this.findOne(requestId) + async retryValidation( + requestId: string, + retryDto: RetryValidationDto, + ): Promise { + const request = await this.findOne(requestId); if (request.status === ValidationStatus.IN_PROGRESS) { - throw new BadRequestException("Validation is already in progress") + throw new BadRequestException('Validation is already in progress'); } // Update payload if provided if (retryDto.updatedPayload) { - request.requestPayload = { ...request.requestPayload, ...retryDto.updatedPayload } + request.requestPayload = { + ...request.requestPayload, + ...retryDto.updatedPayload, + }; } // Reset status and clear previous results - request.status = ValidationStatus.PENDING - request.result = null - request.responsePayload = null - request.validationDetails = null - request.errorMessage = null - request.validatedAt = null - request.expiresAt = null - request.confidenceScore = null - request.externalReferenceId = null + request.status = ValidationStatus.PENDING; + request.result = null; + request.responsePayload = null; + request.validationDetails = null; + request.errorMessage = null; + request.validatedAt = null; + request.expiresAt = null; + request.confidenceScore = null; + request.externalReferenceId = null; // Add retry metadata request.metadata = { @@ -220,76 +279,85 @@ export class ExternalValidationService { retryCount: (request.metadata?.retryCount || 0) + 1, retryReason: retryDto.reason, retriedAt: new Date().toISOString(), - } + }; - const updatedRequest = await this.validationRequestRepository.save(request) + const updatedRequest = await this.validationRequestRepository.save(request); // Process validation asynchronously - this.processValidationAsync(updatedRequest.id) + this.processValidationAsync(updatedRequest.id); - return updatedRequest + return updatedRequest; } async getValidationStats(): Promise { const stats = await this.validationRequestRepository - .createQueryBuilder("validation_request") + .createQueryBuilder('validation_request') .select([ - "validation_request.validationType as validationType", - "validation_request.status as status", - "validation_request.result as result", - "COUNT(*) as count", - "AVG(validation_request.confidenceScore) as avgConfidenceScore", + 'validation_request.validationType as validationType', + 'validation_request.status as status', + 'validation_request.result as result', + 'COUNT(*) as count', + 'AVG(validation_request.confidenceScore) as avgConfidenceScore', ]) - .groupBy("validation_request.validationType, validation_request.status, validation_request.result") - .getRawMany() + .groupBy( + 'validation_request.validationType, validation_request.status, validation_request.result', + ) + .getRawMany(); - return stats + return stats; } async checkProviderHealth(): Promise> { - const healthStatus: Record = {} + const healthStatus: Record = {}; for (const [type, provider] of this.providers.entries()) { try { - healthStatus[type] = await provider.healthCheck() + healthStatus[type] = await provider.healthCheck(); } catch (error) { - this.logger.error(`Health check failed for provider ${type}: ${error.message}`) - healthStatus[type] = false + this.logger.error( + `Health check failed for provider ${type}: ${error.message}`, + ); + healthStatus[type] = false; } } - return healthStatus + return healthStatus; } async isDocumentValidated( documentId: string, ): Promise<{ isValidated: boolean; validationResults: ValidationRequest[] }> { - const validationResults = await this.findByDocument(documentId) + const validationResults = await this.findByDocument(documentId); const completedValidations = validationResults.filter( - (v) => v.status === ValidationStatus.COMPLETED && v.result === ValidationResult.VALID, - ) + (v) => + v.status === ValidationStatus.COMPLETED && + v.result === ValidationResult.VALID, + ); return { isValidated: completedValidations.length > 0, validationResults, - } + }; } - async expireOldValidations(daysToExpire = 30): Promise { - const cutoffDate = new Date() - cutoffDate.setDate(cutoffDate.getDate() - daysToExpire) - + async expireOldValidations(daysToExpire = 30): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - daysToExpire); + const result = await this.validationRequestRepository .createQueryBuilder() .update(ValidationRequest) .set({ status: ValidationStatus.EXPIRED }) - .where("expiresAt < :cutoffDate", { cutoffDate }) - .andWhere("status IN (:...activeStatuses)", { - activeStatuses: [ValidationStatus.PENDING, ValidationStatus.IN_PROGRESS, ValidationStatus.COMPLETED], + .where('expiresAt < :cutoffDate', { cutoffDate }) + .andWhere('status IN (:...activeStatuses)', { + activeStatuses: [ + ValidationStatus.PENDING, + ValidationStatus.IN_PROGRESS, + ValidationStatus.COMPLETED, + ], }) - .execute() - return result.affected || 0 - + .execute(); + return result.affected || 0; } } diff --git a/backend/src/external-validation/interfaces/validation-provider.interface.ts b/backend/src/external-validation/interfaces/validation-provider.interface.ts index 1d14ad1..ef332c9 100644 --- a/backend/src/external-validation/interfaces/validation-provider.interface.ts +++ b/backend/src/external-validation/interfaces/validation-provider.interface.ts @@ -1,4 +1,7 @@ -import { ValidationType, ValidationResult } from '../entities/validation-request.entity'; +import { + ValidationType, + ValidationResult, +} from '../entities/validation-request.entity'; export interface ValidationDocumentParams { documentId: string; @@ -20,6 +23,8 @@ export interface ValidationResponse { } export interface IValidationProvider { - validateDocument(params: ValidationDocumentParams): Promise; + validateDocument( + params: ValidationDocumentParams, + ): Promise; healthCheck(): Promise; } diff --git a/backend/src/external-validation/providers/business-registration.provider.ts b/backend/src/external-validation/providers/business-registration.provider.ts index bf1e5f8..2ad2536 100644 --- a/backend/src/external-validation/providers/business-registration.provider.ts +++ b/backend/src/external-validation/providers/business-registration.provider.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { IValidationProvider, ValidationResponse } from '../interfaces/validation-provider.interface'; +import { + IValidationProvider, + ValidationResponse, +} from '../interfaces/validation-provider.interface'; import { ValidationResult } from '../entities/validation-request.entity'; @Injectable() diff --git a/backend/src/external-validation/providers/government-id.provider.ts b/backend/src/external-validation/providers/government-id.provider.ts index 3c8498d..d60832e 100644 --- a/backend/src/external-validation/providers/government-id.provider.ts +++ b/backend/src/external-validation/providers/government-id.provider.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { IValidationProvider, ValidationResponse } from '../interfaces/validation-provider.interface'; +import { + IValidationProvider, + ValidationResponse, +} from '../interfaces/validation-provider.interface'; import { ValidationResult } from '../entities/validation-request.entity'; @Injectable() diff --git a/backend/src/external-validation/providers/land-registry.provider.ts b/backend/src/external-validation/providers/land-registry.provider.ts index 7afc39c..5f16b72 100644 --- a/backend/src/external-validation/providers/land-registry.provider.ts +++ b/backend/src/external-validation/providers/land-registry.provider.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { IValidationProvider, ValidationResponse } from '../interfaces/validation-provider.interface'; +import { + IValidationProvider, + ValidationResponse, +} from '../interfaces/validation-provider.interface'; import { ValidationResult } from '../entities/validation-request.entity'; @Injectable() diff --git a/backend/src/queue/document.processor.ts b/backend/src/queue/document.processor.ts index 50269e3..4044626 100644 --- a/backend/src/queue/document.processor.ts +++ b/backend/src/queue/document.processor.ts @@ -26,7 +26,19 @@ export class DocumentProcessor implements OnModuleDestroy { this.queueService.queueName, async (job) => { if (job.name === 'analyze') { - await this.riskService.assessDocument(job.data.documentId); + await this.documentsService.updateStatus( + job.data.documentId, + DocumentStatus.ANALYZING, + ); + const result = await this.riskService.assessDocument( + job.data.documentId, + ); + if (result.flags.length > 0) { + await this.documentsService.updateStatus( + job.data.documentId, + DocumentStatus.FLAGGED, + ); + } return; } if (job.name === 'anchor') { @@ -37,7 +49,11 @@ export class DocumentProcessor implements OnModuleDestroy { ); this.worker.on('failed', (job, err) => { - this.logger.error(`Job ${job.id} (${job.name}) failed`, err?.message, err?.stack); + this.logger.error( + `Job ${job.id} (${job.name}) failed`, + err?.message, + err?.stack, + ); }); } @@ -48,7 +64,9 @@ export class DocumentProcessor implements OnModuleDestroy { return; } - const { txHash, ledger } = await this.stellarService.anchorHash(document.fileHash); + const { txHash, ledger } = await this.stellarService.anchorHash( + document.fileHash, + ); await this.verificationService.create({ documentId, stellarTxHash: txHash, @@ -57,7 +75,10 @@ export class DocumentProcessor implements OnModuleDestroy { status: VerificationStatus.CONFIRMED, }); - await this.documentsService.updateStatus(documentId, DocumentStatus.VERIFIED); + await this.documentsService.updateStatus( + documentId, + DocumentStatus.VERIFIED, + ); this.logger.log(`Document ${documentId} verified on ledger ${ledger}`); } diff --git a/backend/src/queue/queue.service.ts b/backend/src/queue/queue.service.ts index c1e875f..978662e 100644 --- a/backend/src/queue/queue.service.ts +++ b/backend/src/queue/queue.service.ts @@ -24,7 +24,8 @@ export class QueueService implements OnModuleDestroy { private buildConnection(): RedisConnectionOptions { const host = this.configService.get('REDIS_HOST') || '127.0.0.1'; const port = Number(this.configService.get('REDIS_PORT') || '6379'); - const password = this.configService.get('REDIS_PASSWORD') || undefined; + const password = + this.configService.get('REDIS_PASSWORD') || undefined; return { host, port, password }; } diff --git a/backend/src/risk-assessment/risk-assessment.service.ts b/backend/src/risk-assessment/risk-assessment.service.ts index e47365a..71b25f5 100644 --- a/backend/src/risk-assessment/risk-assessment.service.ts +++ b/backend/src/risk-assessment/risk-assessment.service.ts @@ -51,7 +51,9 @@ export class RiskAssessmentService { flags.push(RiskFlag.MISSING_PARCEL_ID); } - const ownerDocuments = await this.documentsService.findByOwner(document.ownerId); + const ownerDocuments = await this.documentsService.findByOwner( + document.ownerId, + ); if (ownerDocuments.some((doc) => doc.id !== document.id)) { flags.push(RiskFlag.OVERLAPPING_CLAIM); } @@ -80,7 +82,10 @@ export class RiskAssessmentService { } private calculateScore(flags: RiskFlag[]): number { - const rawScore = flags.reduce((total, flag) => total + (FLAG_WEIGHTS[flag] ?? 0), 0); + const rawScore = flags.reduce( + (total, flag) => total + (FLAG_WEIGHTS[flag] ?? 0), + 0, + ); return Math.min(100, Math.max(0, rawScore)); } } diff --git a/backend/src/stellar/stellar.service.spec.ts b/backend/src/stellar/stellar.service.spec.ts new file mode 100644 index 0000000..77cb3b3 --- /dev/null +++ b/backend/src/stellar/stellar.service.spec.ts @@ -0,0 +1,104 @@ +import { BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { StellarService } from './stellar.service'; + +describe('StellarService', () => { + let service: StellarService; + let configService: ConfigService; + + const mockConfigService = { + get: jest.fn((key: string) => { + if (key === 'STELLAR_SECRET_KEY') { + // Valid Stellar testnet secret key format (base32 encoded) + return 'SAC26QSNSIBA6DPBDXQWMX2GWZD4ZQ7E2QL3Q5GO5S32DWYP3H66LE3Q'; + } + if (key === 'STELLAR_HORIZON_URL') { + return 'https://horizon-testnet.stellar.org'; + } + if (key === 'STELLAR_NETWORK') { + return 'Test SDF Network ; September 2015'; + } + return null; + }), + }; + + beforeEach(async () => { + configService = mockConfigService as any; + service = new StellarService(configService); + }); + + describe('Hash Validation', () => { + const validHash = 'a'.repeat(64); // Valid 64-char hex string + + it('should accept a valid 64-character hex hash', async () => { + // We can't test the full flow without mocking Horizon, + // but we can verify the validation doesn't throw + expect(() => { + // Access private method via any cast for testing + (service as any).validateHash(validHash); + }).not.toThrow(); + }); + + it('should reject an empty string with BadRequestException', () => { + expect(() => { + (service as any).validateHash(''); + }).toThrow(BadRequestException); + }); + + it('should reject a short string with BadRequestException', () => { + expect(() => { + (service as any).validateHash('abc123'); + }).toThrow(BadRequestException); + }); + + it('should reject a non-hex string with BadRequestException', () => { + expect(() => { + (service as any).validateHash('g'.repeat(64)); // 'g' is not valid hex + }).toThrow(BadRequestException); + }); + + it('should reject a string with special characters with BadRequestException', () => { + expect(() => { + (service as any).validateHash('a'.repeat(60) + '!@#$'); + }).toThrow(BadRequestException); + }); + + it('should reject a string longer than 64 characters with BadRequestException', () => { + expect(() => { + (service as any).validateHash('a'.repeat(65)); + }).toThrow(BadRequestException); + }); + + it('should accept uppercase hex characters', () => { + expect(() => { + (service as any).validateHash('A'.repeat(64)); + }).not.toThrow(); + }); + + it('should accept mixed case hex characters', () => { + expect(() => { + (service as any).validateHash('aB3cD4eF5'.repeat(8).slice(0, 64)); + }).not.toThrow(); + }); + }); + + describe('buildDataKey', () => { + const validHash = 'a'.repeat(64); + + it('should create a data key with doc_ prefix', () => { + const dataKey = (service as any).buildDataKey(validHash); + expect(dataKey).toBe(`doc_${'a'.repeat(58)}`); + }); + + it('should truncate hash to 58 characters for the key', () => { + const dataKey = (service as any).buildDataKey(validHash); + expect(dataKey.length).toBe(62); // 'doc_' (4) + 58 chars = 62 + }); + + it('should reject invalid hash format', () => { + expect(() => { + (service as any).buildDataKey('invalid'); + }).toThrow(BadRequestException); + }); + }); +}); diff --git a/backend/src/stellar/stellar.service.ts b/backend/src/stellar/stellar.service.ts index a198c6a..e098c46 100644 --- a/backend/src/stellar/stellar.service.ts +++ b/backend/src/stellar/stellar.service.ts @@ -1,6 +1,17 @@ -import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { + Injectable, + BadRequestException, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Keypair, Horizon, Networks, Operation, TransactionBuilder } from 'stellar-sdk'; +import { + Keypair, + Horizon, + Networks, + Operation, + TransactionBuilder, +} from 'stellar-sdk'; @Injectable() export class StellarService { @@ -9,15 +20,20 @@ export class StellarService { private readonly anchorKeypair: Keypair; private readonly networkPassphrase: string; private readonly accountId: string; + private readonly SHA256_HASH_REGEX = /^[a-f0-9]{64}$/i; constructor(private readonly configService: ConfigService) { const secretKey = this.configService.get('STELLAR_SECRET_KEY'); - const horizonUrl = this.configService.get('STELLAR_HORIZON_URL') || 'https://horizon-testnet.stellar.org'; + const horizonUrl = + this.configService.get('STELLAR_HORIZON_URL') || + 'https://horizon-testnet.stellar.org'; this.networkPassphrase = this.configService.get('STELLAR_NETWORK') || Networks.TESTNET; if (!secretKey) { - throw new InternalServerErrorException('Stellar secret key is not configured'); + throw new InternalServerErrorException( + 'Stellar secret key is not configured', + ); } this.anchorKeypair = Keypair.fromSecret(secretKey); @@ -25,16 +41,22 @@ export class StellarService { this.server = new Horizon.Server(horizonUrl); } + private validateHash(hash: string): void { + if (!this.SHA256_HASH_REGEX.test(hash)) { + throw new BadRequestException( + 'Invalid SHA-256 hash format. Must be a 64-character hexadecimal string', + ); + } + } + private buildDataKey(hash: string) { - const sanitized = hash.replace(/[^a-zA-Z0-9]/g, ''); - const payload = sanitized.slice(0, 58); + this.validateHash(hash); + const payload = hash.slice(0, 58); return `doc_${payload}`; } async anchorHash(hash: string): Promise<{ txHash: string; ledger: number }> { - if (!hash) { - throw new InternalServerErrorException('Hash is required to anchor a document'); - } + this.validateHash(hash); try { const account = await this.server.loadAccount(this.accountId); @@ -56,14 +78,14 @@ export class StellarService { return { txHash: result.hash, ledger: result.ledger }; } catch (error) { this.logger.error('Failed to anchor document hash', error); - throw new InternalServerErrorException('Unable to anchor document hash on Stellar'); + throw new InternalServerErrorException( + 'Unable to anchor document hash on Stellar', + ); } } async verifyHash(hash: string): Promise { - if (!hash) { - throw new InternalServerErrorException('Hash is required to verify a document'); - } + this.validateHash(hash); try { const key = this.buildDataKey(hash); @@ -74,7 +96,9 @@ export class StellarService { return false; } this.logger.error('Failed to verify document hash', error); - throw new InternalServerErrorException('Unable to verify document hash on Stellar'); + throw new InternalServerErrorException( + 'Unable to verify document hash on Stellar', + ); } } } diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 3fe1012..e3cdd9d 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -5,7 +5,9 @@ import { User } from './entities/user.entity'; @Injectable() export class UsersService { - constructor(@InjectRepository(User) private readonly userRepository: Repository) {} + constructor( + @InjectRepository(User) private readonly userRepository: Repository, + ) {} async create(data: Partial): Promise { const user = this.userRepository.create(data); diff --git a/backend/src/verification/verification.service.ts b/backend/src/verification/verification.service.ts index 398b2d7..dfeaeb1 100644 --- a/backend/src/verification/verification.service.ts +++ b/backend/src/verification/verification.service.ts @@ -30,7 +30,10 @@ export class VerificationService { }); } - async updateStatus(id: string, status: VerificationStatus): Promise { + async updateStatus( + id: string, + status: VerificationStatus, + ): Promise { await this.verificationRepository.update(id, { status }); return this.verificationRepository.findOne({ where: { id } }); }