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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added props_errors.txt
Empty file.
2 changes: 1 addition & 1 deletion src/analytics/analytics.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AnalyticsService } from './analytics.service';
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
240 changes: 142 additions & 98 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { User as PrismaUser, ApiKey, TokenType } from '@prisma/client';
import { User as PrismaUser } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { randomUUID } from 'crypto';
import * as jwt from 'jsonwebtoken';
Expand Down Expand Up @@ -41,7 +41,6 @@
verifyTotpCode,
} from './security.utils';
import { AuthUserPayload } from './types/auth-user.type';

import { LoginRateLimitService } from './login-rate-limit.service';
import { UserRole } from '../types/prisma.types';
import { FraudService } from '../fraud/fraud.service';
Expand All @@ -56,6 +55,21 @@
exp?: number;
};

type ApiKeyWithSecrets = {
id: string;
userId: string;
name: string;
keyPrefix: string;
keyHash: string;
permissions: string[];
usageCount: number;
lastUsedAt: Date | null;
expiresAt: Date | null;
revokedAt: Date | null;
createdAt: Date;
updatedAt: Date;
};

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
Expand Down Expand Up @@ -89,18 +103,26 @@
this.bcryptRounds = parseInt(this.configService.get<string>('BCRYPT_ROUNDS') ?? '12', 10);
}

/**
* Helper to map transactions to activity items for dashboard
*/
private transactionsToActivityItems(transactions: any[], type: 'purchase' | 'sale') {
return transactions.map((tx) => ({
type: 'transaction' as const,
id: tx.id,
title: `Property ${type === 'purchase' ? 'Purchased' : 'Sold'}: ${tx.property?.title || 'Unknown'}`,
description: `${type === 'purchase' ? 'Bought' : 'Sold'} for $${tx.amount}`,
timestamp: tx.createdAt,
}));
}
/**
* Helper to map transactions to activity items for dashboard
*/
private transactionsToActivityItems(
transactions: Array<{
id: string;
property: { title?: string };
amount: string | number | bigint | { toString(): string };
createdAt: Date | string;
}>,
type: 'purchase' | 'sale',
) {
return transactions.map((tx) => ({
type: 'transaction' as const,
id: tx.id,
title: `Property ${type === 'purchase' ? 'Purchased' : 'Sold'}: ${tx.property?.title || 'Unknown'}`,
description: `${type === 'purchase' ? 'Bought' : 'Sold'} for $${typeof tx.amount === 'object' && tx.amount !== null ? (tx.amount as { toString(): string }).toString() : tx.amount}`,
timestamp: tx.createdAt,
}));
}

