;
8 |
9 | constructor(private configService: ConfigService) {
10 | this.transporter = nodemailer.createTransport({
11 | host: this.configService.get('SMTP_HOST'),
12 | port: this.configService.get('SMTP_PORT'),
13 | secure: false,
14 | auth: {
15 | user: this.configService.get('SMTP_USER'),
16 | pass: this.configService.get('SMTP_PASS'),
17 | },
18 | });
19 | }
20 |
21 | async sendEmailVerification(email: string, token: string) {
22 | const verificationUrl = `${this.configService.get('FRONTEND_URL')}/verify-email?token=${token}`;
23 |
24 | await this.transporter.sendMail({
25 | from: this.configService.get('FROM_EMAIL'),
26 | to: email,
27 | subject: 'Email Verification',
28 | html: `
29 | Email Verification
30 | Please click the link below to verify your email address:
31 | Verify Email
32 | `,
33 | });
34 | }
35 |
36 | async sendPasswordReset(email: string, token: string) {
37 | const resetUrl = `${this.configService.get('FRONTEND_URL')}/reset-password?token=${token}`;
38 |
39 | await this.transporter.sendMail({
40 | from: this.configService.get('FROM_EMAIL'),
41 | to: email,
42 | subject: 'Password Reset',
43 | html: `
44 | Password Reset
45 | Please click the link below to reset your password:
46 | Reset Password
47 | This link expires in 1 hour.
48 | `,
49 | });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/backend/src/audit/audit.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Post, Body, Query, Delete } from '@nestjs/common';
2 | import { AuditService } from './audit.service';
3 |
4 | @Controller('audit')
5 | export class AuditController {
6 | constructor(private readonly auditService: AuditService) {}
7 |
8 | @Get('search')
9 | async search(
10 | @Query('userId') userId?: string,
11 | @Query('action') action?: string,
12 | @Query('eventType') eventType?: string,
13 | @Query('resource') resource?: string,
14 | @Query('resourceId') resourceId?: string,
15 | @Query('outcome') outcome?: string,
16 | @Query('requestId') requestId?: string,
17 | @Query('from') from?: string,
18 | @Query('to') to?: string,
19 | ) {
20 | const logs = await this.auditService.searchLogs({
21 | userId,
22 | action,
23 | eventType,
24 | resource,
25 | resourceId,
26 | outcome,
27 | requestId,
28 | from: from ? new Date(from) : undefined,
29 | to: to ? new Date(to) : undefined,
30 | });
31 | return { logs };
32 | }
33 |
34 | @Post('anchor')
35 | async anchor(@Body('fromId') fromId?: string) {
36 | return this.auditService.anchorBatchToIpfs(fromId);
37 | }
38 |
39 | @Get('verify')
40 | async verify() {
41 | return this.auditService.verifyChain();
42 | }
43 |
44 | @Delete('purge')
45 | async purge(@Query('before') before?: string) {
46 | const cutoff = before ? new Date(before) : new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
47 | return this.auditService.purgeBefore(cutoff);
48 | }
49 |
50 | @Get('export')
51 | async exportCsv(
52 | @Query('from') from?: string,
53 | @Query('to') to?: string,
54 | ) {
55 | return this.auditService.exportCsv({
56 | from: from ? new Date(from) : undefined,
57 | to: to ? new Date(to) : undefined,
58 | });
59 | }
60 | }
61 |
62 |
63 |
--------------------------------------------------------------------------------
/backend/src/transaction-analysis/entities/analysis-rule.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from "typeorm"
2 |
3 | export enum RuleType {
4 | CATEGORIZATION = "categorization",
5 | FRAUD_DETECTION = "fraud_detection",
6 | RISK_ASSESSMENT = "risk_assessment",
7 | SPENDING_LIMIT = "spending_limit",
8 | PATTERN_DETECTION = "pattern_detection",
9 | }
10 |
11 | export enum RuleStatus {
12 | ACTIVE = "active",
13 | INACTIVE = "inactive",
14 | TESTING = "testing",
15 | }
16 |
17 | @Entity("analysis_rules")
18 | @Index(["ruleType", "status"])
19 | export class AnalysisRule {
20 | @PrimaryGeneratedColumn("uuid")
21 | id: string
22 |
23 | @Column({ unique: true })
24 | name: string
25 |
26 | @Column({ type: "text", nullable: true })
27 | description: string
28 |
29 | @Column({ type: "enum", enum: RuleType, name: "rule_type" })
30 | ruleType: RuleType
31 |
32 | @Column({ type: "enum", enum: RuleStatus, default: RuleStatus.ACTIVE })
33 | status: RuleStatus
34 |
35 | @Column({ type: "jsonb" })
36 | conditions: Record // Rule conditions in JSON format
37 |
38 | @Column({ type: "jsonb" })
39 | actions: Record // Actions to take when rule matches
40 |
41 | @Column({ type: "integer", default: 0 })
42 | priority: number // Rule execution priority
43 |
44 | @Column({ type: "decimal", precision: 5, scale: 4, nullable: true })
45 | threshold: number // Threshold for rule activation
46 |
47 | @Column({ name: "version", default: "1.0.0" })
48 | version: string
49 |
50 | @Column({ name: "created_by" })
51 | createdBy: string
52 |
53 | @Column({ name: "updated_by", nullable: true })
54 | updatedBy: string
55 |
56 | @CreateDateColumn({ name: "created_at" })
57 | createdAt: Date
58 |
59 | @UpdateDateColumn({ name: "updated_at" })
60 | updatedAt: Date
61 | }
62 |
--------------------------------------------------------------------------------
/backend/src/documents/documents.service.spec.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Test, TestingModule } from '@nestjs/testing';
3 | import { DocumentsService } from './documents.service';
4 | import { DocumentProcessingService } from './document-processing.service';
5 |
6 | import { getRepositoryToken } from '@nestjs/typeorm';
7 | import { DocumentProcessing } from './entities/document-processing.entity';
8 | import { getQueueToken } from '@nestjs/bull';
9 |
10 | const mockDocuments = [
11 | { id: 'doc1', userId: 'user1' },
12 | { id: 'doc2', userId: 'user1' },
13 | { id: 'doc3', userId: 'user2' },
14 | ];
15 |
16 | const mockDocumentRepo = {
17 | find: jest.fn(({ where }) => mockDocuments.filter(doc => doc.userId === where.userId)),
18 | delete: jest.fn((id) => Promise.resolve({ affected: 1 })),
19 | };
20 |
21 | describe('DocumentsService', () => {
22 | let service: DocumentsService;
23 |
24 | beforeEach(async () => {
25 | const module: TestingModule = await Test.createTestingModule({
26 | providers: [
27 | DocumentsService,
28 | {
29 | provide: getRepositoryToken(DocumentProcessing),
30 | useValue: mockDocumentRepo,
31 | },
32 | { provide: getQueueToken('document-processing'), useValue: {} },
33 | ],
34 | }).compile();
35 |
36 | service = module.get(DocumentsService);
37 | });
38 |
39 | it('should get all documents for a user', async () => {
40 | const docs = await service.getUserDocuments('user1');
41 | expect(docs).toHaveLength(2);
42 | expect(docs[0].userId).toBe('user1');
43 | });
44 |
45 | it('should delete all documents for a user', async () => {
46 | const result = await service.deleteUserDocuments('user1');
47 | expect(result.status).toBe('deleted');
48 | expect(result.userId).toBe('user1');
49 | expect(mockDocumentRepo.delete).toHaveBeenCalledTimes(2);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Credit Oracle Frontend
2 |
3 | A Next.js application for the Credit Oracle system built on the Flare Network
4 |
5 | ## 🚀 Features
6 |
7 | - **Modern Stack**: Next.js 15, React 19, TypeScript, Tailwind CSS 4
8 | - **Web3 Integration**: Ethers.js and Web3.js for blockchain interactions
9 | - **Flare Network**: Built specifically for Flare Network integration
10 | - **Credit Scoring**: Decentralized credit score verification and display
11 | - **Responsive Design**: Mobile-first approach
12 |
13 | ## 🛠️ Tech Stack
14 |
15 | - **Framework**: Next.js 15.4.5
16 | - **Language**: TypeScript 5+
17 | - **Styling**: Tailwind CSS 4
18 | - **Web3**: Ethers.js 6+ and Web3.js 4+
19 | - **Development**: ESLint, Prettier
20 |
21 | ## 📋 Prerequisites
22 |
23 | - Node.js 18+
24 | - npm or yarn
25 | - Git
26 |
27 | ## 🚀 Getting Started
28 |
29 | ### 1. Clone the repository
30 |
31 | \`\`\`bash
32 | git clone
33 | cd credora/frontend
34 | \`\`\`
35 |
36 | ### 2. Install dependencies
37 |
38 | \`\`\`bash
39 | npm install
40 | \`\`\`
41 |
42 | ### 3. Environment Setup
43 |
44 | Copy the example environment file and configure your variables:
45 |
46 | \`\`\`bash
47 | cp .env.example .env.local
48 | \`\`\`
49 |
50 | Update the environment variables in `.env.local` with your actual values. See `.env.example` for all required variables.
51 |
52 | ### 4. Run the development server
53 |
54 | \`\`\`bash
55 | npm run dev
56 | \`\`\`
57 |
58 | Open [http://localhost:3000](http://localhost:3000) in your browser.
59 |
60 | ## 🔧 Available Scripts
61 |
62 | - `npm run dev` - Start development server
63 | - `npm run build` - Build for production
64 | - `npm run start` - Start production server
65 | - `npm run lint` - Run ESLint
66 | - `npm run format` - Format code with Prettier
67 |
68 | ## 🤝 Contributing
69 |
70 | Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on contributing to this project.
71 |
--------------------------------------------------------------------------------
/backend/bankingAnalysis/banking.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Post, Param, UsePipes, ValidationPipe } from '@nestjs/common';
2 | import { BankingService } from './banking.service';
3 | import { LinkAccountDto } from './dto/link-account.dto';
4 |
5 | @Controller('banking')
6 | export class BankingController {
7 | constructor(private readonly bankingService: BankingService) {}
8 |
9 | /**
10 | * Endpoint for the frontend to get a link_token to initialize Plaid Link.
11 | * In a real app, the userId would come from an authenticated session.
12 | */
13 | @Post('create-link-token/:userId')
14 | createLinkToken(@Param('userId') userId: string) {
15 | return this.bankingService.generateLinkToken(userId);
16 | }
17 |
18 | /**
19 | * Endpoint to link a bank account. The frontend sends the public_token
20 | * it receives from a successful Plaid Link flow.
21 | */
22 | @Post('link-account')
23 | @UsePipes(new ValidationPipe({ whitelist: true }))
24 | linkAccount(@Body() linkAccountDto: LinkAccountDto) {
25 | return this.bankingService.setLinkedAccount(
26 | linkAccountDto.userId,
27 | linkAccountDto.public_token,
28 | );
29 | }
30 |
31 | /**
32 | * Endpoint to fetch transactions for a specific user.
33 | */
34 | @Get('transactions/:userId')
35 | getTransactions(@Param('userId') userId: string) {
36 | return this.bankingService.getUserTransactions(userId);
37 | }
38 |
39 | /**
40 | * Webhook endpoint for Plaid to send real-time transaction updates.
41 | * This endpoint needs to be publicly accessible.
42 | */
43 | @Post('plaid/webhook')
44 | handlePlaidWebhook(@Body() webhookData: any) {
45 | console.log('Received Plaid Webhook:', webhookData);
46 | // TODO: Add logic to handle webhook events, e.g., new transactions, item errors.
47 | // This requires implementing webhook verification for security.
48 | return { status: 'received' };
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/backend/src/screening/services/fuzzy-matching.service.ts:
--------------------------------------------------------------------------------
1 | // src/screening/services/fuzzy-matching.service.ts
2 | @Injectable()
3 | export class FuzzyMatchingService {
4 | calculateSimilarity(str1: string, str2: string): number {
5 | // Levenshtein distance implementation
6 | const matrix = Array(str2.length + 1)
7 | .fill(null)
8 | .map(() => Array(str1.length + 1).fill(null));
9 |
10 | for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
11 | for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
12 |
13 | for (let j = 1; j <= str2.length; j++) {
14 | for (let i = 1; i <= str1.length; i++) {
15 | const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
16 | matrix[j][i] = Math.min(
17 | matrix[j][i - 1] + 1,
18 | matrix[j - 1][i] + 1,
19 | matrix[j - 1][i - 1] + indicator,
20 | );
21 | }
22 | }
23 |
24 | const distance = matrix[str2.length][str1.length];
25 | const maxLength = Math.max(str1.length, str2.length);
26 | return ((maxLength - distance) / maxLength) * 100;
27 | }
28 |
29 | normalizeString(str: string): string {
30 | return str
31 | .toLowerCase()
32 | .replace(/[^\w\s]/g, '')
33 | .replace(/\s+/g, ' ')
34 | .trim();
35 | }
36 |
37 | findMatches(
38 | searchTerm: string,
39 | watchlistEntries: any[],
40 | threshold = 80,
41 | ): any[] {
42 | const normalizedSearch = this.normalizeString(searchTerm);
43 | const matches = [];
44 |
45 | for (const entry of watchlistEntries) {
46 | const normalizedEntry = this.normalizeString(entry.name);
47 | const score = this.calculateSimilarity(normalizedSearch, normalizedEntry);
48 |
49 | if (score >= threshold) {
50 | matches.push({
51 | entry,
52 | score,
53 | matchedField: 'name',
54 | });
55 | }
56 | }
57 |
58 | return matches.sort((a, b) => b.score - a.score);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/dto/api-key.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsOptional, IsArray, IsNumber, IsDateString, IsBoolean } from 'class-validator';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | export class CreateApiKeyDto {
5 | @ApiProperty({ description: 'API key name', required: false })
6 | @IsOptional()
7 | @IsString()
8 | name?: string;
9 |
10 | @ApiProperty({ description: 'API key permissions', required: false })
11 | @IsOptional()
12 | @IsArray()
13 | @IsString({ each: true })
14 | permissions?: string[];
15 |
16 | @ApiProperty({ description: 'Rate limit per period', required: false, default: 1000 })
17 | @IsOptional()
18 | @IsNumber()
19 | rateLimit?: number;
20 |
21 | @ApiProperty({ description: 'Rate limit period', required: false, default: 'hour' })
22 | @IsOptional()
23 | @IsString()
24 | rateLimitPeriod?: string;
25 |
26 | @ApiProperty({ description: 'API key expiration date', required: false })
27 | @IsOptional()
28 | @IsDateString()
29 | expiresAt?: Date;
30 | }
31 |
32 | export class UpdateApiKeyDto {
33 | @ApiProperty({ description: 'API key name', required: false })
34 | @IsOptional()
35 | @IsString()
36 | name?: string;
37 |
38 | @ApiProperty({ description: 'API key permissions', required: false })
39 | @IsOptional()
40 | @IsArray()
41 | @IsString({ each: true })
42 | permissions?: string[];
43 |
44 | @ApiProperty({ description: 'Rate limit per period', required: false })
45 | @IsOptional()
46 | @IsNumber()
47 | rateLimit?: number;
48 |
49 | @ApiProperty({ description: 'Rate limit period', required: false })
50 | @IsOptional()
51 | @IsString()
52 | rateLimitPeriod?: string;
53 |
54 | @ApiProperty({ description: 'API key active status', required: false })
55 | @IsOptional()
56 | @IsBoolean()
57 | isActive?: boolean;
58 |
59 | @ApiProperty({ description: 'API key expiration date', required: false })
60 | @IsOptional()
61 | @IsDateString()
62 | expiresAt?: Date;
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/backend/src/documents/entities/document-processing.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
2 |
3 | export enum DocumentProcessingStatus {
4 | QUEUED = 'queued',
5 | PROCESSING = 'processing',
6 | COMPLETED = 'completed',
7 | FAILED = 'failed',
8 | RETRY = 'retry',
9 | }
10 |
11 | @Entity('document_processing')
12 | export class DocumentProcessing {
13 | @PrimaryGeneratedColumn('uuid')
14 | id: string;
15 |
16 | @Column({ unique: true })
17 | fileId: string;
18 |
19 | @Column({ type: 'enum', enum: DocumentProcessingStatus, default: DocumentProcessingStatus.QUEUED })
20 | status: DocumentProcessingStatus;
21 |
22 | @Column({ type: 'jsonb', nullable: true })
23 | ocrResult?: any;
24 |
25 | @Column({ type: 'varchar', nullable: true })
26 | documentType?: string;
27 |
28 | @Column({ type: 'jsonb', nullable: true })
29 | extractedData?: any;
30 |
31 | @Column({ type: 'jsonb', nullable: true })
32 | authenticityCheck?: any;
33 |
34 | @Column({ type: 'jsonb', nullable: true })
35 | imageQuality?: any;
36 |
37 | @Column({ type: 'jsonb', nullable: true })
38 | faceMatch?: any;
39 |
40 | @Column({ type: 'jsonb', nullable: true })
41 | duplicateCheck?: any;
42 |
43 | // Storage metadata
44 | @Column({ type: 'varchar', nullable: true })
45 | bucket?: string;
46 |
47 | @Column({ type: 'varchar', nullable: true })
48 | objectKey?: string;
49 |
50 | @Column({ type: 'varchar', nullable: true })
51 | thumbnailKey?: string;
52 |
53 | @Column({ type: 'varchar', nullable: true })
54 | mimeType?: string;
55 |
56 | @Column({ type: 'varchar', nullable: true })
57 | etag?: string;
58 |
59 | @Column({ type: 'int', nullable: true })
60 | size?: number;
61 |
62 |
63 | @Column({ type: 'varchar', nullable: false })
64 | userId: string;
65 |
66 | @Column({ type: 'jsonb', nullable: true })
67 | error?: any;
68 |
69 | @Column({ type: 'int', default: 0 })
70 | retryCount: number;
71 |
72 | @CreateDateColumn()
73 | createdAt: Date;
74 |
75 | @UpdateDateColumn()
76 | updatedAt: Date;
77 | }
78 |
--------------------------------------------------------------------------------
/backend/src/users/entities/user.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | Index,
6 | CreateDateColumn,
7 | OneToMany,
8 | UpdateDateColumn,
9 | } from 'typeorm';
10 | import { IsOptional, IsString, IsBoolean, IsObject } from 'class-validator';
11 | import { Exclude } from 'class-transformer';
12 | import { RefreshToken } from './refresh-token.entity';
13 |
14 | export enum UserRole {
15 | USER = 'user',
16 | ADMIN = 'admin',
17 | ANALYST = 'analyst',
18 | OPERATOR = 'operator',
19 | }
20 |
21 | @Entity({ name: 'users' })
22 | export class User {
23 | @PrimaryGeneratedColumn('uuid')
24 | id: string;
25 |
26 |
27 | // Encrypted PII fields
28 | @Column({ type: 'varchar', nullable: true })
29 | encryptedFullName: string;
30 |
31 | @Column({ type: 'varchar', nullable: true })
32 | encryptedEmail: string;
33 |
34 | // Wallet linking
35 | @Column({ type: 'varchar', nullable: true })
36 | walletAddress: string;
37 |
38 | // Profile completion tracking
39 | @Column({ default: false })
40 | profileCompleted: boolean;
41 |
42 | // User preferences (JSON)
43 | @Column({ type: 'json', nullable: true })
44 | preferences: Record;
45 |
46 | @Column({ default: true })
47 | isActive: boolean;
48 |
49 | @Column()
50 | @Exclude()
51 | password: string;
52 |
53 | @Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
54 | role: UserRole;
55 |
56 | @Column({ default: false })
57 | isEmailVerified: boolean;
58 |
59 | @Column({ nullable: true })
60 | emailVerificationToken: string;
61 |
62 | @Column({ nullable: true })
63 | passwordResetToken: string;
64 |
65 | @Column({ nullable: true })
66 | passwordResetExpires: Date;
67 |
68 | @Column({ default: false })
69 | twoFactorEnabled: boolean;
70 |
71 | @Column({ nullable: true })
72 | @Exclude()
73 | twoFactorSecret: string;
74 |
75 | @CreateDateColumn()
76 | createdAt: Date;
77 |
78 | @UpdateDateColumn()
79 | updatedAt: Date;
80 |
81 | @OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user)
82 | refreshTokens: RefreshToken[];
83 | }
84 |
--------------------------------------------------------------------------------
/backend/src/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | // src/users/users.controller.ts
2 | import { Body, Controller, Get, Post, Put, Delete, Param, UseInterceptors } from '@nestjs/common';
3 | import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';
4 | import { UsersService } from './users.service';
5 | import { CreateProfileDto } from './dto/create-profile.dto';
6 | import { UpdateProfileDto } from './dto/update-profile.dto';
7 | import { PreferencesDto } from './dto/preferences.dto';
8 | import { DeactivateProfileDto } from './dto/deactivate-profile.dto';
9 |
10 | @Controller('users')
11 | @UseInterceptors(CacheInterceptor) // Apply the interceptor to the whole controller
12 | export class UsersController {
13 | constructor(private readonly usersService: UsersService) {}
14 |
15 | @Post()
16 | createProfile(@Body() dto: CreateProfileDto) {
17 | return this.usersService.createProfile(dto);
18 | }
19 |
20 | @Get(':id')
21 | @CacheKey('user_profile_response') // Custom cache key for this endpoint
22 | @CacheTTL(300) // Cache response for 5 minutes (300 seconds)
23 | getProfile(@Param('id') id: string) {
24 | return this.usersService.getProfile(id);
25 | }
26 |
27 | @Put(':id')
28 | updateProfile(@Param('id') id: string, @Body() dto: UpdateProfileDto) {
29 | return this.usersService.updateProfile(id, dto);
30 | }
31 |
32 | @Delete(':id')
33 | deleteProfile(@Param('id') id: string) {
34 | return this.usersService.deleteProfile(id);
35 | }
36 |
37 | @Put(':id/preferences')
38 | setPreferences(@Param('id') id: string, @Body() dto: PreferencesDto) {
39 | return this.usersService.setPreferences(id, dto);
40 | }
41 |
42 | @Get(':id/export')
43 | @CacheKey('user_export_response') // A different cache key for the export endpoint
44 | @CacheTTL(300) // Cache response for 5 minutes
45 | exportProfile(@Param('id') id: string) {
46 | return this.usersService.exportProfile(id);
47 | }
48 |
49 | @Put(':id/deactivate')
50 | deactivateProfile(@Param('id') id: string, @Body() dto: DeactivateProfileDto) {
51 | return this.usersService.deactivateProfile(id, dto);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/backend/src/privacy/entities/audit-log.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
2 |
3 | @Entity('audit_log')
4 | export class AuditLog {
5 | @PrimaryGeneratedColumn('uuid')
6 | id: string;
7 |
8 | @Index()
9 | @Column()
10 | userId: string;
11 |
12 | @Column()
13 | action: string;
14 |
15 | @Column('json', { nullable: true })
16 | details: any;
17 |
18 | // High-level classification of event (e.g., user.activity, data.access, system.event, compliance.event)
19 | @Index()
20 | @Column({ default: 'user.activity' })
21 | eventType: string;
22 |
23 | // Target resource info for data access tracking
24 | @Index()
25 | @Column({ nullable: true })
26 | resource?: string;
27 |
28 | @Index()
29 | @Column({ nullable: true })
30 | resourceId?: string;
31 |
32 | // Result of the action (success/failure)
33 | @Index()
34 | @Column({ default: 'success' })
35 | outcome: string;
36 |
37 | // Request context
38 | @Column({ nullable: true })
39 | ip?: string;
40 |
41 | @Column({ nullable: true })
42 | userAgent?: string;
43 |
44 | @Index()
45 | @Column({ nullable: true })
46 | requestId?: string;
47 |
48 | @Column({ nullable: true })
49 | sessionId?: string;
50 |
51 | // Actor and service context
52 | @Index()
53 | @Column({ default: 'user' })
54 | actorType: string; // user | system | service
55 |
56 | @Index()
57 | @Column({ nullable: true })
58 | service?: string;
59 |
60 | // Tamper-evident chain
61 | @Index()
62 | @Column({ type: 'varchar', length: 128, nullable: true })
63 | prevHash?: string;
64 |
65 | @Index()
66 | @Column({ type: 'varchar', length: 128, nullable: true })
67 | hash?: string;
68 |
69 | // Anchoring/immutability
70 | @Index()
71 | @Column({ nullable: true })
72 | anchorCid?: string;
73 |
74 | // Sequencing within a day or stream
75 | @Index()
76 | @Column({ type: 'int', default: 0 })
77 | sequenceNumber: number;
78 |
79 | // Retention control
80 | @Index()
81 | @Column({ type: 'timestamptz', nullable: true })
82 | retentionUntil?: Date;
83 |
84 | @CreateDateColumn()
85 | timestamp: Date;
86 | }
87 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/interceptors/transformation.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | Logger,
7 | } from '@nestjs/common';
8 | import { Reflector } from '@nestjs/core';
9 | import { Observable } from 'rxjs';
10 | import { map } from 'rxjs/operators';
11 | import { TransformationService, TransformationRule } from '../services/transformation.service';
12 |
13 | export const TRANSFORMATION_RULES = 'transformationRules';
14 | export const Transform = (rules: {
15 | request?: TransformationRule[];
16 | response?: TransformationRule[];
17 | }) => Reflect.metadata(TRANSFORMATION_RULES, rules);
18 |
19 | @Injectable()
20 | export class TransformationInterceptor implements NestInterceptor {
21 | private readonly logger = new Logger(TransformationInterceptor.name);
22 |
23 | constructor(
24 | private readonly reflector: Reflector,
25 | private readonly transformationService: TransformationService,
26 | ) {}
27 |
28 | intercept(context: ExecutionContext, next: CallHandler): Observable {
29 | const request = context.switchToHttp().getRequest();
30 |
31 | const transformationRules = this.reflector.get<{
32 | request?: TransformationRule[];
33 | response?: TransformationRule[];
34 | }>(TRANSFORMATION_RULES, context.getHandler());
35 |
36 | if (transformationRules?.request && request.body) {
37 | try {
38 | request.body = this.transformationService.transformRequest(
39 | request.body,
40 | transformationRules.request,
41 | );
42 | } catch (error) {
43 | this.logger.error('Request transformation failed', error);
44 | }
45 | }
46 |
47 | if (transformationRules?.response) {
48 | return next.handle().pipe(
49 | map(data => {
50 | try {
51 | return this.transformationService.transformResponse(
52 | data,
53 | transformationRules.response!,
54 | );
55 | } catch (error) {
56 | this.logger.error('Response transformation failed', error);
57 | return data;
58 | }
59 | }),
60 | );
61 | }
62 |
63 | return next.handle();
64 | }
65 | }
--------------------------------------------------------------------------------
/contracts/tests/mocks/MockContracts.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | /**
5 | * @title MockFlareContractRegistry
6 | * @dev Mock contract for testing Flare contract registry functionality
7 | */
8 | contract MockFlareContractRegistry {
9 | mapping(string => address) private contracts;
10 |
11 | function setContractAddress(string calldata _name, address _address) external {
12 | contracts[_name] = _address;
13 | }
14 |
15 | function getContractAddressByName(string calldata _name) external view returns (address) {
16 | return contracts[_name];
17 | }
18 | }
19 |
20 | /**
21 | * @title MockFtsoRegistry
22 | * @dev Mock contract for testing FTSO price feeds
23 | */
24 | contract MockFtsoRegistry {
25 | mapping(string => uint256) private prices;
26 | mapping(string => uint256) private timestamps;
27 |
28 | function setPrice(string calldata _symbol, uint256 _price) external {
29 | prices[_symbol] = _price;
30 | timestamps[_symbol] = block.timestamp;
31 | }
32 |
33 | function getCurrentPrice(string calldata _symbol) external view returns (uint256 _price, uint256 _timestamp) {
34 | return (prices[_symbol], timestamps[_symbol]);
35 | }
36 | }
37 |
38 | /**
39 | * @title MockStateConnector
40 | * @dev Mock contract for testing State Connector attestations
41 | */
42 | contract MockStateConnector {
43 | mapping(bytes32 => bool) private attestationProofs;
44 | mapping(bytes32 => bytes) private attestationData;
45 | uint256 private requestCounter;
46 |
47 | function setAttestation(bytes32 _attestationId, bool _proved, bytes calldata _data) external {
48 | attestationProofs[_attestationId] = _proved;
49 | attestationData[_attestationId] = _data;
50 | }
51 |
52 | function requestAttestation(bytes calldata _attestationRequest) external returns (bytes32) {
53 | requestCounter++;
54 | return keccak256(abi.encodePacked(_attestationRequest, requestCounter, block.timestamp));
55 | }
56 |
57 | function getAttestation(bytes32 _attestationId) external view returns (bool _proved, bytes memory _data) {
58 | return (attestationProofs[_attestationId], attestationData[_attestationId]);
59 | }
60 | }
--------------------------------------------------------------------------------
/backend/src/api-gateway/scripts/initialize-endpoints.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from '../../app.module';
3 | import { ApiGatewayService } from '../api-gateway.service';
4 |
5 | async function initializeDefaultEndpoints() {
6 | const app = await NestFactory.createApplicationContext(AppModule);
7 | const apiGatewayService = app.get(ApiGatewayService);
8 |
9 | const defaultEndpoints = [
10 | {
11 | path: '/auth/login',
12 | method: 'POST',
13 | version: 'v1',
14 | targetUrl: 'http://localhost:3000/auth/login',
15 | rateLimitConfig: {
16 | windowMs: 900000,
17 | maxRequests: 5,
18 | },
19 | },
20 | {
21 | path: '/users/:id',
22 | method: 'GET',
23 | version: 'v1',
24 | targetUrl: 'http://localhost:3000/users',
25 | transformationRules: {
26 | response: [
27 | { type: 'filter', field: 'password' },
28 | { type: 'rename', source: 'id', target: 'userId' },
29 | ],
30 | },
31 | },
32 | {
33 | path: '/banking/accounts',
34 | method: 'GET',
35 | version: 'v1',
36 | targetUrl: 'http://localhost:3000/banking/accounts',
37 | circuitBreakerConfig: {
38 | failureThreshold: 3,
39 | successThreshold: 2,
40 | timeout: 30000,
41 | },
42 | },
43 | {
44 | path: '/screening/check',
45 | method: 'POST',
46 | version: 'v1',
47 | targetUrl: 'http://localhost:3000/screening/check',
48 | rateLimitConfig: {
49 | windowMs: 60000,
50 | maxRequests: 10,
51 | circuitBreakerConfig: {
52 | failureThreshold: 5,
53 | successThreshold: 3,
54 | timeout: 60000,
55 | },
56 | },
57 | },
58 | ];
59 |
60 | for (const endpoint of defaultEndpoints) {
61 | try {
62 | await apiGatewayService.registerEndpoint(endpoint);
63 | console.log(` Registered endpoint: ${endpoint.method} ${endpoint.path}`);
64 | } catch (error) {
65 | console.error(` Failed to register endpoint: ${endpoint.method} ${endpoint.path}`, error.message);
66 | }
67 | }
68 |
69 | await app.close();
70 | console.log(' Default endpoints initialization completed');
71 | }
--------------------------------------------------------------------------------
/backend/src/verification/verification.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import axios from 'axios';
3 | import { VerificationRequestDto } from './dto/verification-request.dto';
4 | import { VerificationResponseDto } from './dto/verification-response.dto';
5 | import * as crypto from 'crypto';
6 |
7 | @Injectable()
8 | export class VerificationService {
9 | private jumioApiKey = process.env.JUMIO_API_KEY;
10 | private onfidoApiKey = process.env.ONFIDO_API_KEY;
11 |
12 | // Initiate verification with Jumio using the VerificationRequestDto
13 | async verifyWithJumio(verificationData: VerificationRequestDto): Promise {
14 | const response = await axios.post(
15 | 'https://api.jumio.com/verify',
16 | verificationData,
17 | {
18 | headers: {
19 | Authorization: `Bearer ${this.jumioApiKey}`,
20 | },
21 | },
22 | );
23 | return response.data; // Return data in the shape of VerificationResponseDto
24 | }
25 |
26 | // Initiate verification with Onfido using the VerificationRequestDto
27 | async verifyWithOnfido(verificationData: VerificationRequestDto): Promise {
28 | const response = await axios.post(
29 | 'https://api.onfido.com/v3.6/checks',
30 | verificationData,
31 | {
32 | headers: {
33 | Authorization: `Bearer ${this.onfidoApiKey}`,
34 | },
35 | },
36 | );
37 | return response.data; // Return data in the shape of VerificationResponseDto
38 | }
39 |
40 | // Multi-provider fallback logic
41 | async performVerification(verificationData: VerificationRequestDto): Promise {
42 | try {
43 | const jumioResult = await this.verifyWithJumio(verificationData);
44 | if (jumioResult.verificationStatus === 'verified') {
45 | return jumioResult;
46 | }
47 | } catch (error) {
48 | console.error('Jumio failed, falling back to Onfido');
49 | }
50 |
51 | try {
52 | const onfidoResult = await this.verifyWithOnfido(verificationData);
53 | if (onfidoResult.verificationStatus === 'verified') {
54 | return onfidoResult;
55 | }
56 | } catch (error) {
57 | console.error('Both providers failed');
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/frontend/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Credit Oracle Frontend
2 |
3 | Thank you for your interest in contributing to the Credit Oracle Frontend!
4 |
5 | ## 🚀 Getting Started
6 |
7 | ### Prerequisites
8 |
9 | - Node.js 18+
10 | - npm or yarn
11 | - Git
12 | - Basic knowledge of Next.js, React, and TypeScript
13 |
14 | ### Development Setup
15 |
16 | 1. Fork the repository
17 | 2. Clone your fork: `git clone `
18 | 3. Navigate to frontend: `cd credora/frontend`
19 | 4. Install dependencies: `npm install`
20 | 5. Copy environment file: `cp .env.example .env.local`
21 | 6. Start development server: `npm run dev`
22 |
23 | ## 📋 Development Guidelines
24 |
25 | ### Code Style
26 |
27 | We use ESLint and Prettier for consistent code formatting:
28 |
29 | \`\`\`bash
30 | npm run lint # Check linting
31 | npm run format # Format code
32 | \`\`\`
33 |
34 | ### Best Practices
35 |
36 | - Use TypeScript with proper types
37 | - Follow Next.js App Router conventions
38 | - Use Tailwind CSS utilities
39 | - Maintain responsive design (mobile-first)
40 | - Handle Web3 wallet connections properly
41 | - Implement proper error handling
42 |
43 | ## 🔄 Workflow
44 |
45 | ### Branch Naming
46 |
47 | - `feature/description` - New features
48 | - `fix/description` - Bug fixes
49 | - `docs/description` - Documentation updates
50 |
51 | ### Pull Request Process
52 |
53 | 1. Create a feature branch from `main`
54 | 2. Make your changes following the guidelines
55 | 3. Test your changes thoroughly
56 | 4. Run `npm run lint` and `npm run format`
57 | 5. Submit a pull request with clear description
58 |
59 | ## 🧪 Before Submitting
60 |
61 | \`\`\`bash
62 | npm run lint # Check for linting errors
63 | npm run format # Format code
64 | npm run build # Ensure build succeeds
65 | \`\`\`
66 |
67 | ## 📞 Getting Help
68 |
69 | If you have any issues or questions while contributing, please reach out to the project maintainer by contacting them directly.
70 |
71 | ## 💡 Feature Requests
72 |
73 | For new features:
74 |
75 | 1. Check issues Descriptions first
76 | 2. Provide clear use case description
77 | 3. Discuss with maintainers before starting if any questions
78 |
79 | Thank you for contributing to Credit Oracle Frontend! 🚀
80 |
--------------------------------------------------------------------------------
/backend/src/audit/audit.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | } from '@nestjs/common';
7 | import { Observable } from 'rxjs';
8 | import { tap, catchError } from 'rxjs/operators';
9 | import { AuditService } from './audit.service';
10 | import type { CreateAuditLogInput } from './audit.service';
11 |
12 | @Injectable()
13 | export class AuditInterceptor implements NestInterceptor {
14 | constructor(private readonly auditService: AuditService) {}
15 |
16 | intercept(context: ExecutionContext, next: CallHandler): Observable {
17 | const req = context.switchToHttp().getRequest();
18 | const userId: string | undefined = req.user?.id || req.user?.userId || undefined;
19 | const requestId: string | undefined = (req.headers['x-request-id'] as string) || (req.id as string | undefined);
20 | const sessionId: string | undefined = req.session?.id as string | undefined;
21 | const ip = (req.ip as string) || (req.headers['x-forwarded-for'] as string) || (req.connection?.remoteAddress as string | undefined);
22 | const userAgent = req.headers['user-agent'] as string | undefined;
23 | const action = `${req.method} ${req.route?.path || req.url}`;
24 | const resource = (req.route?.path || req.path) as string | undefined;
25 |
26 | const common: CreateAuditLogInput = {
27 | userId,
28 | action,
29 | eventType: 'user.activity',
30 | resource,
31 | outcome: 'success',
32 | ip: typeof ip === 'string' ? ip : Array.isArray(ip as any) ? (ip as any)[0] : undefined,
33 | userAgent,
34 | requestId: typeof requestId === 'string' ? requestId : undefined,
35 | sessionId,
36 | actorType: (userId ? 'user' : 'system'),
37 | details: { params: req.params, query: req.query, bodyKeys: Object.keys(req.body || {}) },
38 | };
39 |
40 | return next.handle().pipe(
41 | tap(async () => {
42 | try {
43 | await this.auditService.recordLog(common);
44 | } catch {}
45 | }),
46 | catchError((err, caught) => {
47 | // Record failure outcome
48 | try {
49 | this.auditService.recordLog({ ...common, outcome: 'failure', details: { error: err?.message } });
50 | } catch {}
51 | throw err;
52 | }) as any,
53 | );
54 | }
55 | }
56 |
57 |
58 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/api-gateway.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { RedisModule } from '../redis/redis.module';
4 | import { ApiGatewayController } from './api-gateway.controller';
5 | import { ApiGatewayService } from './api-gateway.service';
6 | import { RateLimitService } from './services/rate-limit.service';
7 | import { LoadBalancerService } from './services/load-balancer.service';
8 | import { AnalyticsService } from './services/analytics.service';
9 | import { ApiKeyService } from './services/api-key.service';
10 | import { TransformationService } from './services/transformation.service';
11 | import { CircuitBreakerService } from './services/circuit-breaker.service';
12 | import { ApiKey } from './entities/api-key.entity';
13 | import { ApiUsage } from './entities/api-usage.entity';
14 | import { ApiEndpoint } from './entities/api-endpoint.entity';
15 | import { ServiceHealth } from './entities/service-health.entity';
16 | import { RateLimitGuard } from './guards/rate-limit.guard';
17 | import { ApiKeyGuard } from './guards/api-key.guard';
18 | import { AnalyticsInterceptor } from './interceptors/analytics.interceptor';
19 | import { TransformationInterceptor } from './interceptors/transformation.interceptor';
20 | import { CircuitBreakerInterceptor } from './interceptors/circuit-breaker.interceptor';
21 | import { HealthMonitorService } from './services/health-monitor.service';
22 | import { ScheduleModule } from '@nestjs/schedule';
23 |
24 | @Module({
25 | imports: [
26 | TypeOrmModule.forFeature([
27 | ApiKey,
28 | ApiUsage,
29 | ApiEndpoint,
30 | ServiceHealth,
31 | ]),
32 | RedisModule,
33 | ScheduleModule.forRoot(),
34 | ],
35 | controllers: [ApiGatewayController],
36 | providers: [
37 | ApiGatewayService,
38 | RateLimitService,
39 | LoadBalancerService,
40 | AnalyticsService,
41 | ApiKeyService,
42 | TransformationService,
43 | CircuitBreakerService,
44 | RateLimitGuard,
45 | ApiKeyGuard,
46 | HealthMonitorService,
47 | AnalyticsInterceptor,
48 | TransformationInterceptor,
49 | CircuitBreakerInterceptor,
50 | ],
51 | exports: [
52 | ApiGatewayService,
53 | RateLimitService,
54 | AnalyticsService,
55 | ApiKeyService,
56 | ],
57 | })
58 | export class ApiGatewayModule {}
--------------------------------------------------------------------------------
/backend/src/transaction-analysis/transaction-analysis.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from "@nestjs/common"
2 | import { TypeOrmModule } from "@nestjs/typeorm"
3 |
4 | // Entities
5 | import { Transaction, TransactionAnalysis, UserFinancialProfile, AnalysisRule } from "./entities"
6 |
7 | // Services
8 | import { TransactionAnalysisService } from "./services/transaction-analysis.service"
9 | import { CategorizationService } from "./services/categorization.service"
10 | import { SpendingPatternService } from "./services/spending-pattern.service"
11 | import { IncomeStabilityService } from "./services/income-stability.service"
12 | import { CashFlowService } from "./services/cash-flow.service"
13 | import { BehavioralScoringService } from "./services/behavioral-scoring.service"
14 | import { FraudDetectionService } from "./services/fraud-detection.service"
15 | import { RiskAssessmentService } from "./services/risk-assessment.service"
16 | import { TimeSeriesAnalysisService } from "./services/time-series-analysis.service"
17 | import { RuleEngineService } from "./services/rule-engine.service"
18 | import { RuleConditionEvaluator } from "./services/rule-condition-evaluator.service"
19 | import { RuleActionExecutor } from "./services/rule-action-executor.service"
20 |
21 | // Controllers
22 | import { TransactionAnalysisController } from "./controllers/transaction-analysis.controller"
23 | import { RuleManagementController } from "./controllers/rule-management.controller"
24 | import { UserProfileController } from "./controllers/user-profile.controller"
25 |
26 | @Module({
27 | imports: [TypeOrmModule.forFeature([Transaction, TransactionAnalysis, UserFinancialProfile, AnalysisRule])],
28 | controllers: [TransactionAnalysisController, RuleManagementController, UserProfileController],
29 | providers: [
30 | // Core Analysis Services
31 | TransactionAnalysisService,
32 | CategorizationService,
33 | SpendingPatternService,
34 | IncomeStabilityService,
35 | CashFlowService,
36 | BehavioralScoringService,
37 | FraudDetectionService,
38 | RiskAssessmentService,
39 | TimeSeriesAnalysisService,
40 |
41 | // Rule Engine Services
42 | RuleEngineService,
43 | RuleConditionEvaluator,
44 | RuleActionExecutor,
45 | ],
46 | exports: [
47 | TransactionAnalysisService,
48 | RuleEngineService,
49 | CategorizationService,
50 | FraudDetectionService,
51 | RiskAssessmentService,
52 | ],
53 | })
54 | export class TransactionAnalysisModule {}
55 |
--------------------------------------------------------------------------------
/backend/src/transaction-analysis/entities/transaction-analysis.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | ManyToOne,
8 | JoinColumn,
9 | Index,
10 | } from "typeorm"
11 | import { Transaction } from "./transaction.entity"
12 |
13 | export enum AnalysisType {
14 | CATEGORIZATION = "categorization",
15 | SPENDING_PATTERN = "spending_pattern",
16 | INCOME_STABILITY = "income_stability",
17 | CASH_FLOW = "cash_flow",
18 | DEBT_TO_INCOME = "debt_to_income",
19 | BEHAVIORAL_SCORING = "behavioral_scoring",
20 | FRAUD_DETECTION = "fraud_detection",
21 | RISK_ASSESSMENT = "risk_assessment",
22 | TREND_ANALYSIS = "trend_analysis",
23 | }
24 |
25 | export enum RiskLevel {
26 | LOW = "low",
27 | MEDIUM = "medium",
28 | HIGH = "high",
29 | CRITICAL = "critical",
30 | }
31 |
32 | @Entity("transaction_analyses")
33 | @Index(["transactionId", "analysisType"])
34 | @Index(["userId", "analysisType", "createdAt"])
35 | export class TransactionAnalysis {
36 | @PrimaryGeneratedColumn("uuid")
37 | id: string
38 |
39 | @Column({ name: "transaction_id" })
40 | transactionId: string
41 |
42 | @Column({ name: "user_id" })
43 | @Index()
44 | userId: string
45 |
46 | @Column({ type: "enum", enum: AnalysisType, name: "analysis_type" })
47 | analysisType: AnalysisType
48 |
49 | @Column({ type: "decimal", precision: 5, scale: 4, nullable: true })
50 | confidence: number // ML confidence score (0-1)
51 |
52 | @Column({ type: "enum", enum: RiskLevel, name: "risk_level", nullable: true })
53 | riskLevel: RiskLevel
54 |
55 | @Column({ type: "decimal", precision: 10, scale: 2, nullable: true })
56 | score: number // Numerical score for the analysis
57 |
58 | @Column({ type: "jsonb" })
59 | result: Record // Detailed analysis results
60 |
61 | @Column({ type: "jsonb", nullable: true })
62 | features: Record // ML features used
63 |
64 | @Column({ name: "model_version", nullable: true })
65 | modelVersion: string
66 |
67 | @Column({ name: "rule_version", nullable: true })
68 | ruleVersion: string
69 |
70 | @Column({ type: "text", nullable: true })
71 | notes: string
72 |
73 | @CreateDateColumn({ name: "created_at" })
74 | createdAt: Date
75 |
76 | @UpdateDateColumn({ name: "updated_at" })
77 | updatedAt: Date
78 |
79 | @ManyToOne(
80 | () => Transaction,
81 | (transaction) => transaction.analyses,
82 | )
83 | @JoinColumn({ name: "transaction_id" })
84 | transaction: Transaction
85 | }
86 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/api-gateway.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { getRepositoryToken } from '@nestjs/typeorm';
3 | import { ApiGatewayService } from './api-gateway.service';
4 | import { RateLimitService } from './services/rate-limit.service';
5 | import { ApiKeyService } from './services/api-key.service';
6 | import { AnalyticsService } from './services/analytics.service';
7 | import { ApiKey } from './entities/api-key.entity';
8 | import { ApiUsage } from './entities/api-usage.entity';
9 | import { ApiEndpoint } from './entities/api-endpoint.entity';
10 |
11 | describe('API Gateway', () => {
12 | let service: ApiGatewayService;
13 | let apiKeyService: ApiKeyService;
14 | let rateLimitService: RateLimitService;
15 |
16 | beforeEach(async () => {
17 | const module: TestingModule = await Test.createTestingModule({
18 | providers: [
19 | ApiGatewayService,
20 | ApiKeyService,
21 | RateLimitService,
22 | AnalyticsService,
23 | {
24 | provide: getRepositoryToken(ApiKey),
25 | useValue: {
26 | create: jest.fn(),
27 | save: jest.fn(),
28 | findOne: jest.fn(),
29 | find: jest.fn(),
30 | delete: jest.fn(),
31 | },
32 | },
33 | {
34 | provide: getRepositoryToken(ApiUsage),
35 | useValue: {
36 | create: jest.fn(),
37 | save: jest.fn(),
38 | find: jest.fn(),
39 | count: jest.fn(),
40 | },
41 | },
42 | {
43 | provide: getRepositoryToken(ApiEndpoint),
44 | useValue: {
45 | create: jest.fn(),
46 | save: jest.fn(),
47 | findOne: jest.fn(),
48 | find: jest.fn(),
49 | delete: jest.fn(),
50 | },
51 | },
52 | {
53 | provide: 'REDIS_SERVICE',
54 | useValue: {
55 | get: jest.fn(),
56 | set: jest.fn(),
57 | incr: jest.fn(),
58 | expire: jest.fn(),
59 | },
60 | },
61 | ],
62 | }).compile();
63 |
64 | service = module.get(ApiGatewayService);
65 | apiKeyService = module.get(ApiKeyService);
66 | rateLimitService = module.get(RateLimitService);
67 | });
68 |
69 | it('should be defined', () => {
70 | expect(service).toBeDefined();
71 | expect(apiKeyService).toBeDefined();
72 | expect(rateLimitService).toBeDefined();
73 | });
74 |
75 | });
76 |
77 |
--------------------------------------------------------------------------------
/backend/src/credit-bureaus/credit-bureau.adapter.ts:
--------------------------------------------------------------------------------
1 | // Import axios using require syntax to avoid TypeScript issues
2 | const axios = require('axios');
3 |
4 | /**
5 | * Normalized structure for credit reports from different bureaus
6 | */
7 | export interface NormalizedCreditReport {
8 | userId: string;
9 | bureauSource: string;
10 | score: number;
11 | scoreRange: {
12 | min: number;
13 | max: number;
14 | };
15 | accounts: Array<{
16 | type: string;
17 | balance: number;
18 | paymentStatus: string;
19 | accountNumber: string;
20 | }>;
21 | inquiries: Array<{
22 | date: Date;
23 | type: string;
24 | requestor: string;
25 | }>;
26 | publicRecords: Array<{
27 | type: string;
28 | date: Date;
29 | amount?: number;
30 | status: string;
31 | }>;
32 | rawData?: any; // Original data (may be stored encrypted)
33 | }
34 |
35 | /**
36 | * Interface that all credit bureau adapters must implement
37 | */
38 | export interface CreditBureauAdapter {
39 | getCreditReport(userId: string, extra?: any): Promise;
40 | handleWebhook(payload: any): Promise;
41 | }
42 |
43 | /**
44 | * Base class for credit bureau adapters with common functionality
45 | */
46 | export abstract class BaseCreditBureauAdapter implements CreditBureauAdapter {
47 | protected axios: any; // Axios instance
48 | protected maxRetries = 3;
49 |
50 | constructor(axios: any) {
51 | this.axios = axios;
52 | }
53 |
54 | abstract getCreditReport(userId: string, extra?: any): Promise;
55 | abstract handleWebhook(payload: any): Promise;
56 |
57 | /**
58 | * Helper method to normalize bureau-specific fields to common format
59 | */
60 | protected normalizeData(rawData: any): Partial {
61 | // This is a base implementation that specific adapters can extend
62 | return {
63 | rawData
64 | };
65 | }
66 |
67 | /**
68 | * Helper method for retrying failed API calls
69 | */
70 | protected async withRetry(
71 | operation: () => Promise,
72 | retries = this.maxRetries
73 | ): Promise {
74 | try {
75 | return await operation();
76 | } catch (error) {
77 | if (retries <= 0) {
78 | throw error;
79 | }
80 |
81 | // Exponential backoff: 2^retryAttempt * 100ms
82 | const delay = Math.pow(2, this.maxRetries - retries) * 100;
83 | await new Promise(resolve => setTimeout(resolve, delay));
84 |
85 | return this.withRetry(operation, retries - 1);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/backend/bankingAnalysis/PlaidService.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { PlaidApi, Configuration, PlaidEnvironments, CountryCode, Products } from 'plaid';
4 |
5 | @Injectable()
6 | export class PlaidService {
7 | private readonly plaidClient: PlaidApi;
8 |
9 | constructor(private configService: ConfigService) {
10 | const configuration = new Configuration({
11 | basePath: PlaidEnvironments[this.configService.get('PLAID_ENV')],
12 | baseOptions: {
13 | headers: {
14 | 'PLAID-CLIENT-ID': this.configService.get('PLAID_CLIENT_ID'),
15 | 'PLAID-SECRET': this.configService.get('PLAID_SECRET_SANDBOX'),
16 | },
17 | },
18 | });
19 |
20 | this.plaidClient = new PlaidApi(configuration);
21 | }
22 |
23 | /**
24 | * Creates a link_token required to initialize the Plaid Link flow on the frontend.
25 | * This token authenticates our app with Plaid and customizes the Link experience.
26 | * @param userId The ID of the user initiating the connection.
27 | */
28 | async createLinkToken(userId: string) {
29 | const request = {
30 | user: {
31 | client_user_id: userId,
32 | },
33 | client_name: 'My Banking App',
34 | products: [Products.Auth, Products.Transactions],
35 | country_codes: [CountryCode.Us],
36 | language: 'en',
37 | webhook: 'https://your-api-url.com/banking/plaid/webhook', // A public URL for Plaid to send updates
38 | };
39 |
40 | const response = await this.plaidClient.linkTokenCreate(request);
41 | return response.data;
42 | }
43 |
44 | /**
45 | * Exchanges a temporary public_token (from the frontend) for a permanent access_token.
46 | * This access_token is what we use to make API calls on behalf of the user.
47 | * @param publicToken The temporary token from Plaid Link.
48 | */
49 | async exchangePublicToken(publicToken: string) {
50 | const response = await this.plaidClient.itemPublicTokenExchange({
51 | public_token: publicToken,
52 | });
53 | return response.data; // Contains access_token and item_id
54 | }
55 |
56 | /**
57 | * Fetches transaction data for a linked bank account.
58 | * @param accessToken The permanent token for the user's item.
59 | */
60 | async getTransactions(accessToken: string) {
61 | const response = await this.plaidClient.transactionsGet({
62 | access_token: accessToken,
63 | start_date: '2024-01-01', // Example start date
64 | end_date: '2024-12-31', // Example end date
65 | });
66 | return response.data;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "credora",
3 | "version": "1.0.0",
4 | "description": "Credora is a decentralized identity and credit scoring protocol built on the Flare network. It enables users to verify their identity, build on-chain/off-chain reputation, and interact with DeFi applications in a privacy-preserving, composable way.",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "npx hardhat compile",
8 | "test": "npx hardhat test",
9 | "test:coverage": "npx hardhat coverage",
10 | "test:gas": "REPORT_GAS=true npx hardhat test",
11 | "deploy:local": "npx hardhat run scripts/deploy.ts --network localhost",
12 | "deploy:flare": "npx hardhat run scripts/deploy.ts --network flare",
13 | "typechain": "npx hardhat typechain",
14 | "lint": "eslint . --ext .ts",
15 | "lint:fix": "eslint . --ext .ts --fix",
16 | "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
17 | "dev:backend": "npm run start:dev --workspace=backend",
18 | "dev:frontend": "npm run dev --workspace=frontend",
19 | "install:all": "npm install && npm install --workspace=backend && npm install --workspace=frontend"
20 | },
21 | "keywords": [],
22 | "author": "",
23 | "license": "ISC",
24 | "devDependencies": {
25 | "@nomicfoundation/hardhat-toolbox": "^6.1.0",
26 | "@openzeppelin/hardhat-upgrades": "^3.9.1",
27 | "concurrently": "^9.2.0",
28 | "hardhat": "^0.0.7"
29 | },
30 | "dependencies": {
31 | "@flarenetwork/flare-periphery-contract-artifacts": "^0.1.34",
32 | "@flarenetwork/flare-periphery-contracts": "^0.1.34",
33 | "@flarenetwork/flarejs": "^4.0.5",
34 | "@nestjs/common": "^11.1.6",
35 | "@nestjs/typeorm": "^11.0.0",
36 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
37 | "@nomicfoundation/hardhat-ethers": "^3.0.0",
38 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
39 | "@nomicfoundation/hardhat-toolbox": "^4.0.0",
40 | "@nomicfoundation/hardhat-verify": "^2.0.0",
41 | "@openzeppelin/contracts": "^5.4.0",
42 | "@typechain/ethers-v6": "^0.5.0",
43 | "@typechain/hardhat": "^9.0.0",
44 | "@types/chai": "^4.2.0",
45 | "@types/mocha": ">=9.1.0",
46 | "@types/node": ">=16.0.0",
47 | "@typescript-eslint/eslint-plugin": "^6.0.0",
48 | "@typescript-eslint/parser": "^6.0.0",
49 | "chai": "^4.2.0",
50 | "dotenv": "^17.2.1",
51 | "eslint": "^8.0.0",
52 | "ethers": "^6.4.0",
53 | "hardhat": "^2.19.0",
54 | "hardhat-gas-reporter": "^1.0.8",
55 | "solidity-coverage": "^0.8.1",
56 | "ts-node": ">=8.0.0",
57 | "typechain": "^8.3.0",
58 | "typeorm": "^0.3.25",
59 | "typescript": ">=4.5.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/interceptors/circuit-breaker.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | Logger,
7 | ServiceUnavailableException,
8 | } from '@nestjs/common';
9 | import { Reflector } from '@nestjs/core';
10 | import { Observable, throwError, from } from 'rxjs';
11 | import { tap, catchError, mergeMap } from 'rxjs/operators';
12 | import { CircuitBreakerService, CircuitBreakerConfig } from '../services/circuit-breaker.service';
13 |
14 | export const CIRCUIT_BREAKER_CONFIG = 'circuitBreakerConfig';
15 | export const CircuitBreaker = (config: CircuitBreakerConfig) =>
16 | Reflect.metadata(CIRCUIT_BREAKER_CONFIG, config);
17 |
18 | @Injectable()
19 | export class CircuitBreakerInterceptor implements NestInterceptor {
20 | private readonly logger = new Logger(CircuitBreakerInterceptor.name);
21 |
22 | constructor(
23 | private readonly reflector: Reflector,
24 | private readonly circuitBreakerService: CircuitBreakerService,
25 | ) {}
26 |
27 | intercept(context: ExecutionContext, next: CallHandler): Observable {
28 | const request = context.switchToHttp().getRequest();
29 | const serviceName = this.getServiceName(context, request);
30 |
31 | const config = this.reflector.get(
32 | CIRCUIT_BREAKER_CONFIG,
33 | context.getHandler(),
34 | );
35 |
36 | return from(this.circuitBreakerService.checkCircuitBreaker(serviceName, config)).pipe(
37 | mergeMap(isAllowed => {
38 | if (!isAllowed) {
39 | this.logger.warn(`Circuit breaker is OPEN for service: ${serviceName}`);
40 | return throwError(() => new ServiceUnavailableException('Service is temporarily unavailable'));
41 | }
42 |
43 | return next.handle().pipe(
44 | tap(() => {
45 | this.circuitBreakerService.recordSuccess(serviceName, config).catch(err =>
46 | this.logger.error('Failed to record success', err),
47 | );
48 | }),
49 | catchError(error => {
50 | this.circuitBreakerService.recordFailure(serviceName, config).catch(err =>
51 | this.logger.error('Failed to record failure', err),
52 | );
53 | return throwError(() => error);
54 | }),
55 | );
56 | }),
57 | );
58 | }
59 |
60 | private getServiceName(context: ExecutionContext, request: any): string {
61 | const className = context.getClass().name;
62 | const handlerName = context.getHandler().name;
63 | const endpoint = `${request.method}_${request.route?.path || request.url}`;
64 |
65 | return `${className}_${handlerName}_${endpoint}`;
66 | }
67 | }
--------------------------------------------------------------------------------
/backend/src/api-gateway/services/health-monitor.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
2 | import { Cron } from '@nestjs/schedule';
3 | import { LoadBalancerService } from './load-balancer.service';
4 | import { AnalyticsService } from './analytics.service';
5 | import { CircuitBreakerService } from './circuit-breaker.service';
6 |
7 | @Injectable()
8 | export class HealthMonitorService implements OnModuleInit {
9 | private readonly logger = new Logger(HealthMonitorService.name);
10 |
11 | constructor(
12 | private readonly loadBalancerService: LoadBalancerService,
13 | private readonly analyticsService: AnalyticsService,
14 | private readonly circuitBreakerService: CircuitBreakerService,
15 | ) {}
16 |
17 | onModuleInit() {
18 | this.logger.log('Health Monitor Service initialized');
19 | }
20 |
21 | @Cron('*/30 * * * * *')
22 | async performHealthChecks() {
23 | try {
24 | const circuitBreakers = await this.circuitBreakerService.getAllCircuitBreakers();
25 |
26 | for (const [serviceName, status] of Object.entries(circuitBreakers)) {
27 | if (status.state === 'OPEN') {
28 | this.logger.warn(`Service ${serviceName} circuit breaker is OPEN`);
29 | }
30 | }
31 | } catch (error) {
32 | this.logger.error('Health check failed', error);
33 | }
34 | }
35 |
36 |
37 | @Cron('0 0 * * *')
38 | async cleanupOldData() {
39 | try {
40 | const deletedRecords = await this.analyticsService.cleanupOldUsageData(90);
41 | this.logger.log(`Cleaned up ${deletedRecords} old usage records`);
42 | } catch (error) {
43 | this.logger.error('Data cleanup failed', error);
44 | }
45 | }
46 |
47 |
48 | @Cron('0 1 * * *')
49 | async generateDailyReport() {
50 | try {
51 | const yesterday = new Date();
52 | yesterday.setDate(yesterday.getDate() - 1);
53 | yesterday.setHours(0, 0, 0, 0);
54 |
55 | const today = new Date();
56 | today.setHours(0, 0, 0, 0);
57 |
58 | const metrics = await this.analyticsService.getUsageMetrics(yesterday, today);
59 | const traffic = await this.analyticsService.getTrafficAnalytics(yesterday, today);
60 | const performance = await this.analyticsService.getPerformanceMetrics(yesterday, today);
61 |
62 | this.logger.log('Daily Report Generated', {
63 | date: yesterday.toISOString().split('T')[0],
64 | totalRequests: metrics.totalRequests,
65 | successRate: metrics.successRate.toFixed(2) + '%',
66 | avgResponseTime: performance.p50ResponseTime + 'ms',
67 | topEndpoints: traffic.trafficByEndpoint.slice(0, 5),
68 | });
69 |
70 | } catch (error) {
71 | this.logger.error('Daily report generation failed', error);
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/backend/src/transaction-analysis/services/__tests__/fraud-detection.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, type TestingModule } from "@nestjs/testing"
2 | import { FraudDetectionService } from "../fraud-detection.service"
3 | import { mockTransactions } from "../../../test/mocks/transaction.mock"
4 |
5 | describe("FraudDetectionService", () => {
6 | let service: FraudDetectionService
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [FraudDetectionService],
11 | }).compile()
12 |
13 | service = module.get(FraudDetectionService)
14 | })
15 |
16 | describe("detectFraud", () => {
17 | it("should detect low risk for normal transaction", async () => {
18 | const transaction = mockTransactions[0]
19 |
20 | const result = await service.detectFraud(transaction)
21 |
22 | expect(result.riskScore).toBeLessThan(0.3)
23 | expect(result.fraudScore).toBeLessThan(0.2)
24 | expect(result.indicators).toHaveLength(0)
25 | })
26 |
27 | it("should detect high risk for unusual amount", async () => {
28 | const transaction = {
29 | ...mockTransactions[0],
30 | amount: -5000, // Unusually high amount
31 | }
32 |
33 | const result = await service.detectFraud(transaction)
34 |
35 | expect(result.riskScore).toBeGreaterThan(0.7)
36 | expect(result.indicators).toContain("UNUSUAL_AMOUNT")
37 | })
38 |
39 | it("should detect suspicious location patterns", async () => {
40 | const transaction = {
41 | ...mockTransactions[0],
42 | location: "Unknown Location, Foreign Country",
43 | metadata: { country: "XX", city: "Unknown" },
44 | }
45 |
46 | const result = await service.detectFraud(transaction)
47 |
48 | expect(result.indicators).toContain("SUSPICIOUS_LOCATION")
49 | })
50 |
51 | it("should detect velocity fraud patterns", async () => {
52 | const transactions = Array(10)
53 | .fill(null)
54 | .map((_, i) => ({
55 | ...mockTransactions[0],
56 | id: `tx-${i}`,
57 | timestamp: new Date(Date.now() - i * 60000), // 1 minute apart
58 | amount: -100,
59 | }))
60 |
61 | const result = await service.detectVelocityFraud(transactions)
62 |
63 | expect(result.riskScore).toBeGreaterThan(0.8)
64 | expect(result.indicators).toContain("HIGH_VELOCITY")
65 | })
66 | })
67 |
68 | describe("analyzeTransactionPatterns", () => {
69 | it("should identify normal spending patterns", async () => {
70 | const transactions = mockTransactions
71 |
72 | const result = await service.analyzeTransactionPatterns(transactions)
73 |
74 | expect(result.patterns).toBeDefined()
75 | expect(result.anomalies).toBeDefined()
76 | expect(result.riskFactors).toBeDefined()
77 | })
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/guards/api-key.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | CanActivate,
4 | ExecutionContext,
5 | UnauthorizedException,
6 | ForbiddenException,
7 | Logger,
8 | SetMetadata,
9 | } from '@nestjs/common';
10 | import { Reflector } from '@nestjs/core';
11 | import { ApiKeyService } from '../services/api-key.service';
12 |
13 | export const API_KEY_REQUIRED = 'apiKeyRequired';
14 | export const REQUIRED_PERMISSIONS = 'requiredPermissions';
15 |
16 | export const RequireApiKey = () => SetMetadata(API_KEY_REQUIRED, true);
17 | export const RequirePermissions = (...permissions: string[]) =>
18 | SetMetadata(REQUIRED_PERMISSIONS, permissions);
19 |
20 | @Injectable()
21 | export class ApiKeyGuard implements CanActivate {
22 | private readonly logger = new Logger(ApiKeyGuard.name);
23 |
24 | constructor(
25 | private readonly reflector: Reflector,
26 | private readonly apiKeyService: ApiKeyService,
27 | ) {}
28 |
29 | async canActivate(context: ExecutionContext): Promise {
30 | const request = context.switchToHttp().getRequest();
31 |
32 | const apiKeyRequired = this.reflector.getAllAndOverride(
33 | API_KEY_REQUIRED,
34 | [context.getHandler(), context.getClass()],
35 | );
36 |
37 | if (!apiKeyRequired) {
38 | return true;
39 | }
40 |
41 | const apiKeyHeader = request.headers['x-api-key'];
42 |
43 | if (!apiKeyHeader) {
44 | throw new UnauthorizedException('API key is required');
45 | }
46 |
47 | try {
48 | const apiKey = await this.apiKeyService.validateApiKey(apiKeyHeader);
49 |
50 | if (!apiKey) {
51 | throw new UnauthorizedException('Invalid API key');
52 | }
53 |
54 | const requiredPermissions = this.reflector.getAllAndOverride(
55 | REQUIRED_PERMISSIONS,
56 | [context.getHandler(), context.getClass()],
57 | );
58 |
59 | if (requiredPermissions && requiredPermissions.length > 0) {
60 | const hasPermission = await this.checkPermissions(apiKey, requiredPermissions);
61 |
62 | if (!hasPermission) {
63 | throw new ForbiddenException('Insufficient permissions');
64 | }
65 | }
66 |
67 | request.apiKey = apiKey;
68 |
69 | return true;
70 | } catch (error) {
71 | if (error instanceof UnauthorizedException || error instanceof ForbiddenException) {
72 | throw error;
73 | }
74 |
75 | this.logger.error('API key validation failed', error);
76 | throw new UnauthorizedException('API key validation failed');
77 | }
78 | }
79 |
80 | private async checkPermissions(apiKey: any, requiredPermissions: string[]): Promise {
81 | for (const permission of requiredPermissions) {
82 | const hasPermission = await this.apiKeyService.hasPermission(apiKey, permission);
83 | if (!hasPermission) {
84 | return false;
85 | }
86 | }
87 | return true;
88 | }
89 | }
--------------------------------------------------------------------------------
/backend/src/credit-bureaus/README.md:
--------------------------------------------------------------------------------
1 | # Credit Bureau Integration
2 |
3 | This module provides integration with major credit bureaus (Experian, Equifax, TransUnion) to fetch credit reports and process credit data updates.
4 |
5 | ## Features
6 |
7 | - Adapter pattern for different credit bureau APIs
8 | - Normalized response format for consistent data structure
9 | - Circuit breaker pattern for resilience
10 | - Rate limiting and quota management via axios interceptors
11 | - Error handling and retry logic with exponential backoff
12 | - Webhook handling for updates from credit bureaus
13 | - Support for sandbox/production environment switching
14 | - Comprehensive test coverage
15 |
16 | ## Usage
17 |
18 | ### Getting Credit Reports
19 |
20 | ```typescript
21 | // Inject CreditBureauService
22 | constructor(private creditBureauService: CreditBureauService) {}
23 |
24 | // Get credit report from a specific bureau
25 | const experianReport = await this.creditBureauService.getCreditReport(
26 | 'experian',
27 | userId,
28 | {
29 | firstName: 'John',
30 | lastName: 'Doe',
31 | ssn: '123-45-6789',
32 | dob: '1990-01-01'
33 | }
34 | );
35 |
36 | // Get reports from all bureaus
37 | const allReports = await this.creditBureauService.getAllCreditReports(userId);
38 | ```
39 |
40 | ### API Endpoints
41 |
42 | - `GET /credit-bureaus/reports/:userId/:bureau` - Get credit report from specific bureau
43 | - `GET /credit-bureaus/reports/:userId` - Get credit reports from all bureaus
44 | - `POST /credit-bureaus/reports/request` - Request new credit report
45 | - `POST /credit-bureaus/webhooks/:bureau` - Handle webhook from bureau
46 |
47 | ## Configuration
48 |
49 | Add the following configuration to your `.env` file:
50 |
51 | ```
52 | # Experian
53 | CREDIT_BUREAU_EXPERIAN_API_KEY=your_experian_api_key
54 | CREDIT_BUREAU_EXPERIAN_SANDBOX=true
55 | CREDIT_BUREAU_EXPERIAN_SANDBOX_URL=https://sandbox-api.experian.com
56 | CREDIT_BUREAU_EXPERIAN_PRODUCTION_URL=https://api.experian.com
57 |
58 | # Equifax
59 | CREDIT_BUREAU_EQUIFAX_API_KEY=your_equifax_api_key
60 | CREDIT_BUREAU_EQUIFAX_SANDBOX=true
61 | CREDIT_BUREAU_EQUIFAX_SANDBOX_URL=https://sandbox-api.equifax.com
62 | CREDIT_BUREAU_EQUIFAX_PRODUCTION_URL=https://api.equifax.com
63 |
64 | # TransUnion
65 | CREDIT_BUREAU_TRANSUNION_API_KEY=your_transunion_api_key
66 | CREDIT_BUREAU_TRANSUNION_SANDBOX=true
67 | CREDIT_BUREAU_TRANSUNION_SANDBOX_URL=https://sandbox-api.transunion.com
68 | CREDIT_BUREAU_TRANSUNION_PRODUCTION_URL=https://api.transunion.com
69 | ```
70 |
71 | ## Architecture
72 |
73 | The module uses the adapter pattern to normalize interactions with different credit bureau APIs:
74 |
75 | - `CreditBureauAdapter` - Interface that all adapters implement
76 | - `BaseCreditBureauAdapter` - Common functionality for all adapters
77 | - Specific adapters (`ExperianAdapter`, `EquifaxAdapter`, `TransUnionAdapter`)
78 | - `CreditBureauService` - Orchestrates adapters and provides circuit breaking
79 | - `CreditBureauController` - Exposes API endpoints
80 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/dto/endpoint.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsOptional, IsObject, IsNumber, IsBoolean, IsUrl } from 'class-validator';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | export class CreateEndpointDto {
5 | @ApiProperty({ description: 'API endpoint path' })
6 | @IsString()
7 | path: string;
8 |
9 | @ApiProperty({ description: 'HTTP method' })
10 | @IsString()
11 | method: string;
12 |
13 | @ApiProperty({ description: 'API version', default: 'v1' })
14 | @IsString()
15 | version: string;
16 |
17 | @ApiProperty({ description: 'Target URL or service instances' })
18 | @IsString()
19 | targetUrl: string;
20 |
21 | @ApiProperty({ description: 'Request/Response transformation rules', required: false })
22 | @IsOptional()
23 | @IsObject()
24 | transformationRules?: Record;
25 |
26 | @ApiProperty({ description: 'Rate limiting configuration', required: false })
27 | @IsOptional()
28 | @IsObject()
29 | rateLimitConfig?: Record;
30 |
31 | @ApiProperty({ description: 'Circuit breaker configuration', required: false })
32 | @IsOptional()
33 | @IsObject()
34 | circuitBreakerConfig?: Record;
35 |
36 | @ApiProperty({ description: 'Custom headers', required: false })
37 | @IsOptional()
38 | @IsObject()
39 | headers?: Record;
40 |
41 | @ApiProperty({ description: 'Request timeout in milliseconds', required: false, default: 30000 })
42 | @IsOptional()
43 | @IsNumber()
44 | timeout?: number;
45 | }
46 |
47 | export class UpdateEndpointDto {
48 | @ApiProperty({ description: 'API endpoint path', required: false })
49 | @IsOptional()
50 | @IsString()
51 | path?: string;
52 |
53 | @ApiProperty({ description: 'HTTP method', required: false })
54 | @IsOptional()
55 | @IsString()
56 | method?: string;
57 |
58 | @ApiProperty({ description: 'API version', required: false })
59 | @IsOptional()
60 | @IsString()
61 | version?: string;
62 |
63 | @ApiProperty({ description: 'Target URL or service instances', required: false })
64 | @IsOptional()
65 | @IsString()
66 | targetUrl?: string;
67 |
68 | @ApiProperty({ description: 'Active status', required: false })
69 | @IsOptional()
70 | @IsBoolean()
71 | isActive?: boolean;
72 |
73 | @ApiProperty({ description: 'Request/Response transformation rules', required: false })
74 | @IsOptional()
75 | @IsObject()
76 | transformationRules?: Record;
77 |
78 | @ApiProperty({ description: 'Rate limiting configuration', required: false })
79 | @IsOptional()
80 | @IsObject()
81 | rateLimitConfig?: Record;
82 |
83 | @ApiProperty({ description: 'Circuit breaker configuration', required: false })
84 | @IsOptional()
85 | @IsObject()
86 | circuitBreakerConfig?: Record;
87 |
88 | @ApiProperty({ description: 'Custom headers', required: false })
89 | @IsOptional()
90 | @IsObject()
91 | headers?: Record;
92 |
93 | @ApiProperty({ description: 'Request timeout in milliseconds', required: false })
94 | @IsOptional()
95 | @IsNumber()
96 | timeout?: number;
97 | }
98 |
--------------------------------------------------------------------------------
/backend/bankingAnalysis/banking-service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { PlaidService } from './plaid.service';
4 | import { encrypt, decrypt } from './utils/encryption.util';
5 |
6 | @Injectable()
7 | export class BankingService {
8 | // In a real application, you would store this in a database (e.g., using TypeORM).
9 | // We map a user ID to their encrypted Plaid access token.
10 | private linkedAccounts = new Map();
11 | private readonly encryptionKey: string;
12 |
13 | constructor(
14 | private readonly plaidService: PlaidService,
15 | private readonly configService: ConfigService,
16 | ) {
17 | this.encryptionKey = this.configService.get('ENCRYPTION_KEY');
18 | if (!this.encryptionKey || this.encryptionKey.length < 32) {
19 | throw new InternalServerErrorException('A 32-byte ENCRYPTION_KEY must be configured.');
20 | }
21 | }
22 |
23 | /**
24 | * Orchestrates the creation of a Plaid Link token for the frontend.
25 | * @param userId The ID of the user linking their account.
26 | */
27 | async generateLinkToken(userId: string) {
28 | // This calls the PlaidService to get a token from the Plaid API.
29 | return this.plaidService.createLinkToken(userId);
30 | }
31 |
32 | /**
33 | * Sets a linked account for a user by exchanging a public token for an access token
34 | * and storing the encrypted access token.
35 | * @param userId The ID of the user.
36 | * @param publicToken The temporary public token from the Plaid Link frontend flow.
37 | */
38 | async setLinkedAccount(userId: string, publicToken: string) {
39 | const tokenData = await this.plaidService.exchangePublicToken(publicToken);
40 | const accessToken = tokenData.access_token;
41 |
42 | // Encrypt the access token before storing it.
43 | const encryptedAccessToken = encrypt(accessToken, this.encryptionKey);
44 |
45 | // Store the encrypted token, linking it to our internal user ID.
46 | this.linkedAccounts.set(userId, encryptedAccessToken);
47 | console.log(`Account linked for user ${userId}.`);
48 |
49 | return { message: 'Bank account linked successfully.' };
50 | }
51 |
52 | /**
53 | * Retrieves transactions for a user's linked account.
54 | * @param userId The ID of the user.
55 | */
56 | async getUserTransactions(userId: string) {
57 | const encryptedAccessToken = this.linkedAccounts.get(userId);
58 | if (!encryptedAccessToken) {
59 | throw new UnauthorizedException('User does not have a linked bank account.');
60 | }
61 |
62 | // Decrypt the token just before using it.
63 | const accessToken = decrypt(encryptedAccessToken, this.encryptionKey);
64 |
65 | // Use the decrypted token to fetch transactions from Plaid.
66 | const transactionsData = await this.plaidService.getTransactions(accessToken);
67 |
68 | // Here, you could implement transaction categorization and analysis algorithms.
69 | console.log(`Fetched ${transactionsData.transactions.length} transactions for user ${userId}.`);
70 |
71 | return transactionsData;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/backend/src/transaction-analysis/services/__tests__/categorization.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, type TestingModule } from "@nestjs/testing"
2 | import { CategorizationService } from "../categorization.service"
3 | import { mockTransactions } from "../../../test/mocks/transaction.mock"
4 |
5 | describe("CategorizationService", () => {
6 | let service: CategorizationService
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [CategorizationService],
11 | }).compile()
12 |
13 | service = module.get(CategorizationService)
14 | })
15 |
16 | describe("categorizeTransaction", () => {
17 | it("should categorize restaurant transaction correctly", async () => {
18 | const transaction = mockTransactions.find((t) => t.description.includes("Restaurant"))
19 |
20 | const result = await service.categorizeTransaction(transaction)
21 |
22 | expect(result.category).toBe("FOOD_DINING")
23 | expect(result.subcategory).toBe("Restaurant")
24 | expect(result.confidence).toBeGreaterThan(0.8)
25 | })
26 |
27 | it("should categorize gas station transaction correctly", async () => {
28 | const transaction = {
29 | ...mockTransactions[0],
30 | description: "SHELL GAS STATION",
31 | merchant: "Shell",
32 | amount: -45.5,
33 | }
34 |
35 | const result = await service.categorizeTransaction(transaction)
36 |
37 | expect(result.category).toBe("TRANSPORTATION")
38 | expect(result.subcategory).toBe("Gas")
39 | expect(result.confidence).toBeGreaterThan(0.8)
40 | })
41 |
42 | it("should handle unknown merchants with lower confidence", async () => {
43 | const transaction = {
44 | ...mockTransactions[0],
45 | description: "UNKNOWN MERCHANT XYZ",
46 | merchant: "Unknown XYZ",
47 | amount: -25.0,
48 | }
49 |
50 | const result = await service.categorizeTransaction(transaction)
51 |
52 | expect(result.category).toBeDefined()
53 | expect(result.confidence).toBeLessThan(0.7)
54 | })
55 | })
56 |
57 | describe("categorizeTransactionsBatch", () => {
58 | it("should categorize multiple transactions efficiently", async () => {
59 | const transactions = mockTransactions.slice(0, 5)
60 |
61 | const results = await service.categorizeTransactionsBatch(transactions)
62 |
63 | expect(results).toHaveLength(5)
64 | results.forEach((result) => {
65 | expect(result.category).toBeDefined()
66 | expect(result.subcategory).toBeDefined()
67 | expect(result.confidence).toBeGreaterThan(0)
68 | })
69 | })
70 | })
71 |
72 | describe("trainModel", () => {
73 | it("should train categorization model with labeled data", async () => {
74 | const labeledData = mockTransactions.map((t) => ({
75 | transaction: t,
76 | category: "FOOD_DINING",
77 | subcategory: "Restaurant",
78 | }))
79 |
80 | const result = await service.trainModel(labeledData)
81 |
82 | expect(result.accuracy).toBeGreaterThan(0.8)
83 | expect(result.modelVersion).toBeDefined()
84 | })
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/backend/src/transaction-analysis/controllers/__tests__/transaction-analysis.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, type TestingModule } from "@nestjs/testing"
2 | import { TransactionAnalysisController } from "../transaction-analysis.controller"
3 | import { TransactionAnalysisService } from "../../services/transaction-analysis.service"
4 | import { jest } from "@jest/globals"
5 |
6 | describe("TransactionAnalysisController", () => {
7 | let controller: TransactionAnalysisController
8 | let service: TransactionAnalysisService
9 |
10 | beforeEach(async () => {
11 | const module: TestingModule = await Test.createTestingModule({
12 | controllers: [TransactionAnalysisController],
13 | providers: [
14 | {
15 | provide: TransactionAnalysisService,
16 | useValue: {
17 | analyzeTransaction: jest.fn(),
18 | analyzeUserTransactions: jest.fn(),
19 | calculateCreditScore: jest.fn(),
20 | getAnalysisHistory: jest.fn(),
21 | },
22 | },
23 | ],
24 | }).compile()
25 |
26 | controller = module.get(TransactionAnalysisController)
27 | service = module.get(TransactionAnalysisService)
28 | })
29 |
30 | describe("analyzeTransaction", () => {
31 | it("should analyze single transaction", async () => {
32 | const transactionId = "tx-1"
33 | const mockAnalysis = {
34 | id: "1",
35 | transactionId,
36 | category: "FOOD_DINING",
37 | riskScore: 0.2,
38 | fraudScore: 0.1,
39 | }
40 |
41 | jest.spyOn(service, "analyzeTransaction").mockResolvedValue(mockAnalysis as any)
42 |
43 | const result = await controller.analyzeTransaction(transactionId)
44 |
45 | expect(result).toEqual(mockAnalysis)
46 | expect(service.analyzeTransaction).toHaveBeenCalledWith(transactionId)
47 | })
48 | })
49 |
50 | describe("analyzeUserTransactions", () => {
51 | it("should analyze all user transactions", async () => {
52 | const userId = "user-1"
53 | const mockAnalysis = {
54 | userId,
55 | totalTransactions: 10,
56 | spendingPatterns: {},
57 | riskAssessment: {},
58 | }
59 |
60 | jest.spyOn(service, "analyzeUserTransactions").mockResolvedValue(mockAnalysis as any)
61 |
62 | const result = await controller.analyzeUserTransactions(userId)
63 |
64 | expect(result).toEqual(mockAnalysis)
65 | expect(service.analyzeUserTransactions).toHaveBeenCalledWith(userId)
66 | })
67 | })
68 |
69 | describe("getCreditScore", () => {
70 | it("should calculate and return credit score", async () => {
71 | const userId = "user-1"
72 | const mockScore = {
73 | score: 750,
74 | factors: ["Payment history", "Credit utilization"],
75 | recommendations: ["Pay bills on time"],
76 | }
77 |
78 | jest.spyOn(service, "calculateCreditScore").mockResolvedValue(mockScore as any)
79 |
80 | const result = await controller.getCreditScore(userId)
81 |
82 | expect(result).toEqual(mockScore)
83 | expect(service.calculateCreditScore).toHaveBeenCalledWith(userId)
84 | })
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/backend/src/anonymization/controllers/anonymization.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, Query, ParseIntPipe } from '@nestjs/common';
2 | import { AnonymizeDataDto } from '../dto/anonymize-data.dto';
3 | import { PiiDetectionService } from '../services/pii-detection.service';
4 | import { PseudonymizationService } from '../services/pseudonymization.service';
5 | import { KAnonymityService } from '../services/k-anonymity.service';
6 | import { DifferentialPrivacyService } from '../services/differential-privacy.service';
7 | import { IsNumber, IsNotEmpty } from 'class-validator';
8 |
9 | // A DTO for the K-anonymity endpoint
10 | class KAnonymityDto {
11 | @IsNotEmpty()
12 | readonly data: any[];
13 | }
14 |
15 | // A DTO for the differential privacy endpoint
16 | class DifferentialPrivacyDto {
17 | @IsNumber()
18 | readonly value: number;
19 | }
20 |
21 | @Controller('anonymize')
22 | export class AnonymizationController {
23 | constructor(
24 | private readonly piiDetectionService: PiiDetectionService,
25 | private readonly pseudonymizationService: PseudonymizationService,
26 | private readonly kAnonymityService: KAnonymityService,
27 | private readonly differentialPrivacyService: DifferentialPrivacyService,
28 | ) {}
29 |
30 | @Post('mask')
31 | maskData(@Body() anonymizeDataDto: AnonymizeDataDto) {
32 | const maskedData = this.piiDetectionService.detectAndMaskPii(anonymizeDataDto.data);
33 | return { original: anonymizeDataDto.data, masked: maskedData };
34 | }
35 |
36 | @Post('pseudonymize')
37 | pseudonymizeData(@Body() anonymizeDataDto: AnonymizeDataDto) {
38 | const pseudonymizedData = this.pseudonymizationService.pseudonymize(anonymizeDataDto.data);
39 | return { original: anonymizeDataDto.data, pseudonymized: pseudonymizedData };
40 | }
41 |
42 | @Post('reverse-pseudonymize')
43 | reversePseudonymizeData(@Body() anonymizeDataDto: AnonymizeDataDto) {
44 | const originalData = this.pseudonymizationService.reversePseudonym(anonymizeDataDto.data);
45 | return { pseudonymized: anonymizeDataDto.data, original: originalData };
46 | }
47 |
48 | @Post('k-anonymity')
49 | applyKAnonymity(
50 | @Body() kAnonymityDto: KAnonymityDto,
51 | @Query('k', ParseIntPipe) k: number,
52 | ) {
53 | // You would replace this with your actual quasi-identifiers from the data.
54 | const quasiIdentifiers = ['city', 'age'];
55 | const anonymizedData = this.kAnonymityService.applyKAnonymity(
56 | kAnonymityDto.data,
57 | quasiIdentifiers,
58 | k,
59 | );
60 | return { original: kAnonymityDto.data, anonymized: anonymizedData };
61 | }
62 |
63 | @Post('differential-privacy')
64 | addDifferentialPrivacy(
65 | @Body() differentialPrivacyDto: DifferentialPrivacyDto,
66 | @Query('epsilon', ParseIntPipe) epsilon: number,
67 | ) {
68 | // A simplified sensitivity for demonstration purposes.
69 | const sensitivity = 1;
70 | const noisyValue = this.differentialPrivacyService.addNoise(
71 | differentialPrivacyDto.value,
72 | epsilon,
73 | sensitivity,
74 | );
75 | return {
76 | original: differentialPrivacyDto.value,
77 | noisy: noisyValue,
78 | epsilon: epsilon,
79 | };
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/backend/src/api-gateway/services/rate-limit.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { RedisService } from '../../redis/redis.service';
3 |
4 | export interface RateLimitConfig {
5 | windowMs: number;
6 | maxRequests: number;
7 | keyGenerator?: (req: any) => string;
8 | skipSuccessfulRequests?: boolean;
9 | skipFailedRequests?: boolean;
10 | }
11 |
12 | export interface RateLimitResult {
13 | allowed: boolean;
14 | remaining: number;
15 | resetTime: Date;
16 | totalHits: number;
17 | }
18 |
19 | @Injectable()
20 | export class RateLimitService {
21 | private readonly logger = new Logger(RateLimitService.name);
22 |
23 | constructor(private readonly redisService: RedisService) {}
24 |
25 | async checkRateLimit(
26 | key: string,
27 | config: RateLimitConfig,
28 | ): Promise {
29 | try {
30 | const windowStart = Math.floor(Date.now() / config.windowMs);
31 | const redisKey = `rate_limit:${key}:${windowStart}`;
32 |
33 | this.redisService.incr(redisKey);
34 | const currentStr = await this.redisService.get(redisKey);
35 | const current = parseInt(currentStr ?? '0', 10);
36 | if (current === 1) {
37 | this.redisService.expire(redisKey, Math.ceil(config.windowMs / 1000));
38 | }
39 | const resetTime = new Date((windowStart + 1) * config.windowMs);
40 | const remaining = Math.max(0, config.maxRequests - current);
41 |
42 | return {
43 | allowed: current <= config.maxRequests,
44 | remaining,
45 | resetTime,
46 | totalHits: current,
47 | };
48 | } catch (error) {
49 | this.logger.error('Rate limit check failed', error);
50 | return {
51 | allowed: true,
52 | remaining: config.maxRequests,
53 | resetTime: new Date(Date.now() + config.windowMs),
54 | totalHits: 0,
55 | };
56 | }
57 | }
58 |
59 | async resetRateLimit(key: string): Promise {
60 | try {
61 | const pattern = `rate_limit:${key}:*`;
62 | const keys = await this.redisService.keys(pattern);
63 |
64 | if (keys && keys.length > 0) {
65 | await this.redisService.del(keys[0]);
66 | }
67 | } catch (error) {
68 | this.logger.error('Rate limit reset failed', error);
69 | }
70 | }
71 |
72 | async getRateLimitStatus(key: string, windowMs: number): Promise {
73 | try {
74 | const windowStart = Math.floor(Date.now() / windowMs);
75 | const redisKey = `rate_limit:${key}:${windowStart}`;
76 |
77 | const current = await this.redisService.get(redisKey);
78 | return parseInt(current ?? '0', 10);
79 | } catch (error) {
80 | this.logger.error('Rate limit status check failed', error);
81 | return 0;
82 | }
83 | }
84 |
85 | generateApiKeyRateLimitKey(apiKey: string): string {
86 | return `api_key:${apiKey}`;
87 | }
88 |
89 | generateUserRateLimitKey(userId: string): string {
90 | return `user:${userId}`;
91 | }
92 |
93 | generateIPRateLimitKey(ip: string): string {
94 | return `ip:${ip}`;
95 | }
96 |
97 | generateEndpointRateLimitKey(endpoint: string, method: string): string {
98 | return `endpoint:${method}:${endpoint}`;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/backend/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule, ConfigService } from '@nestjs/config';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { AppController } from './app.controller';
5 | import { AppService } from './app.service';
6 | import { UsersModule } from './users/users.module';
7 | import { PrivacyModule } from './privacy/privacy.module';
8 | import { AuthModule } from './auth/auth.module';
9 | import { RedisModule } from './redis/redis.module';
10 | import { EmailModule } from './email/email.module';
11 | import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
12 | import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
13 | import { ThrottlerModule } from '@nestjs/throttler';
14 | import { IpfsModule } from './ipfs/ipfs.module';
15 | import { CreditBureauModule } from './credit-bureaus/credit-bureau.module';
16 | import { DocumentsModule } from './documents/documents.module';
17 | import { BullModule } from '@nestjs/bull';
18 | import { RiskModule } from './risk/risk.module';
19 | import { AuditModule } from './audit/audit.module';
20 | import { AuditInterceptor } from './audit/audit.interceptor';
21 | import { ScreeningModule } from './screening/screening.module';
22 | import { ApiGatewayModule } from './api-gateway/api-gateway.module';
23 |
24 | @Module({
25 | imports: [
26 | // 1. Load the .env file
27 | ConfigModule.forRoot({
28 | isGlobal: true, // Makes the .env variables available everywhere
29 | envFilePath: '.env',
30 | }),
31 |
32 | // 2. Setup the TypeORM connection
33 | TypeOrmModule.forRootAsync({
34 | imports: [UsersModule, ConfigModule.forRoot({ isGlobal: true })],
35 | inject: [ConfigService],
36 | useFactory: (configService: ConfigService) => ({
37 | type: 'postgres',
38 | host: configService.get('DB_HOST'),
39 | port: configService.get('DB_PORT'),
40 | username: configService.get('DB_USERNAME'),
41 | password: configService.get('DB_PASSWORD'),
42 | database: configService.get('DB_NAME'),
43 | entities: ['src/**/*.entity.ts', 'dist/**/*.entity.js'],
44 | synchronize: false, // Use migrations instead
45 | logging: true,
46 | extra: {
47 | max: configService.get('DB_POOL_MAX', 20), // Default to 20 if not set
48 | },
49 | }),
50 | }),
51 | // 3. Register Bull queue for document processing
52 | BullModule.forRoot({
53 | redis: {
54 | host: process.env.REDIS_HOST || 'localhost',
55 | port: Number.parseInt(process.env.REDIS_PORT || '6379', 10),
56 | },
57 | }),
58 | ThrottlerModule.forRoot([
59 | {
60 | ttl: 60000,
61 | limit: 10,
62 | },
63 | ]),
64 | UsersModule,
65 | AuthModule,
66 | RedisModule,
67 | EmailModule,
68 | PrivacyModule,
69 | DocumentsModule,
70 | IpfsModule,
71 | // Credit Bureau Integration
72 | CreditBureauModule,
73 | RiskModule,
74 | AuditModule,
75 | ScreeningModule,
76 | ApiGatewayModule,
77 | ],
78 | controllers: [AppController],
79 | providers: [
80 | AppService,
81 | {
82 | provide: APP_GUARD,
83 | useClass: JwtAuthGuard,
84 | },
85 | {
86 | provide: APP_INTERCEPTOR,
87 | useClass: AuditInterceptor,
88 | },
89 | ],
90 | })
91 | export class AppModule {}
92 |
--------------------------------------------------------------------------------
/backend/src/anonymization/services/k-anonymity.service.ts:
--------------------------------------------------------------------------------
1 | // A service for implementing K-anonymity on datasets.
2 | import { Injectable } from '@nestjs/common';
3 |
4 | @Injectable()
5 | export class KAnonymityService {
6 | /**
7 | * Applies k-anonymity to a dataset based on specified quasi-identifiers.
8 | * This is a simplified example using generalization.
9 | * In a real-world scenario, this would involve more complex algorithms.
10 | *
11 | * @param data The input array of objects to anonymize.
12 | * @param quasiIdentifiers An array of keys to identify quasi-identifiers.
13 | * @param k The anonymity factor.
14 | * @returns The anonymized dataset.
15 | */
16 | applyKAnonymity(data: T[], quasiIdentifiers: (keyof T)[], k: number): T[] {
17 | const anonymizedData: T[] = [];
18 | const groups = this.groupDataByQuasiIdentifiers(data, quasiIdentifiers);
19 |
20 | for (const groupKey in groups) {
21 | if (groups[groupKey].length >= k) {
22 | // This group meets the k-anonymity requirement. Add it to the result.
23 | anonymizedData.push(...groups[groupKey]);
24 | } else {
25 | // If a group does not meet the requirement, generalize or suppress it.
26 | // Here, we'll demonstrate a simple generalization.
27 | anonymizedData.push(...this.generalizeGroup(groups[groupKey], quasiIdentifiers));
28 | }
29 | }
30 |
31 | return anonymizedData;
32 | }
33 |
34 | /**
35 | * Groups data objects by the values of their quasi-identifiers.
36 | * @param data The input array of objects.
37 | * @param quasiIdentifiers The keys to group by.
38 | * @returns An object where keys are the combined quasi-identifier values and
39 | * values are the arrays of data objects belonging to that group.
40 | */
41 | private groupDataByQuasiIdentifiers(data: T[], quasiIdentifiers: (keyof T)[]): { [key: string]: T[] } {
42 | const groups: { [key: string]: T[] } = {};
43 | for (const item of data) {
44 | const groupKey = quasiIdentifiers.map(key => item[key]).join('|');
45 | if (!groups[groupKey]) {
46 | groups[groupKey] = [];
47 | }
48 | groups[groupKey].push(item);
49 | }
50 | return groups;
51 | }
52 |
53 | /**
54 | * Generalizes a group's quasi-identifiers if the group is too small.
55 | * This is a placeholder for a more sophisticated generalization strategy.
56 | * @param group The small group of data objects.
57 | * @param quasiIdentifiers The keys to generalize.
58 | * @returns The generalized data objects.
59 | */
60 | private generalizeGroup(group: T[], quasiIdentifiers: (keyof T)[]): T[] {
61 | return group.map(item => {
62 | const generalizedItem = { ...item };
63 | for (const key of quasiIdentifiers) {
64 | if (typeof generalizedItem[key] === 'string' && generalizedItem[key].toString().includes('@')) {
65 | // Example: Generalize email domain.
66 | (generalizedItem[key] as any) = '[generalized-email]';
67 | } else if (typeof generalizedItem[key] === 'number') {
68 | // Example: Generalize age into ranges.
69 | const age = generalizedItem[key] as number;
70 | (generalizedItem[key] as any) = `${Math.floor(age / 10) * 10}-${Math.floor(age / 10) * 10 + 9}`;
71 | }
72 | }
73 | return generalizedItem;
74 | });
75 | }
76 | }
--------------------------------------------------------------------------------