Skip to content
Open
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
838 changes: 838 additions & 0 deletions issue-891-test-files-fix.patch

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions src/admin/admin-audit.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { AdminAuditInterceptor } from './admin-audit.interceptor';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';
import { PrismaService } from '../database/prisma.service';

interface MockPrisma {
activityLog: {
create: jest.Mock;
};
}

describe('AdminAuditInterceptor', () => {
let interceptor: AdminAuditInterceptor;
let mockPrisma: any;
let mockPrisma: MockPrisma;

beforeEach(() => {
mockPrisma = {
activityLog: {
create: jest.fn().mockResolvedValue({}),
},
};
interceptor = new AdminAuditInterceptor(mockPrisma);
interceptor = new AdminAuditInterceptor(mockPrisma as unknown as PrismaService);
});

function makeContext(overrides: Partial<any> = {}): ExecutionContext {
function makeContext(overrides: Record<string, unknown> = {}): ExecutionContext {
const request = {
authUser: { sub: 'admin-1' },
headers: { 'user-agent': 'test-agent' },
Expand Down
2 changes: 1 addition & 1 deletion src/admin/admin-audit.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { PrismaService } from '../database/prisma.service';
export class AdminAuditInterceptor implements NestInterceptor {
constructor(private readonly prisma: PrismaService) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const user = request.authUser;
const ip =
Expand Down
2 changes: 1 addition & 1 deletion src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';

Check warning on line 21 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

'ApiQuery' is defined but never used

Check warning on line 21 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

'ApiOperation' is defined but never used
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
Expand Down Expand Up @@ -55,22 +55,22 @@
) {}

@Get('dashboard')
getDashboard() {

Check warning on line 58 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function

Check warning on line 58 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function
return this.adminService.getDashboard();
}

@Get('backups')
listBackups() {

Check warning on line 63 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function

Check warning on line 63 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function
return this.adminService.listBackups();
}

@Get('backups/status')
getBackupStatus() {

Check warning on line 68 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function

Check warning on line 68 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function
return this.adminService.getBackupStatus();
}

@Get('backups/schedule')
getBackupSchedule() {

Check warning on line 73 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function

Check warning on line 73 in src/admin/admin.controller.ts

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function
return this.adminService.getBackupSchedule();
}

Expand Down Expand Up @@ -225,7 +225,7 @@

@Get('email/preview/:templateName')
async previewEmailTemplate(@Param('templateName') templateName: string) {
const sampleDataMap: Record<string, any> = {
const sampleDataMap: Record<string, Record<string, unknown>> = {
'password-reset': {
resetUrl: 'http://localhost:3000/reset-password?token=sample-token-123',
},
Expand Down
2 changes: 1 addition & 1 deletion src/admin/interceptors/admin-access-logging.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { AuditService } from '../../audit/audit.service';
export class AdminAccessLoggingInterceptor implements NestInterceptor {
constructor(private readonly auditService: AuditService) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();

const response = context.switchToHttp().getResponse();
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/analytics.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { AuthUserPayload } from '../auth/types/auth-user.type';
export class AnalyticsInterceptor implements NestInterceptor {
constructor(private readonly analytics: AnalyticsService) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
const start = Date.now();
Expand Down
36 changes: 28 additions & 8 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ type JwtPayload = {
exp?: number;
};

type TransactionWithPropertyTitle = Prisma.TransactionGetPayload<{
include: { property: { select: { title: true } } };
}>;

type PropertyWithOwnerName = Prisma.PropertyGetPayload<{
include: { owner: { select: { firstName: true; lastName: true } } };
}>;

interface RecaptchaVerifyResponse {
success: boolean;
score?: number;
action?: string;
challenge_ts?: string;
hostname?: string;
'error-codes'?: string[];
}

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
Expand Down Expand Up @@ -106,7 +123,10 @@ export class AuthService {
/**
* Helper to map transactions to activity items for dashboard
*/
private transactionsToActivityItems(transactions: any[], type: 'purchase' | 'sale') {
private transactionsToActivityItems(
transactions: TransactionWithPropertyTitle[],
type: 'purchase' | 'sale',
) {
return transactions.map((tx) => ({
type: 'transaction' as const,
id: tx.id,
Expand Down Expand Up @@ -466,7 +486,7 @@ export class AuthService {
* Handle token reuse detection - invalidate entire token family
*/
private async handleTokenReuse(
blacklistedToken: any,
blacklistedToken: Prisma.BlacklistedToken,
reusedJti: string,
ipAddress?: string,
userAgent?: string,
Expand Down Expand Up @@ -742,7 +762,7 @@ export class AuthService {
const recentActivity = [
...this.transactionsToActivityItems(buyerTransactions, 'purchase'),
...this.transactionsToActivityItems(sellerTransactions, 'sale'),
...documents.map((doc: any) => ({
...documents.map((doc: Prisma.Document) => ({
type: 'document' as const,
id: doc.id,
title: doc.fileName,
Expand All @@ -766,7 +786,7 @@ export class AuthService {
apiKeysCount: apiKeys.length,
},
recentActivity,
recommendations: recommendationProperties.map((p: any) => ({
recommendations: recommendationProperties.map((p: PropertyWithOwnerName) => ({
id: p.id,
title: p.title,
address: p.address,
Expand Down Expand Up @@ -981,7 +1001,7 @@ export class AuthService {
orderBy: { createdAt: 'desc' },
});

return apiKeys.map((apiKey: any) => this.toApiKeyResponse(apiKey));
return apiKeys.map((apiKey: Prisma.ApiKey) => this.toApiKeyResponse(apiKey));
}

async rotateApiKey(user: AuthUserPayload, apiKeyId: string) {
Expand Down Expand Up @@ -1340,7 +1360,7 @@ export class AuthService {
return `pc_${randomToken(24)}`;
}

private toApiKeyResponse(apiKey: any) {
private toApiKeyResponse(apiKey: Prisma.ApiKey) {
return {
id: apiKey.id,
name: apiKey.name,
Expand Down Expand Up @@ -1483,7 +1503,7 @@ export class AuthService {
if (historyEntries.length > 0) {
await tx.passwordHistory.deleteMany({
where: {
id: { in: historyEntries.map((entry: any) => entry.id) },
id: { in: historyEntries.map((entry: Prisma.PasswordHistory) => entry.id) },
},
});
}
Expand Down Expand Up @@ -1553,7 +1573,7 @@ export class AuthService {
body: `secret=${secret}&response=${token}`,
});

const data = (await response.json()) as any;
const data = (await response.json()) as RecaptchaVerifyResponse;

// reCAPTCHA v3 returns a score between 0.0 and 1.0. Typically, 0.5 is a good threshold.
if (data.success && data.score !== undefined && data.score >= 0.5) {
Expand Down
3 changes: 2 additions & 1 deletion src/auth/guards/rate-limit.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { RateLimitService } from '../rate-limit.service';
import { RATE_LIMIT_HEADERS } from '../rate-limit.config';

Expand Down Expand Up @@ -155,7 +156,7 @@ export class RateLimitGuard implements CanActivate {
/**
* Extract client IP from request
*/
private getClientIp(request: any): string {
private getClientIp(request: Request): string {
return (
request.headers['x-forwarded-for']?.split(',')[0].trim() ||
request.connection?.remoteAddress ||
Expand Down
2 changes: 1 addition & 1 deletion src/auth/interceptors/rate-limit-headers.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RATE_LIMIT_HEADERS } from '../rate-limit.config';
*/
@Injectable()
export class RateLimitHeadersInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();

Expand Down
14 changes: 12 additions & 2 deletions src/auth/login-rate-limit.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { LoginRateLimitService } from './login-rate-limit.service';
import { PrismaService } from '../database/prisma.service';

interface MockPrisma {
loginAttempt: {
findFirst: jest.Mock;
count: jest.Mock;
create: jest.Mock;
updateMany: jest.Mock;
};
}

describe('LoginRateLimitService', () => {
let service: LoginRateLimitService;
let mockPrisma: any;
let mockPrisma: MockPrisma;

const email = 'test@example.com';
const ip = '1.2.3.4';
Expand All @@ -16,7 +26,7 @@ describe('LoginRateLimitService', () => {
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
},
};
service = new LoginRateLimitService(mockPrisma);
service = new LoginRateLimitService(mockPrisma as unknown as PrismaService);
});

describe('isAccountLocked', () => {
Expand Down
11 changes: 6 additions & 5 deletions src/auth/rate-limit.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RateLimitService } from './rate-limit.service';
import { ExecutionContext, HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

function makeContext(overrides: Partial<any> = {}): ExecutionContext {
function makeContext(overrides: Record<string, unknown> = {}): ExecutionContext {
const request = {
method: 'POST',
url: '/auth/login',
Expand Down Expand Up @@ -40,7 +40,7 @@ describe('RateLimitGuard - auth/signup endpoints', () => {
checkUserRateLimit: jest.fn().mockResolvedValue(notExceeded),
checkEndpointRateLimit: jest.fn().mockResolvedValue({ ...notExceeded, limit: 0 }),
getHeaders: jest.fn().mockReturnValue({}),
} as any;
} as unknown as jest.Mocked<RateLimitService>;

guard = new RateLimitGuard(reflector, rateLimitService);
});
Expand Down Expand Up @@ -68,10 +68,11 @@ describe('RateLimitGuard - auth/signup endpoints', () => {
try {
await guard.canActivate(ctx);
fail('should have thrown');
} catch (e: any) {
} catch (e: unknown) {
expect(e).toBeInstanceOf(HttpException);
expect(e.getStatus()).toBe(429);
const body = e.getResponse();
const httpException = e as HttpException;
expect(httpException.getStatus()).toBe(429);
const body = httpException.getResponse();
expect(body).toMatchObject({ retryAfter: 60 });
}
});
Expand Down
Loading
Loading