async register(data: RegisterDto) {
const existingUser = await this.usersService.findByEmail(data.email);
Expand Down Expand Up @@ -331,7 +353,12 @@
* Handle token reuse detection - invalidate entire token family
*/
private async handleTokenReuse(
blacklistedToken: any,
blacklistedToken: {
jti: string;
tokenFamily: string | null;
ipAddress: string | null;
userAgent: string | null;
},
reusedJti: string,
ipAddress?: string,
userAgent?: string,
Expand Down Expand Up @@ -501,70 +528,64 @@
throw new NotFoundException('User not found');
}

const [properties, buyerTransactions, sellerTransactions, documents, apiKeys] =
await Promise.all([
this.prisma.property.findMany({
where: { ownerId: user.sub },
orderBy: { createdAt: 'desc' },
take: 10,
}),
this.prisma.transaction.findMany({
where: { buyerId: user.sub },
orderBy: { createdAt: 'desc' },
take: 5,
include: {
property: {
select: {
id: true,
title: true,
address: true,
city: true,
state: true,
price: true,
},
const [buyerTransactions, sellerTransactions, documents, apiKeys] = await Promise.all([
this.prisma.transaction.findMany({
where: { buyerId: user.sub },
orderBy: { createdAt: 'desc' },
take: 5,
include: {
property: {
select: {
id: true,
title: true,
address: true,
city: true,
state: true,
price: true,
},
seller: {
select: {
firstName: true,
lastName: true,
},
},
seller: {
select: {
firstName: true,
lastName: true,
},
},
}),
this.prisma.transaction.findMany({
where: { sellerId: user.sub },
orderBy: { createdAt: 'desc' },
take: 5,
include: {
property: {
select: {
id: true,
title: true,
address: true,
city: true,
state: true,
price: true,
},
},
}),
this.prisma.transaction.findMany({
where: { sellerId: user.sub },
orderBy: { createdAt: 'desc' },
take: 5,
include: {
property: {
select: {
id: true,
title: true,
address: true,
city: true,
state: true,
price: true,
},
buyer: {
select: {
firstName: true,
lastName: true,
},
},
buyer: {
select: {
firstName: true,
lastName: true,
},
},
}),
this.prisma.document.findMany({
where: { userId: user.sub },
orderBy: { createdAt: 'desc' },
take: 5,
}),
this.prisma.apiKey.findMany({
where: { userId: user.sub },
orderBy: { createdAt: 'desc' },
take: 3,
}),
]);
},
}),
this.prisma.document.findMany({
where: { userId: user.sub },
orderBy: { createdAt: 'desc' },
take: 5,
}),
this.prisma.apiKey.findMany({
where: { userId: user.sub },
orderBy: { createdAt: 'desc' },
take: 3,
}),
]);

const [
totalProperties,
Expand Down Expand Up @@ -607,13 +628,20 @@
const recentActivity = [
...this.transactionsToActivityItems(buyerTransactions, 'purchase'),
...this.transactionsToActivityItems(sellerTransactions, 'sale'),
...documents.map((doc: any) => ({
type: 'document' as const,
id: doc.id,
title: doc.fileName,
description: `Uploaded ${doc.documentType.toLowerCase().replace('_', ' ')}`,
timestamp: doc.createdAt,
})),
...documents.map(
(doc: {
id: string;
fileName: string;
documentType: string;
createdAt: Date | string;
}) => ({
type: 'document' as const,
id: doc.id,
title: doc.fileName,
description: `Uploaded ${doc.documentType.toLowerCase().replace('_', ' ')}`,
timestamp: doc.createdAt,
}),
),
]
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 10);
Expand All @@ -631,21 +659,37 @@
apiKeysCount: apiKeys.length,
},
recentActivity,
recommendations: recommendationProperties.map((p: any) => ({
id: p.id,
title: p.title,
address: p.address,
city: p.city,
state: p.state,
price: p.price.toString(),
propertyType: p.propertyType,
bedrooms: p.bedrooms,
bathrooms: p.bathrooms?.toString(),
squareFeet: p.squareFeet?.toString(),
status: p.status,
agent: `${p.owner.firstName} ${p.owner.lastName}`,
createdAt: p.createdAt,
})),
recommendations: recommendationProperties.map(
(p: {
id: string;
title: string;
address: string;
city: string;
state: string;
price: string | number | bigint;
propertyType: string;
bedrooms?: number | null;
bathrooms?: string | number | bigint | null;
squareFeet?: string | number | bigint | null;
status: string;
owner: { firstName: string; lastName: string };
createdAt: Date | string;
}) => ({
id: p.id,
title: p.title,
address: p.address,
city: p.city,
state: p.state,
price: p.price.toString(),
propertyType: p.propertyType,
bedrooms: p.bedrooms,
bathrooms: p.bathrooms?.toString(),
squareFeet: p.squareFeet?.toString(),
status: p.status,
agent: `${p.owner.firstName} ${p.owner.lastName}`,
createdAt: p.createdAt,
}),
),
};
}

Expand Down Expand Up @@ -837,7 +881,7 @@
orderBy: { createdAt: 'desc' },
});

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

async rotateApiKey(user: AuthUserPayload, apiKeyId: string) {
Expand Down Expand Up @@ -1136,7 +1180,7 @@
return `pc_${randomToken(24)}`;
}

private toApiKeyResponse(apiKey: any) {
private toApiKeyResponse(apiKey: ApiKeyWithSecrets) {
return {
id: apiKey.id,
name: apiKey.name,
Expand Down Expand Up @@ -1270,7 +1314,7 @@
if (historyEntries.length > 0) {
await tx.passwordHistory.deleteMany({
where: {
id: { in: historyEntries.map((entry: any) => entry.id) },
id: { in: historyEntries.map((entry: { id: string }) => entry.id) },
},
});
}
Expand Down Expand Up @@ -1330,7 +1374,7 @@
body: `secret=${secret}&response=${token}`,
});

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

Check warning on line 1377 in src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / Lint & Build

Unexpected any. Specify a different type

// 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
10 changes: 4 additions & 6 deletions src/auth/decorators/gql-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const GqlUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.authUser;
},
);
export const GqlUser = createParamDecorator((data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.authUser;
});
18 changes: 16 additions & 2 deletions src/common/common.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { registerEnumType } from '@nestjs/graphql';
import { UserRole, PropertyStatus, TransactionType, TransactionStatus, DocumentType, VerificationStatus } from '@prisma/client';
import {
UserRole,
PropertyStatus,
TransactionType,
TransactionStatus,
DocumentType,
VerificationStatus,
} from '@prisma/client';

registerEnumType(UserRole, { name: 'UserRole' });
registerEnumType(PropertyStatus, { name: 'PropertyStatus' });
Expand All @@ -8,4 +15,11 @@ registerEnumType(TransactionStatus, { name: 'TransactionStatus' });
registerEnumType(DocumentType, { name: 'DocumentType' });
registerEnumType(VerificationStatus, { name: 'VerificationStatus' });

export { UserRole, PropertyStatus, TransactionType, TransactionStatus, DocumentType, VerificationStatus };
export {
UserRole,
PropertyStatus,
TransactionType,
TransactionStatus,
DocumentType,
VerificationStatus,
};
Loading
Loading