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

Filter by extension

Filter by extension


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

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@
"@nestjs/core": "^10.3.0",
"@nestjs/graphql": "^12.2.2",
"@nestjs/platform-express": "^10.3.0",
"@nestjs/platform-socket.io": "^10.4.22",
"@nestjs/schedule": "^6.1.3",
"@nestjs/swagger": "^7.1.16",
"@nestjs/websockets": "^10.4.22",
"@prisma/client": "^6.19.2",
"@types/uuid": "^9.0.7",
"archiver": "^7.0.1",
"bcrypt": "^6.0.0",
"cache-manager": "^7.2.8",
Expand All @@ -57,7 +60,9 @@
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"sharp": "^0.34.5",
"swagger-ui-express": "^5.0.1"
"socket.io": "^4.8.3",
"swagger-ui-express": "^5.0.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
Expand Down
96 changes: 96 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ enum FraudPattern {
HIGH_VALUE_NEW_ACCOUNT_LISTING
}

enum NotificationStatus {
PENDING
DELIVERED
READ
}

enum EmailStatus {
ACTIVE
BOUNCED
INVALID
}

enum BounceType {
HARD
SOFT
}


// User model
model User {
id String @id @default(uuid())
Expand Down Expand Up @@ -147,6 +165,12 @@ model User {
savedFilters SavedFilter[]
searchAnalytics SearchAnalytics[]
searchHistory SearchHistory[]
emailStatus EmailStatus @default(ACTIVE) @map("email_status")
notifications Notification[]
linkClicks LinkClick[]
emailEngagements EmailEngagement[]
emailBounces EmailBounce[]


@@index([email])
@@index([role])
Expand Down Expand Up @@ -602,3 +626,75 @@ model SearchSuggestion {
@@index([expiresAt])
@@map("search_suggestions")
}

model Notification {
id String @id @default(uuid())
userId String @map("user_id")
title String
message String
type String
status NotificationStatus @default(PENDING)
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
readAt DateTime? @map("read_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
@@index([status])
@@index([createdAt])
@@map("notifications")
}

model LinkClick {
id String @id @default(uuid())
url String
userId String? @map("user_id")
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")

user User? @relation(fields: [userId], references: [id], onDelete: SetNull)

@@index([url])
@@index([userId])
@@index([createdAt])
@@map("link_clicks")
}

model EmailEngagement {
id String @id @default(uuid())
trackingId String @unique @map("tracking_id")
userId String @map("user_id")
emailType String @map("email_type")
openedAt DateTime? @map("opened_at")
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
@@index([trackingId])
@@index([createdAt])
@@map("email_engagements")
}

model EmailBounce {
id String @id @default(uuid())
userId String @map("user_id")
email String
bounceType BounceType @map("bounce_type")
reason String?
rawEvent Json? @map("raw_event")
createdAt DateTime @default(now()) @map("created_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
@@index([email])
@@index([bounceType])
@@map("email_bounces")
}

4 changes: 4 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import './common/common.types'; // Load registered enums
import { AdminModule } from './admin/admin.module';
import { FraudModule } from './fraud/fraud.module';
import { SearchModule } from './search/search.module';
import { TrackingModule } from './tracking/tracking.module';
import { NotificationsModule } from './notifications/notifications.module';
@Module({
imports: [
ConfigModule.forRoot({
Expand Down Expand Up @@ -52,6 +54,8 @@ import { SearchModule } from './search/search.module';
DocumentsModule,
IntegrationsModule,
SearchModule,
TrackingModule,
NotificationsModule,
],
controllers: [AppController],
})
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,
};
12 changes: 3 additions & 9 deletions src/content/content.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ export class ContentController {
constructor(private service: ContentService) {}

@Post('pages/:slug')
updatePage(
@Param('slug') slug: string,
@Body() body: { title: string; content: string },
) {
updatePage(@Param('slug') slug: string, @Body() body: { title: string; content: string }) {
return this.service.updatePage(slug, body);
}

Expand Down Expand Up @@ -39,15 +36,12 @@ export class ContentController {
}

@Post('legal/:type')
updateLegal(
@Param('type') type: string,
@Body() body: { content: string },
) {
updateLegal(@Param('type') type: string, @Body() body: { content: string }) {
return this.service.updateLegal(type, body.content);
}

@Get('legal/:type')
getLegal(@Param('type') type: string) {
return this.service.getLegal(type);
}
}
}
2 changes: 1 addition & 1 deletion src/content/content.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ import { PrismaService } from 'src/database/prisma.service';
providers: [ContentService, PrismaService],
controllers: [ContentController],
})
export class ContentModule {}
export class ContentModule {}
2 changes: 1 addition & 1 deletion src/content/content.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ export class ContentService {
getLegal(type: string) {
return this.legal.get(type) || null;
}
}
}
25 changes: 25 additions & 0 deletions src/email/email-webhook.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { EmailService } from './email.service';
import { ApiTags, ApiOperation } from '@nestjs/swagger';

@ApiTags('webhooks')
@Controller('webhooks/email')
export class EmailWebhookController {
constructor(private emailService: EmailService) {}

@Post('bounce')
@HttpCode(200)
@ApiOperation({ summary: 'Handle email bounce webhooks' })
async handleBounce(@Body() payload: any) {
// Basic extraction logic - in a real app, this would be provider-specific
const email = payload.email || payload.recipient;
const type = payload.type || (payload.bounceType === 'Hard' ? 'HARD' : 'SOFT');
const reason = payload.reason || payload.diagnosticCode;

if (email) {
await this.emailService.handleBounce(email, type as 'HARD' | 'SOFT', reason, payload);
}

return { received: true };
}
}
5 changes: 5 additions & 0 deletions src/email/email.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
import { EmailWebhookController } from './email-webhook.controller';
import { PrismaModule } from '../database/prisma.module';
import { TrackingModule } from '../tracking/tracking.module';

@Module({
imports: [PrismaModule, TrackingModule],
controllers: [EmailWebhookController],
providers: [EmailService],
exports: [EmailService],
})
Expand Down
Loading
Loading