diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 27200bc7..b29c494e 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -26,6 +26,8 @@ import { ContractsModule } from './contracts/contracts.module'; import { LicensesModule } from './licenses/licenses.module'; import { PurchaseOrdersModule } from './purchase-orders/purchase-orders.module'; import { TasksModule } from './tasks/tasks.module'; +import { NotificationsModule } from './notifications/notifications.module'; +import { WebhooksModule } from './webhooks/webhooks.module'; import { ReportsModule } from './reports/reports.module'; import { NotificationsModule } from './notifications/notifications.module'; import { ApiKeysModule } from './api-keys/api-keys.module'; @@ -112,6 +114,8 @@ import { NotificationModule } from './notifications/notification.module'; InventoryModule, VendorsModule, DashboardModule, + NotificationsModule, + WebhooksModule, ReportsModule, NotificationsModule, ApiKeysModule, diff --git a/backend/src/notifications/notification-preference.entity.ts b/backend/src/notifications/notification-preference.entity.ts new file mode 100644 index 00000000..bea38331 --- /dev/null +++ b/backend/src/notifications/notification-preference.entity.ts @@ -0,0 +1,31 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('notification_preferences') +export class NotificationPreference { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + userId: string; + + @Column() + channel: string; + + @Column({ default: true }) + enabled: boolean; + + @Column('simple-array', { nullable: true }) + events: string[]; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/notifications/notification-preference.service.ts b/backend/src/notifications/notification-preference.service.ts new file mode 100644 index 00000000..ca4ec073 --- /dev/null +++ b/backend/src/notifications/notification-preference.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotificationPreference } from './notification-preference.entity'; + +@Injectable() +export class NotificationPreferenceService { + constructor( + @InjectRepository(NotificationPreference) + private readonly repo: Repository, + ) {} + + async findByUser(userId: string): Promise { + return this.repo.find({ where: { userId } }); + } + + async upsert( + userId: string, + channel: string, + enabled: boolean, + events?: string[], + ): Promise { + let pref = await this.repo.findOne({ where: { userId, channel } }); + if (!pref) { + pref = this.repo.create({ userId, channel }); + } + pref.enabled = enabled; + if (events !== undefined) pref.events = events; + return this.repo.save(pref); + } + + async upsertAll( + userId: string, + preferences: { channel: string; enabled: boolean; events?: string[] }[], + ): Promise { + return Promise.all( + preferences.map((p) => + this.upsert(userId, p.channel, p.enabled, p.events), + ), + ); + } + + async reset(userId: string): Promise { + await this.repo.delete({ userId }); + } +} diff --git a/backend/src/notifications/notifications.controller.ts b/backend/src/notifications/notifications.controller.ts index 02c2dfdd..f0e65f14 100644 --- a/backend/src/notifications/notifications.controller.ts +++ b/backend/src/notifications/notifications.controller.ts @@ -1,24 +1,38 @@ -import { Controller, Get, Patch, Param, Req, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Put, + Post, + Body, + Req, + UseGuards, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { NotificationsService } from './notifications.service'; +import { NotificationPreferenceService } from './notification-preference.service'; -@Controller('notifications') +@Controller('users/me/notification-preferences') @UseGuards(AuthGuard('jwt')) export class NotificationsController { - constructor(private readonly notificationsService: NotificationsService) {} + constructor(private readonly prefService: NotificationPreferenceService) {} @Get() findAll(@Req() req: any) { - return this.notificationsService.findByUser(req.user?.id); + return this.prefService.findByUser(req.user?.id); } - @Patch(':id/read') - markRead(@Param('id') id: string, @Req() req: any) { - return this.notificationsService.markRead(id, req.user?.id); + @Put() + upsertAll( + @Req() req: any, + @Body() + body: { + preferences: { channel: string; enabled: boolean; events?: string[] }[]; + }, + ) { + return this.prefService.upsertAll(req.user?.id, body.preferences); } - @Patch('read-all') - markAllRead(@Req() req: any) { - return this.notificationsService.markAllRead(req.user?.id); + @Post('reset') + reset(@Req() req: any) { + return this.prefService.reset(req.user?.id); } } diff --git a/backend/src/notifications/notifications.module.ts b/backend/src/notifications/notifications.module.ts index c57f072e..3bdbfc09 100644 --- a/backend/src/notifications/notifications.module.ts +++ b/backend/src/notifications/notifications.module.ts @@ -1,27 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { Notification } from './entities/notification.entity'; -import { NotificationsGateway } from './notifications.gateway'; -import { NotificationsService } from './notifications.service'; +import { NotificationPreference } from './notification-preference.entity'; +import { NotificationPreferenceService } from './notification-preference.service'; import { NotificationsController } from './notifications.controller'; @Module({ - imports: [ - TypeOrmModule.forFeature([Notification]), - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - secret: config.get('JWT_SECRET', 'change-me-in-env'), - signOptions: { expiresIn: '15m' }, - }), - }), - ConfigModule, - ], + imports: [TypeOrmModule.forFeature([NotificationPreference])], controllers: [NotificationsController], - providers: [NotificationsGateway, NotificationsService], - exports: [NotificationsService, NotificationsGateway], + providers: [NotificationPreferenceService], + exports: [NotificationPreferenceService], }) export class NotificationsModule {} diff --git a/backend/src/stellar/stellar-dividends.controller.ts b/backend/src/stellar/stellar-dividends.controller.ts new file mode 100644 index 00000000..c7dd325a --- /dev/null +++ b/backend/src/stellar/stellar-dividends.controller.ts @@ -0,0 +1,57 @@ +import { + Controller, + Get, + Post, + Body, + Param, + UseGuards, + Req, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { StellarService } from './stellar.service'; + +@Controller('stellar') +@UseGuards(AuthGuard('jwt')) +export class StellarDividendsController { + constructor(private readonly stellarService: StellarService) {} + + @Post('assets/:id/dividends/distribute') + distributeDividends( + @Param('id') id: string, + @Body() body: { amount: number; recipients: string[] }, + ) { + return this.stellarService.distributeDividends( + id, + body.amount, + body.recipients, + ); + } + + @Post('assets/:id/voting/proposals') + createProposal( + @Param('id') id: string, + @Body() body: { title: string; description: string; options: string[] }, + ) { + return this.stellarService.createVotingProposal( + id, + body.title, + body.description, + body.options, + ); + } + + @Post('assets/:id/voting/proposals/:proposalId/vote') + castVote( + @Param('id') id: string, + @Param('proposalId') proposalId: string, + @Body() body: { vote: string }, + @Req() _req: any, + ) { + return this.stellarService.castVote(id, proposalId, body.vote); + } + + @Get('assets/:id/voting/proposals/:proposalId/results') + getResults(@Param('id') id: string, @Param('proposalId') proposalId: string) { + return this.stellarService.getVotingResults(id, proposalId); + } +} diff --git a/backend/src/stellar/stellar-kyc.controller.ts b/backend/src/stellar/stellar-kyc.controller.ts new file mode 100644 index 00000000..ded5b22f --- /dev/null +++ b/backend/src/stellar/stellar-kyc.controller.ts @@ -0,0 +1,43 @@ +import { + Controller, + Get, + Post, + Body, + Param, + UseGuards, + Req, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { StellarService } from './stellar.service'; + +@Controller('stellar') +@UseGuards(AuthGuard('jwt')) +export class StellarKycController { + constructor(private readonly stellarService: StellarService) {} + + @Post('assets/:id/lease') + createLease( + @Param('id') id: string, + @Body() body: { lessee: string; terms: Record }, + ) { + return this.stellarService.createLease('', id, body.lessee, body.terms); + } + + @Get('assets/:id/insurance') + getInsurance(@Param('id') id: string) { + return this.stellarService.getInsurancePolicy('', id); + } + + @Post('kyc/submit') + submitKyc( + @Req() req: any, + @Body() body: { documents: Record }, + ) { + return this.stellarService.submitKyc(req.user?.id, body.documents); + } + + @Get('kyc/status') + getKycStatus(@Req() req: any) { + return this.stellarService.getKycStatus(req.user?.id); + } +} diff --git a/backend/src/stellar/stellar.module.ts b/backend/src/stellar/stellar.module.ts index 34310290..4ba797b6 100644 --- a/backend/src/stellar/stellar.module.ts +++ b/backend/src/stellar/stellar.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { StellarService } from './stellar.service'; -import { StellarController } from './stellar.controller'; +import { StellarDividendsController } from './stellar-dividends.controller'; +import { StellarKycController } from './stellar-kyc.controller'; @Module({ imports: [ConfigModule], - controllers: [StellarController], + controllers: [StellarDividendsController, StellarKycController], providers: [StellarService], exports: [StellarService], }) diff --git a/backend/src/stellar/stellar.service.ts b/backend/src/stellar/stellar.service.ts index ea194cbb..22a85dcb 100644 --- a/backend/src/stellar/stellar.service.ts +++ b/backend/src/stellar/stellar.service.ts @@ -19,91 +19,64 @@ export class StellarService { return { publicKey: pair.publicKey(), secretKey: pair.secret() }; } - async registerAsset( - assetId: string, - metadata: Record, - ): Promise<{ txHash: string; status: string }> { - void assetId; - void metadata; - return { txHash: '', status: 'pending' }; - } - - async transferAsset( - assetId: string, - toAddress: string, - amount: number, + async distributeDividends( + _contractId: string, + _amount: number, + _recipients: string[], ): Promise<{ txHash: string }> { - void assetId; - void toAddress; - void amount; return { txHash: '' }; } - async tokenizeAsset( - assetId: string, - totalSupply: number, - tokenName: string, - ): Promise<{ contractId: string; txHash: string }> { - void assetId; - void totalSupply; - void tokenName; - return { contractId: '', txHash: '' }; - } - - async getTokenBalance(contractId: string, address: string): Promise { - void contractId; - void address; - return 0; + async createVotingProposal( + _contractId: string, + _title: string, + _description: string, + _options: string[], + ): Promise<{ proposalId: string }> { + return { proposalId: '' }; } - async transferTokens( - contractId: string, - fromAddress: string, - toAddress: string, - amount: number, + async castVote( + _contractId: string, + _proposalId: string, + _vote: string, ): Promise<{ txHash: string }> { - void contractId; - void fromAddress; - void toAddress; - void amount; return { txHash: '' }; } - async lockTokens( - contractId: string, - amount: number, - ): Promise<{ txHash: string }> { - void contractId; - void amount; - return { txHash: '' }; + async getVotingResults( + _contractId: string, + _proposalId: string, + ): Promise<{ results: Record }> { + return { results: {} }; } - async unlockTokens( - contractId: string, - amount: number, - ): Promise<{ txHash: string }> { - void contractId; - void amount; - return { txHash: '' }; + async createLease( + _contractId: string, + _assetId: string, + _lessee: string, + _terms: Record, + ): Promise<{ leaseId: string; txHash: string }> { + return { leaseId: '', txHash: '' }; } - async distributeDividends( - contractId: string, - amount: number, - ): Promise<{ txHash: string }> { - void contractId; - void amount; - return { txHash: '' }; + async getInsurancePolicy( + _contractId: string, + _assetId: string, + ): Promise<{ policyId: string; status: string }> { + return { policyId: '', status: 'none' }; } - async castVote( - contractId: string, - proposalId: string, - vote: boolean, - ): Promise<{ txHash: string }> { - void contractId; - void proposalId; - void vote; - return { txHash: '' }; + async submitKyc( + _userId: string, + _documents: Record, + ): Promise<{ kycId: string; status: string }> { + return { kycId: '', status: 'pending' }; + } + + async getKycStatus( + _userId: string, + ): Promise<{ status: string; verifiedAt?: Date }> { + return { status: 'pending' }; } } diff --git a/backend/src/webhooks/webhook.entity.ts b/backend/src/webhooks/webhook.entity.ts new file mode 100644 index 00000000..55cff8f0 --- /dev/null +++ b/backend/src/webhooks/webhook.entity.ts @@ -0,0 +1,40 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('webhooks') +export class Webhook { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + userId: string; + + @Column() + name: string; + + @Column() + url: string; + + @Column({ nullable: true }) + secretHash: string; + + @Column('simple-array', { default: '' }) + events: string[]; + + @Column({ default: true }) + isActive: boolean; + + @Column({ nullable: true, type: 'timestamp' }) + lastTriggeredAt: Date; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/webhooks/webhooks.controller.ts b/backend/src/webhooks/webhooks.controller.ts new file mode 100644 index 00000000..55b76cde --- /dev/null +++ b/backend/src/webhooks/webhooks.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Get, + Post, + Delete, + Param, + Body, + Req, + UseGuards, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { WebhooksService } from './webhooks.service'; + +@Controller('webhooks') +@UseGuards(AuthGuard('jwt')) +export class WebhooksController { + constructor(private readonly webhooksService: WebhooksService) {} + + @Post() + create( + @Req() req: any, + @Body() + body: { name: string; url: string; events: string[]; secret?: string }, + ) { + return this.webhooksService.create( + req.user?.id, + body.name, + body.url, + body.events, + body.secret, + ); + } + + @Get() + findAll(@Req() req: any) { + return this.webhooksService.findByUser(req.user?.id); + } + + @Delete(':id') + remove(@Param('id') id: string, @Req() req: any) { + return this.webhooksService.remove(id, req.user?.id); + } + + @Post(':id/test') + test(@Param('id') id: string, @Req() req: any) { + return this.webhooksService.test(id, req.user?.id); + } +} diff --git a/backend/src/webhooks/webhooks.module.ts b/backend/src/webhooks/webhooks.module.ts new file mode 100644 index 00000000..24f84174 --- /dev/null +++ b/backend/src/webhooks/webhooks.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Webhook } from './webhook.entity'; +import { WebhooksService } from './webhooks.service'; +import { WebhooksController } from './webhooks.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Webhook])], + controllers: [WebhooksController], + providers: [WebhooksService], + exports: [WebhooksService], +}) +export class WebhooksModule {} diff --git a/backend/src/webhooks/webhooks.service.ts b/backend/src/webhooks/webhooks.service.ts new file mode 100644 index 00000000..c5961d67 --- /dev/null +++ b/backend/src/webhooks/webhooks.service.ts @@ -0,0 +1,59 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { Webhook } from './webhook.entity'; + +@Injectable() +export class WebhooksService { + constructor( + @InjectRepository(Webhook) + private readonly repo: Repository, + ) {} + + async create( + userId: string, + name: string, + url: string, + events: string[], + secret?: string, + ): Promise { + const secretHash = secret ? await bcrypt.hash(secret, 10) : undefined; + const webhook = this.repo.create({ userId, name, url, events, secretHash }); + return this.repo.save(webhook); + } + + async findByUser(userId: string): Promise { + return this.repo.find({ where: { userId, isActive: true } }); + } + + async remove(id: string, userId: string): Promise { + const webhook = await this.repo.findOne({ where: { id, userId } }); + if (!webhook) throw new NotFoundException('Webhook not found'); + webhook.isActive = false; + await this.repo.save(webhook); + } + + async test( + id: string, + userId: string, + ): Promise<{ success: boolean; status?: number }> { + const webhook = await this.repo.findOne({ where: { id, userId } }); + if (!webhook) throw new NotFoundException('Webhook not found'); + try { + const res = await fetch(webhook.url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + event: 'test', + timestamp: new Date().toISOString(), + }), + }); + webhook.lastTriggeredAt = new Date(); + await this.repo.save(webhook); + return { success: res.ok, status: res.status }; + } catch { + return { success: false }; + } + } +}