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
23 changes: 22 additions & 1 deletion harvest-finance/backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ApiQuery,
} from '@nestjs/swagger';
import { AdminService } from './admin.service';
import { EmailTemplatingService } from '../notifications/email/email-templating.service';
import { DashboardStatsDto } from './dto/dashboard-stats.dto';
import { PlatformAnalyticsDto } from './dto/analytics.dto';
import { CreateVaultDto, UpdateVaultDto } from './dto/vault-crud.dto';
Expand All @@ -39,7 +40,10 @@ import { UserRole } from '../database/entities/user.entity';
@Roles(UserRole.ADMIN)
@ApiBearerAuth()
export class AdminController {
constructor(private readonly adminService: AdminService) {}
constructor(
private readonly adminService: AdminService,
private readonly emailTemplatingService: EmailTemplatingService,
) {}

@Get('stats')
@ApiOperation({ summary: 'Get overall dashboard metrics' })
Expand Down Expand Up @@ -131,4 +135,21 @@ export class AdminController {
async getUserActivity(): Promise<any[]> {
return this.adminService.getUserActivity();
}

@Get('email-preview/:templateName')
@ApiOperation({ summary: 'Preview email template in browser' })
@ApiParam({
name: 'templateName',
description: 'Email template name',
enum: ['welcome', 'deposit-confirmed', 'withdrawal-complete', 'security-alert'],
})
@ApiResponse({ status: 200, description: 'Email preview HTML' })
@ApiResponse({ status: 404, description: 'Template not found' })
async previewEmailTemplate(
@Param('templateName') templateName: string,
): Promise<{ html: string; subject: string }> {
return this.emailTemplatingService.renderPreview(
templateName as any,
);
}
}
5 changes: 3 additions & 2 deletions harvest-finance/backend/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { EmailTemplatingService } from '../notifications/email/email-templating.service';
import { Vault } from '../database/entities/vault.entity';
import { Deposit } from '../database/entities/deposit.entity';
import { User } from '../database/entities/user.entity';
Expand All @@ -13,7 +14,7 @@ import { Withdrawal } from '../database/entities/withdrawal.entity';
TypeOrmModule.forFeature([Vault, Deposit, User, Reward, Withdrawal]),
],
controllers: [AdminController],
providers: [AdminService],
exports: [AdminService],
providers: [AdminService, EmailTemplatingService],
exports: [AdminService, EmailTemplatingService],
})
export class AdminModule {}
20 changes: 20 additions & 0 deletions harvest-finance/backend/src/database/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ export class User {
@Exclude()
emailVerificationToken: string | null;

@Column({ name: 'phone_number', nullable: true })
phoneNumber: string | null;

@Column({ name: 'phone_verified_at', nullable: true })
phoneVerifiedAt: Date | null;

@OneToMany(() => Session, (session) => session.user)
sessions: Session[];

Expand All @@ -136,6 +142,20 @@ export class User {
@Column({ name: 'locked_until', nullable: true, default: null })
lockedUntil: Date | null;

@Column({
name: 'notification_preferences',
type: 'jsonb',
nullable: true,
default: () => `'{
"depositConfirmed": {"email": true, "sms": false, "push": true, "inApp": true},
"withdrawalCompleted": {"email": true, "sms": false, "push": true, "inApp": true},
"vaultPaused": {"email": true, "sms": true, "push": true, "inApp": true},
"securityAlert": {"email": true, "sms": true, "push": true, "inApp": true},
"yieldMilestone": {"email": true, "sms": false, "push": true, "inApp": true}
}'::jsonb`,
})
notificationPreferences: Record<string, any> | null;

@CreateDateColumn({ name: 'created_at' })
createdAt: Date;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

export class AddPhoneAndNotificationPreferencesToUsers1700000000022
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'phone_number',
type: 'varchar',
isNullable: true,
}),
);

await queryRunner.addColumn(
'users',
new TableColumn({
name: 'phone_verified_at',
type: 'timestamp with time zone',
isNullable: true,
}),
);

await queryRunner.addColumn(
'users',
new TableColumn({
name: 'notification_preferences',
type: 'jsonb',
isNullable: true,
default: `'{
"depositConfirmed": {"email": true, "sms": false, "push": true, "inApp": true},
"withdrawalCompleted": {"email": true, "sms": false, "push": true, "inApp": true},
"vaultPaused": {"email": true, "sms": true, "push": true, "inApp": true},
"securityAlert": {"email": true, "sms": true, "push": true, "inApp": true},
"yieldMilestone": {"email": true, "sms": false, "push": true, "inApp": true}
}'::jsonb`,
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'notification_preferences');
await queryRunner.dropColumn('users', 'phone_verified_at');
await queryRunner.dropColumn('users', 'phone_number');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IsObject, IsOptional, IsBoolean } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class ChannelPreferencesDto {
@ApiProperty({ description: 'Email notification enabled', type: Boolean })
@IsBoolean()
email?: boolean;

@ApiProperty({ description: 'SMS notification enabled', type: Boolean })
@IsBoolean()
sms?: boolean;

@ApiProperty({ description: 'Push notification enabled', type: Boolean })
@IsBoolean()
push?: boolean;

@ApiProperty({ description: 'In-app notification enabled', type: Boolean })
@IsBoolean()
inApp?: boolean;
}

export class NotificationPreferencesDto {
@ApiProperty({ type: ChannelPreferencesDto })
@IsOptional()
@IsObject()
depositConfirmed?: ChannelPreferencesDto;

@ApiProperty({ type: ChannelPreferencesDto })
@IsOptional()
@IsObject()
withdrawalCompleted?: ChannelPreferencesDto;

@ApiProperty({ type: ChannelPreferencesDto })
@IsOptional()
@IsObject()
vaultPaused?: ChannelPreferencesDto;

@ApiProperty({ type: ChannelPreferencesDto })
@IsOptional()
@IsObject()
securityAlert?: ChannelPreferencesDto;

@ApiProperty({ type: ChannelPreferencesDto })
@IsOptional()
@IsObject()
yieldMilestone?: ChannelPreferencesDto;
}

export class UpdateNotificationPreferencesDto {
@ApiProperty({ type: NotificationPreferencesDto })
@IsObject()
preferences: NotificationPreferencesDto;
}
38 changes: 38 additions & 0 deletions harvest-finance/backend/src/notifications/dto/sms.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IsPhoneNumber, IsString, Length } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class SetPhoneNumberDto {
@ApiProperty({
description: 'Phone number in E.164 format',
example: '+2348012345678',
})
@IsPhoneNumber()
phoneNumber: string;
}

export class VerifyPhoneNumberDto {
@ApiProperty({
description: 'OTP code (6 digits)',
example: '123456',
})
@IsString()
@Length(6, 6)
otpCode: string;
}

export class SendSMSDto {
@ApiProperty({
description: 'SMS message to send',
example: 'Your verification code is 123456',
})
@IsString()
@Length(1, 500)
message: string;

@ApiProperty({
description: 'Event type for tracking',
example: 'withdrawal_alert',
})
@IsString()
eventType: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { WelcomeEmail, WelcomeEmailText } from '../templates/welcome.email';
import {
DepositConfirmedEmail,
DepositConfirmedEmailText,
} from '../templates/deposit-confirmed.email';
import {
WithdrawalCompleteEmail,
WithdrawalCompleteEmailText,
} from '../templates/withdrawal-complete.email';
import {
SecurityAlertEmail,
SecurityAlertEmailText,
} from '../templates/security-alert.email';

export type EmailTemplate =
| 'welcome'
| 'deposit-confirmed'
| 'withdrawal-complete'
| 'security-alert';

export interface EmailRenderResult {
html: string;
text: string;
subject: string;
}

@Injectable()
export class EmailTemplatingService {
private templates = {
welcome: { html: WelcomeEmail, text: WelcomeEmailText, subject: 'Welcome to Harvest Finance' },
'deposit-confirmed': {
html: DepositConfirmedEmail,
text: DepositConfirmedEmailText,
subject: 'Deposit Confirmed',
},
'withdrawal-complete': {
html: WithdrawalCompleteEmail,
text: WithdrawalCompleteEmailText,
subject: 'Withdrawal Complete',
},
'security-alert': {
html: SecurityAlertEmail,
text: SecurityAlertEmailText,
subject: 'Security Alert',
},
};

/**
* Render an email template to HTML and text
*/
renderTemplate(
templateName: EmailTemplate,
data: Record<string, any>,
): EmailRenderResult {
const template = this.templates[templateName];

if (!template) {
throw new BadRequestException(`Template '${templateName}' not found`);
}

const html = template.html(data);
const text = template.text(data);

return {
html,
text,
subject: template.subject,
};
}

/**
* Get available templates
*/
getAvailableTemplates(): EmailTemplate[] {
return Object.keys(this.templates) as EmailTemplate[];
}

/**
* Render preview (for admin endpoint)
*/
renderPreview(templateName: EmailTemplate): { html: string; subject: string } {
const mockData = this.getMockDataForTemplate(templateName);
const rendered = this.renderTemplate(templateName, mockData);

return {
html: rendered.html,
subject: rendered.subject,
};
}

private getMockDataForTemplate(templateName: EmailTemplate): Record<string, any> {
switch (templateName) {
case 'welcome':
return {
userName: 'John Doe',
verificationLink: 'https://harvestfinance.io/verify?token=abc123',
};
case 'deposit-confirmed':
return {
userName: 'John Doe',
vaultName: 'Summer Crop Fund',
amount: 5000,
transactionHash: '0x1234567890abcdef1234567890abcdef12345678',
timestamp: new Date().toISOString(),
};
case 'withdrawal-complete':
return {
userName: 'John Doe',
vaultName: 'Summer Crop Fund',
amount: 2500,
transactionHash: '0xabcdef1234567890abcdef1234567890abcdef12',
timestamp: new Date().toISOString(),
};
case 'security-alert':
return {
userName: 'John Doe',
alertType: 'New Login',
description: 'A new login was detected from a different location',
timestamp: new Date().toISOString(),
actionUrl: 'https://harvestfinance.io/security',
};
default:
return {};
}
}
}
Loading