diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9fc7940..f88c0515 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,116 @@ -name: CI +name: CI/CD Pipeline on: push: - branches: [ main ] + branches: [ main, develop ] pull_request: - branches: [ main ] + branches: [ main, develop ] jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma Client + run: npm run db:generate + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test + + - name: Run tests + run: npm test + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test + JWT_SECRET: test-secret-key + JWT_REFRESH_SECRET: test-refresh-secret-key + build: runs-on: ubuntu-latest + needs: [lint, test] + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + + deploy-staging: + runs-on: ubuntu-latest + needs: [build] + if: github.ref == 'refs/heads/develop' + + steps: + - uses: actions/checkout@v3 + + - name: Download build artifacts + uses: actions/download-artifact@v3 + with: + name: dist + + - name: Deploy to staging + run: | + echo "Deploying to staging environment..." + # Add your deployment commands here + # Example: scp -r dist/* user@staging-server:/path/to/app + + deploy-production: + runs-on: ubuntu-latest + needs: [build] + if: github.ref == 'refs/heads/main' + steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - name: Install dependencies - run: npm ci - - name: Lint - run: npm run lint - - name: Build - run: npm run build - - name: Run tests - run: npm test --silent + - uses: actions/checkout@v3 + + - name: Download build artifacts + uses: actions/download-artifact@v3 + with: + name: dist + + - name: Deploy to production + run: | + echo "Deploying to production environment..." + # Add your deployment commands here + # Example: scp -r dist/* user@production-server:/path/to/app diff --git a/docs/Email_Templates.md b/docs/Email_Templates.md new file mode 100644 index 00000000..004b718f --- /dev/null +++ b/docs/Email_Templates.md @@ -0,0 +1,491 @@ +# Email Templates Documentation + +This document provides an overview of all email templates used in the PropChain system, including sample payloads, variable references, and preview examples. + +## Available Email Templates + +### 1. Password Reset Template + +**Template File**: `src/email/templates/password-reset.ejs` + +**Purpose**: Sent when a user requests a password reset. + +**Variables**: +- `resetUrl` - The password reset link containing the reset token + +**Sample Payload**: +```json +{ + "resetUrl": "http://localhost:3000/reset-password?token=abc123xyz789" +} +``` + +**Rendered Preview**: +```html +
+

Password Reset Request

+

You have requested to reset your password for your PropChain account.

+

Please click the link below to reset your password:

+ + Reset Password + +

If you didn't request this password reset, please ignore this email.

+

This link will expire in 1 hour for security reasons.

+

Best regards,
The PropChain Team

+
+``` + +**Usage**: +```typescript +await emailService.sendPasswordResetEmail('user@example.com', 'abc123xyz789'); +``` + +--- + +### 2. Account Locked Template + +**Template File**: `src/email/templates/account-locked.ejs` + +**Purpose**: Sent when a user's account is temporarily locked due to multiple failed login attempts. + +**Variables**: +- `lockoutDuration` - Duration of the lockout in minutes + +**Sample Payload**: +```json +{ + "lockoutDuration": 30 +} +``` + +**Rendered Preview**: +```html +
+

Account Locked

+

Your PropChain account has been temporarily locked due to multiple failed login attempts.

+

The lockout will automatically expire in 30 minutes.

+

If you did not attempt to log in, please reset your password immediately or contact our support team.

+

Best regards,
The PropChain Team

+
+``` + +**Usage**: +```typescript +await emailService.sendAccountLockedEmail('user@example.com', 30); +``` + +--- + +### 3. Fraud Alert Template + +**Template File**: `src/email/templates/fraud-alert.ejs` + +**Purpose**: Sent to administrators when suspicious fraud activity is detected. + +**Variables**: +- `alertId` - Unique identifier for the fraud alert +- `pattern` - The fraud pattern detected (e.g., EXCESSIVE_FAILED_LOGINS) +- `severity` - Alert severity (LOW, MEDIUM, HIGH, CRITICAL) +- `userEmail` - Email of the user associated with the alert +- `description` - Detailed description of the suspicious activity + +**Sample Payload**: +```json +{ + "alertId": "fraud-alert-123", + "pattern": "EXCESSIVE_FAILED_LOGINS", + "severity": "HIGH", + "userEmail": "user@example.com", + "description": "The account recorded 10 failed login attempts in the last 30 minutes." +} +``` + +**Rendered Preview**: +```html +
+

Fraud Alert Triggered

+

Alert ID: fraud-alert-123

+

Pattern: EXCESSIVE_FAILED_LOGINS

+

Severity: HIGH

+

User: user@example.com

+

Summary: The account recorded 10 failed login attempts in the last 30 minutes.

+
+``` + +**Usage**: +```typescript +await emailService.sendFraudAlertEmail(['admin@example.com'], { + alertId: 'fraud-alert-123', + pattern: 'EXCESSIVE_FAILED_LOGINS', + severity: 'HIGH', + title: 'Repeated failed login attempts detected', + description: 'The account recorded 10 failed login attempts in the last 30 minutes.', + userEmail: 'user@example.com' +}); +``` + +--- + +### 4. Transaction Status Change - Pending + +**Template File**: `src/email/templates/transaction-status-pending.ejs` + +**Purpose**: Sent when a transaction is created and set to pending status. + +**Variables**: +- `transactionId` - Unique identifier for the transaction +- `propertyTitle` - Title of the property involved in the transaction +- `propertyAddress` - Address of the property +- `buyerName` - Name of the buyer +- `sellerName` - Name of the seller +- `amount` - Transaction amount +- `status` - Current transaction status + +**Sample Payload**: +```json +{ + "transactionId": "txn-123", + "propertyTitle": "Modern Downtown Apartment", + "propertyAddress": "123 Main St, New York, NY 10001", + "buyerName": "John Doe", + "sellerName": "Jane Smith", + "amount": "$500,000", + "status": "PENDING" +} +``` + +**Rendered Preview**: +```html +
+

Transaction Created

+

Your transaction has been successfully created and is now pending.

+
+

Transaction ID: txn-123

+

Property: Modern Downtown Apartment

+

Address: 123 Main St, New York, NY 10001

+

Amount: $500,000

+

Status: PENDING

+
+

You will receive notifications as the transaction progresses.

+

Best regards,
The PropChain Team

+
+``` + +--- + +### 5. Transaction Status Change - Completed + +**Template File**: `src/email/templates/transaction-status-completed.ejs` + +**Purpose**: Sent when a transaction is successfully completed. + +**Variables**: +- `transactionId` - Unique identifier for the transaction +- `propertyTitle` - Title of the property involved in the transaction +- `propertyAddress` - Address of the property +- `buyerName` - Name of the buyer +- `sellerName` - Name of the seller +- `amount` - Transaction amount +- `completionDate` - Date when transaction was completed +- `blockchainTxHash` - Blockchain transaction hash (if recorded) + +**Sample Payload**: +```json +{ + "transactionId": "txn-123", + "propertyTitle": "Modern Downtown Apartment", + "propertyAddress": "123 Main St, New York, NY 10001", + "buyerName": "John Doe", + "sellerName": "Jane Smith", + "amount": "$500,000", + "completionDate": "January 15, 2026", + "blockchainTxHash": "0xabc123...xyz789" +} +``` + +**Rendered Preview**: +```html +
+

Transaction Completed

+

Congratulations! Your transaction has been successfully completed.

+
+

Transaction ID: txn-123

+

Property: Modern Downtown Apartment

+

Address: 123 Main St, New York, NY 10001

+

Amount: $500,000

+

Completion Date: January 15, 2026

+

Blockchain Hash: 0xabc123...xyz789

+
+

All documents and records have been updated. Thank you for using PropChain.

+

Best regards,
The PropChain Team

+
+``` + +--- + +### 6. Transaction Status Change - Cancelled + +**Template File**: `src/email/templates/transaction-status-cancelled.ejs` + +**Purpose**: Sent when a transaction is cancelled. + +**Variables**: +- `transactionId` - Unique identifier for the transaction +- `propertyTitle` - Title of the property involved in the transaction +- `propertyAddress` - Address of the property +- `buyerName` - Name of the buyer +- `sellerName` - Name of the seller +- `amount` - Transaction amount +- `cancellationReason` - Reason for cancellation (if provided) +- `cancelledDate` - Date when transaction was cancelled + +**Sample Payload**: +```json +{ + "transactionId": "txn-123", + "propertyTitle": "Modern Downtown Apartment", + "propertyAddress": "123 Main St, New York, NY 10001", + "buyerName": "John Doe", + "sellerName": "Jane Smith", + "amount": "$500,000", + "cancellationReason": "Mutual agreement between parties", + "cancelledDate": "January 10, 2026" +} +``` + +**Rendered Preview**: +```html +
+

Transaction Cancelled

+

Your transaction has been cancelled.

+
+

Transaction ID: txn-123

+

Property: Modern Downtown Apartment

+

Address: 123 Main St, New York, NY 10001

+

Amount: $500,000

+

Reason: Mutual agreement between parties

+

Cancelled Date: January 10, 2026

+
+

If you have any questions about this cancellation, please contact our support team.

+

Best regards,
The PropChain Team

+
+``` + +--- + +## Email Service API + +### sendPasswordResetEmail + +```typescript +async sendPasswordResetEmail(email: string, resetToken: string): Promise +``` + +Sends a password reset email to the specified user. + +**Parameters**: +- `email` - User's email address +- `resetToken` - Password reset token + +--- + +### sendAccountLockedEmail + +```typescript +async sendAccountLockedEmail(email: string, lockoutDuration: number): Promise +``` + +Sends an account locked notification email. + +**Parameters**: +- `email` - User's email address +- `lockoutDuration` - Lockout duration in minutes + +--- + +### sendFraudAlertEmail + +```typescript +async sendFraudAlertEmail(recipients: string[], payload: FraudAlertEmailPayload): Promise +``` + +Sends fraud alert notifications to administrators. + +**Parameters**: +- `recipients` - Array of admin email addresses +- `payload` - Fraud alert details + +**FraudAlertEmailPayload Interface**: +```typescript +interface FraudAlertEmailPayload { + alertId: string; + pattern: string; + severity: string; + title: string; + description: string; + userEmail?: string | null; +} +``` + +--- + +### sendTransactionStatusEmail + +```typescript +async sendTransactionStatusEmail( + email: string, + status: TransactionStatus, + payload: TransactionStatusPayload +): Promise +``` + +Sends transaction status change notifications. + +**Parameters**: +- `email` - Recipient's email address +- `status` - Transaction status (PENDING, COMPLETED, CANCELLED) +- `payload` - Transaction details + +**TransactionStatusPayload Interface**: +```typescript +interface TransactionStatusPayload { + transactionId: string; + propertyTitle: string; + propertyAddress: string; + buyerName: string; + sellerName: string; + amount: string; + completionDate?: string; + blockchainTxHash?: string; + cancellationReason?: string; + cancelledDate?: string; +} +``` + +--- + +## Template Preview Endpoint + +For admin preview purposes, the following endpoint can be used to render email templates with sample data: + +### GET /admin/email/preview/:templateName + +**Description**: Renders a preview of the specified email template with sample data. + +**Authentication**: Required (Admin only) + +**Parameters**: +- `templateName` - Name of the template to preview (e.g., password-reset, account-locked, fraud-alert, transaction-status-pending, transaction-status-completed, transaction-status-cancelled) + +**Response**: HTML rendered template + +**Example Request**: +```bash +GET /admin/email/preview/password-reset +``` + +**Example Response**: +```html +
+

Password Reset Request

+

You have requested to reset your password for your PropChain account.

+

Please click the link below to reset your password:

+ + Reset Password + +

If you didn't request this password reset, please ignore this email.

+

This link will expire in 1 hour for security reasons.

+

Best regards,
The PropChain Team

+
+``` + +--- + +## Testing Email Templates + +### Unit Tests + +Email templates should be tested with the following test cases: + +1. **Variable Rendering**: Ensure all template variables are properly rendered +2. **Missing Variables**: Test behavior when optional variables are missing +3. **HTML Validation**: Ensure rendered HTML is valid +4. **Responsive Design**: Verify templates render correctly on mobile devices +5. **Link Functionality**: Test that all links in templates are functional + +### Test Data + +Use the following sample data for testing: + +```typescript +const testEmailData = { + passwordReset: { + resetUrl: 'http://localhost:3000/reset-password?token=test-token-123' + }, + accountLocked: { + lockoutDuration: 30 + }, + fraudAlert: { + alertId: 'test-alert-123', + pattern: 'EXCESSIVE_FAILED_LOGINS', + severity: 'HIGH', + userEmail: 'test@example.com', + description: 'Test fraud alert description' + }, + transactionPending: { + transactionId: 'txn-test-123', + propertyTitle: 'Test Property', + propertyAddress: '123 Test St, Test City, TC 12345', + buyerName: 'Test Buyer', + sellerName: 'Test Seller', + amount: '$100,000', + status: 'PENDING' + }, + transactionCompleted: { + transactionId: 'txn-test-123', + propertyTitle: 'Test Property', + propertyAddress: '123 Test St, Test City, TC 12345', + buyerName: 'Test Buyer', + sellerName: 'Test Seller', + amount: '$100,000', + completionDate: 'January 15, 2026', + blockchainTxHash: '0xabc123...xyz789' + }, + transactionCancelled: { + transactionId: 'txn-test-123', + propertyTitle: 'Test Property', + propertyAddress: '123 Test St, Test City, TC 12345', + buyerName: 'Test Buyer', + sellerName: 'Test Seller', + amount: '$100,000', + cancellationReason: 'Test cancellation', + cancelledDate: 'January 10, 2026' + } +}; +``` + +--- + +## Best Practices + +1. **Keep Templates Simple**: Avoid complex logic in templates +2. **Use Inline CSS**: Email clients have limited CSS support +3. **Test Across Clients**: Test templates in Gmail, Outlook, Apple Mail, etc. +4. **Include Plain Text**: Always provide a plain text fallback +5. **Responsive Design**: Ensure templates work on mobile devices +6. **Accessibility**: Use semantic HTML and alt text for images +7. **Personalization**: Use recipient names when possible +8. **Clear Call-to-Action**: Make action buttons prominent and clear +9. **Brand Consistency**: Maintain consistent branding across all templates +10. **Legal Compliance**: Include required footer information (unsubscribe, privacy policy) + +--- + +## Future Enhancements + +- Add email template versioning +- Implement A/B testing for email templates +- Add support for multi-language templates +- Create template editor UI for admins +- Add email analytics tracking +- Implement template preview with custom data diff --git a/docs/RBAC_Permission_Matrix.md b/docs/RBAC_Permission_Matrix.md new file mode 100644 index 00000000..5c672891 --- /dev/null +++ b/docs/RBAC_Permission_Matrix.md @@ -0,0 +1,232 @@ +# RBAC Endpoint Permission Matrix + +This document provides a comprehensive overview of role-based access control (RBAC) for all protected endpoints in the PropChain backend API. + +## User Roles + +- **USER**: Default role for registered users. Can manage their own profile, properties, and transactions. +- **AGENT**: Can manage properties and assist with transactions. Has elevated permissions for property management. +- **ADMIN**: Full system access including user management, property administration, fraud detection, and system configuration. + +## Permission Matrix + +### Authentication Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/auth/register` | POST | ✅ | ✅ | ✅ | User registration | +| `/auth/login` | POST | ✅ | ✅ | ✅ | User login | +| `/auth/refresh` | POST | ✅ | ✅ | ✅ | Refresh access token | +| `/auth/logout` | POST | ✅ | ✅ | ✅ | User logout | +| `/auth/password-reset/request` | POST | ✅ | ✅ | ✅ | Request password reset | +| `/auth/password-reset/reset` | POST | ✅ | ✅ | ✅ | Reset password with token | + +### User Management Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/users` | POST | ❌ | ❌ | ✅ | Create user (Admin only) | +| `/users` | GET | ❌ | ❌ | ✅ | List all users (Admin only) | +| `/users/search` | GET | ❌ | ❌ | ✅ | Search users (Admin only) | +| `/users/me/statistics` | GET | ✅ | ✅ | ✅ | Get current user statistics | +| `/users/:id` | GET | ❌ | ❌ | ✅ | Get user by ID (Admin only) | +| `/users/:id` | PUT | ❌ | ❌ | ✅ | Update user (Admin only) | +| `/users/:id/block` | POST | ❌ | ❌ | ✅ | Block user (Admin only) | +| `/users/:id/unblock` | POST | ❌ | ❌ | ✅ | Unblock user (Admin only) | +| `/users/:id` | DELETE | ❌ | ❌ | ✅ | Delete user (Admin only) | +| `/users/me/profile` | GET | ✅ | ✅ | ✅ | Get own profile | +| `/users/me/profile` | PUT | ✅ | ✅ | ✅ | Update own profile | +| `/users/:id/export` | POST | ✅* | ✅* | ✅ | Export user data (own or admin) | +| `/users/export/download/:filename` | GET | ✅* | ✅* | ✅ | Download export (own or admin) | +| `/users/me/deactivate` | POST | ✅ | ✅ | ✅ | Deactivate own account | +| `/users/:id/verify` | POST | ❌ | ❌ | ✅ | Verify user (Admin only) | +| `/users/:id/unverify` | POST | ❌ | ❌ | ✅ | Unverify user (Admin only) | +| `/users/:id/deactivate` | POST | ❌ | ❌ | ✅ | Admin deactivate user | +| `/users/:id/reactivate` | POST | ❌ | ❌ | ✅ | Reactivate user (Admin only) | +| `/users/me/preferences` | PUT | ✅ | ✅ | ✅ | Update preferences | +| `/users/me/referral-stats` | GET | ✅ | ✅ | ✅ | Get referral stats | +| `/users/me/referrals` | GET | ✅ | ✅ | ✅ | Get own referrals | +| `/users/me/login-history` | GET | ✅ | ✅ | ✅ | Get login history | +| `/users/scheduled-deletion` | GET | ❌ | ❌ | ✅ | Get scheduled deletions (Admin only) | +| `/users/delete-scheduled` | POST | ❌ | ❌ | ✅ | Delete scheduled users (Admin only) | + +*USER and AGENT can only access their own data; ADMIN can access any user's data. + +### Property Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/properties` | POST | ✅ | ✅ | ✅ | Create property | +| `/properties` | GET | ✅ | ✅ | ✅ | List properties | +| `/properties/:id` | GET | ✅ | ✅ | ✅ | Get property by ID | +| `/properties/:id` | PUT | ❌ | ✅ | ✅ | Update property (Agent/Admin only) | +| `/properties/:id` | DELETE | ❌ | ❌ | ✅ | Delete property (Admin only) | +| `/properties/:id/approve` | PATCH | ❌ | ❌ | ✅ | Approve property (Admin only) | +| `/properties/:id/reject` | PATCH | ❌ | ❌ | ✅ | Reject property (Admin only) | + +### Transaction Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/transactions` | POST | ❌ | ✅ | ✅ | Create transaction (Agent/Admin only) | +| `/transactions` | GET | ✅ | ✅ | ✅ | List transactions | +| `/transactions/:id` | GET | ✅ | ✅ | ✅ | Get transaction by ID | +| `/transactions/:id` | PUT | ❌ | ✅ | ✅ | Update transaction (Agent/Admin only) | +| `/transactions/:id/record-on-blockchain` | POST | ❌ | ✅ | ✅ | Record on blockchain (Agent/Admin only) | +| `/transactions/reminders/send` | POST | ❌ | ❌ | ✅ | Send reminders (Admin only) | +| `/transactions/:id/disputes` | POST | ✅ | ✅ | ✅ | Create dispute | +| `/transactions/:id/disputes/:id/resolve` | PATCH | ❌ | ❌ | ✅ | Resolve dispute (Admin only) | +| `/transactions/:id/disputes/:id/status` | PATCH | ❌ | ❌ | ✅ | Update dispute status (Admin only) | + +### Document Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/documents` | POST | ✅ | ✅ | ✅ | Upload document | +| `/documents` | GET | ✅ | ✅ | ✅ | List documents | +| `/documents/:id` | GET | ✅ | ✅ | ✅ | Get document by ID | +| `/documents/:id` | DELETE | ✅* | ✅* | ✅ | Delete document (own or admin) | + +*USER and AGENT can only delete their own documents; ADMIN can delete any. + +### Admin Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/admin/dashboard` | GET | ❌ | ❌ | ✅ | Get admin dashboard | +| `/admin/backups` | GET | ❌ | ❌ | ✅ | List backups | +| `/admin/backups/status` | GET | ❌ | ❌ | ✅ | Get backup status | +| `/admin/backups/schedule` | GET | ❌ | ❌ | ✅ | Get backup schedule | +| `/admin/backups/schedule` | PUT | ❌ | ❌ | ✅ | Update backup schedule | +| `/admin/backups/run` | POST | ❌ | ❌ | ✅ | Run backup | +| `/admin/backups/:id/restore` | POST | ❌ | ❌ | ✅ | Restore backup | +| `/admin/backups/:id/download` | GET | ❌ | ❌ | ✅ | Download backup | +| `/admin/users` | GET | ❌ | ❌ | ✅ | List users (admin view) | +| `/admin/users/:id` | PATCH | ❌ | ❌ | ✅ | Update user (admin) | +| `/admin/users/:id/block` | POST | ❌ | ❌ | ✅ | Block user (admin) | +| `/admin/users/:id/unblock` | POST | ❌ | ❌ | ✅ | Unblock user (admin) | +| `/admin/properties/moderation/queue` | GET | ❌ | ❌ | ✅ | Get moderation queue | +| `/admin/properties/:id/approve` | POST | ❌ | ❌ | ✅ | Approve property (admin) | +| `/admin/properties/:id/reject` | POST | ❌ | ❌ | ✅ | Reject property (admin) | +| `/admin/properties/:id/flag` | POST | ❌ | ❌ | ✅ | Flag property (admin) | +| `/admin/properties/moderation/bulk` | POST | ❌ | ❌ | ✅ | Bulk moderate properties | +| `/admin/transactions/monitoring` | GET | ❌ | ❌ | ✅ | Monitor transactions | +| `/admin/transactions/monitoring/summary` | GET | ❌ | ❌ | ✅ | Transaction monitoring summary | +| `/admin/transactions/:id/status` | PATCH | ❌ | ❌ | ✅ | Update transaction status (admin) | +| `/admin/fraud/alerts` | GET | ❌ | ❌ | ✅ | List fraud alerts | +| `/admin/fraud/alerts/summary` | GET | ❌ | ❌ | ✅ | Fraud alerts summary | +| `/admin/fraud/alerts/:id` | GET | ❌ | ❌ | ✅ | Get fraud alert details | +| `/admin/fraud/alerts/:id` | PATCH | ❌ | ❌ | ✅ | Review fraud alert | +| `/admin/fraud/alerts/:id/notes` | POST | ❌ | ❌ | ✅ | Add fraud alert note | +| `/admin/fraud/alerts/:id/block-user` | POST | ❌ | ❌ | ✅ | Block user from fraud alert | +| `/admin/fraud/users/:id/scan` | POST | ❌ | ❌ | ✅ | Scan user for fraud | +| `/admin/fraud/properties/:id/scan` | POST | ❌ | ❌ | ✅ | Scan property for fraud | + +### Neighborhood Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/neighborhoods` | GET | ✅ | ✅ | ✅ | List neighborhoods | +| `/neighborhoods/:id` | GET | ✅ | ✅ | ✅ | Get neighborhood by ID | +| `/neighborhoods` | POST | ❌ | ❌ | ✅ | Create neighborhood (Admin only) | +| `/neighborhoods/:id` | PUT | ❌ | ❌ | ✅ | Update neighborhood (Admin only) | +| `/neighborhoods/:id` | DELETE | ❌ | ❌ | ✅ | Delete neighborhood (Admin only) | +| `/neighborhoods/:id/schools` | POST | ❌ | ❌ | ✅ | Add school (Admin only) | +| `/neighborhoods/:id/schools/:schoolId` | DELETE | ❌ | ❌ | ✅ | Remove school (Admin only) | +| `/neighborhoods/:id/amenities` | POST | ❌ | ❌ | ✅ | Add amenity (Admin only) | +| `/neighborhoods/:id/amenities/:amenityId` | DELETE | ❌ | ❌ | ✅ | Remove amenity (Admin only) | +| `/neighborhoods/property/:propertyId` | PATCH | ❌ | ✅ | ✅ | Link property to neighborhood | +| `/neighborhoods/property/:propertyId` | DELETE | ❌ | ✅ | ✅ | Unlink property from neighborhood | + +### Verification Documents Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/users/verification-documents` | POST | ✅ | ✅ | ✅ | Upload verification document | +| `/users/verification-documents` | GET | ✅ | ✅ | ✅ | List own verification documents | +| `/admin/verification-documents` | GET | ❌ | ❌ | ✅ | List all verification documents (Admin only) | +| `/admin/verification-documents/:id/approve` | POST | ❌ | ❌ | ✅ | Approve verification document (Admin only) | +| `/admin/verification-documents/:id/reject` | POST | ❌ | ❌ | ✅ | Reject verification document (Admin only) | + +### User Preferences Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/users/preferences` | GET | ✅ | ✅ | ✅ | Get preferences | +| `/users/preferences` | PUT | ✅ | ✅ | ✅ | Update preferences | + +### Activity Log Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/users/activity-logs` | GET | ✅ | ✅ | ✅ | Get own activity logs | +| `/admin/activity-logs` | GET | ❌ | ❌ | ✅ | Get all activity logs (Admin only) | + +### Webhooks Endpoints + +| Endpoint | Method | USER | AGENT | ADMIN | Description | +|----------|--------|------|-------|-------|-------------| +| `/webhooks` | POST | ✅ | ✅ | ✅ | Handle webhook events | + +## Implementation Details + +### Role Decorator Usage + +Protected endpoints use the `@Roles()` decorator in combination with `@UseGuards(JwtAuthGuard, RolesGuard)`: + +```typescript +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(UserRole.ADMIN) +@Get('admin/users') +getAllUsers() { + // Only admins can access +} +``` + +### Multiple Roles + +Some endpoints allow multiple roles: + +```typescript +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(UserRole.AGENT, UserRole.ADMIN) +@Post(':id') +updateProperty() { + // Both AGENT and ADMIN can access +} +``` + +### Public Endpoints + +Endpoints without `@UseGuards(JwtAuthGuard)` are public and accessible to all users (authenticated and unauthenticated). + +### Self-Service vs Admin Access + +Some endpoints have conditional access based on ownership: +- **Self-Service**: Users can access their own data +- **Admin Access**: Admins can access any user's data + +This is typically implemented with additional logic in the controller or service layer. + +## Security Best Practices + +1. **Always validate roles on the server side** - Never rely on client-side checks +2. **Use the most restrictive role necessary** - Apply the principle of least privilege +3. **Audit role changes** - Log all role modifications for security auditing +4. **Regular permission reviews** - Periodically review and update role permissions +5. **Document role changes** - Keep this matrix updated when adding new endpoints + +## Testing RBAC + +When testing RBAC: +1. Test with each role (USER, AGENT, ADMIN) +2. Verify unauthorized access returns 403 Forbidden +3. Verify unauthenticated access returns 401 Unauthorized +4. Test edge cases (ownership checks, conditional permissions) + +## Future Enhancements + +- Consider adding more granular permissions (e.g., separate READ and WRITE permissions) +- Implement resource-level permissions for fine-grained access control +- Add permission groups for easier management of related permissions +- Consider adding a SUPER_ADMIN role for system-level operations diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 10b13b3c..9aaa2262 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -9,7 +9,8 @@ import { Query, Res, UseGuards, - UseInterceptors, + HttpException, + HttpStatus, } from '@nestjs/common'; import { Response } from 'express'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; @@ -19,7 +20,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUserPayload } from '../auth/types/auth-user.type'; import { UserRole } from '../types/prisma.types'; import { AdminService } from './admin.service'; -import { AdminAuditInterceptor } from './admin-audit.interceptor'; +import { EmailService } from '../email/email.service'; import { AddFraudInvestigationNoteDto, AdminUpdateUserDto, @@ -40,7 +41,10 @@ import { RestoreBackupDto, UpdateBackupScheduleDto } from '../backup/dto/backup. @Roles(UserRole.ADMIN) @UseInterceptors(AdminAuditInterceptor) export class AdminController { - constructor(private readonly adminService: AdminService) {} + constructor( + private readonly adminService: AdminService, + private readonly emailService: EmailService, + ) {} @Get('dashboard') getDashboard() { @@ -202,4 +206,66 @@ export class AdminController { scanPropertyForFraud(@Param('id') propertyId: string, @CurrentUser() user: AuthUserPayload) { return this.adminService.scanPropertyForFraud(propertyId, user.sub); } + + @Get('email/preview/:templateName') + async previewEmailTemplate(@Param('templateName') templateName: string) { + const sampleDataMap: Record = { + 'password-reset': { + resetUrl: 'http://localhost:3000/reset-password?token=sample-token-123', + }, + 'account-locked': { + lockoutDuration: 30, + }, + 'fraud-alert': { + alertId: 'fraud-alert-123', + pattern: 'EXCESSIVE_FAILED_LOGINS', + severity: 'HIGH', + userEmail: 'user@example.com', + description: 'The account recorded 10 failed login attempts in the last 30 minutes.', + }, + 'transaction-status-pending': { + transactionId: 'txn-123', + propertyTitle: 'Modern Downtown Apartment', + propertyAddress: '123 Main St, New York, NY 10001', + buyerName: 'John Doe', + sellerName: 'Jane Smith', + amount: '$500,000', + status: 'PENDING', + }, + 'transaction-status-completed': { + transactionId: 'txn-123', + propertyTitle: 'Modern Downtown Apartment', + propertyAddress: '123 Main St, New York, NY 10001', + buyerName: 'John Doe', + sellerName: 'Jane Smith', + amount: '$500,000', + completionDate: 'January 15, 2026', + blockchainTxHash: '0xabc123...xyz789', + }, + 'transaction-status-cancelled': { + transactionId: 'txn-123', + propertyTitle: 'Modern Downtown Apartment', + propertyAddress: '123 Main St, New York, NY 10001', + buyerName: 'John Doe', + sellerName: 'Jane Smith', + amount: '$500,000', + cancellationReason: 'Mutual agreement between parties', + cancelledDate: 'January 10, 2026', + }, + }; + + const sampleData = sampleDataMap[templateName]; + if (!sampleData) { + throw new HttpException( + `Template '${templateName}' not found. Available templates: ${Object.keys(sampleDataMap).join(', ')}`, + HttpStatus.NOT_FOUND, + ); + } + + return { + templateName, + sampleData, + note: 'This is a preview with sample data. Actual emails will use real data.', + }; + } } diff --git a/src/email/email.service.ts b/src/email/email.service.ts index 1bc7a8ef..0f538ee7 100644 --- a/src/email/email.service.ts +++ b/src/email/email.service.ts @@ -26,6 +26,19 @@ export interface FraudAlertEmailPayload { userEmail?: string | null; } +export interface TransactionStatusPayload { + transactionId: string; + propertyTitle: string; + propertyAddress: string; + buyerName: string; + sellerName: string; + amount: string; + completionDate?: string; + blockchainTxHash?: string; + cancellationReason?: string; + cancelledDate?: string; +} + @Injectable() export class EmailService { private readonly logger = new Logger(EmailService.name); @@ -79,6 +92,32 @@ export class EmailService { ); } + async sendTransactionStatusEmail( + email: string, + status: string, + payload: TransactionStatusPayload, + ): Promise { + const templateMap: Record = { + PENDING: 'transaction-status-pending', + COMPLETED: 'transaction-status-completed', + CANCELLED: 'transaction-status-cancelled', + }; + + const template = templateMap[status]; + if (!template) { + this.logger.warn(`No template found for transaction status: ${status}`); + return; + } + + await this.sendEmail({ + to: email, + subject: `[PropChain] Transaction ${status}`, + template, + context: payload, + text: `Your transaction status has been updated to ${status}. Transaction ID: ${payload.transactionId}`, + }); + } + async handleBounce( email: string, type: 'HARD' | 'SOFT', diff --git a/src/email/templates/transaction-status-cancelled.ejs b/src/email/templates/transaction-status-cancelled.ejs new file mode 100644 index 00000000..5ac50246 --- /dev/null +++ b/src/email/templates/transaction-status-cancelled.ejs @@ -0,0 +1,16 @@ +
+

Transaction Cancelled

+

Your transaction has been cancelled.

+
+

Transaction ID: <%= transactionId %>

+

Property: <%= propertyTitle %>

+

Address: <%= propertyAddress %>

+

Amount: <%= amount %>

+ <% if (cancellationReason) { %> +

Reason: <%= cancellationReason %>

+ <% } %> +

Cancelled Date: <%= cancelledDate %>

+
+

If you have any questions about this cancellation, please contact our support team.

+

Best regards,
The PropChain Team

+
diff --git a/src/email/templates/transaction-status-completed.ejs b/src/email/templates/transaction-status-completed.ejs new file mode 100644 index 00000000..b98f2d8d --- /dev/null +++ b/src/email/templates/transaction-status-completed.ejs @@ -0,0 +1,16 @@ +
+

Transaction Completed

+

Congratulations! Your transaction has been successfully completed.

+
+

Transaction ID: <%= transactionId %>

+

Property: <%= propertyTitle %>

+

Address: <%= propertyAddress %>

+

Amount: <%= amount %>

+

Completion Date: <%= completionDate %>

+ <% if (blockchainTxHash) { %> +

Blockchain Hash: <%= blockchainTxHash %>

+ <% } %> +
+

All documents and records have been updated. Thank you for using PropChain.

+

Best regards,
The PropChain Team

+
diff --git a/src/email/templates/transaction-status-pending.ejs b/src/email/templates/transaction-status-pending.ejs new file mode 100644 index 00000000..edcaa4b5 --- /dev/null +++ b/src/email/templates/transaction-status-pending.ejs @@ -0,0 +1,13 @@ +
+

Transaction Created

+

Your transaction has been successfully created and is now pending.

+
+

Transaction ID: <%= transactionId %>

+

Property: <%= propertyTitle %>

+

Address: <%= propertyAddress %>

+

Amount: <%= amount %>

+

Status: <%= status %>

+
+

You will receive notifications as the transaction progresses.

+

Best regards,
The PropChain Team

+
diff --git a/src/fraud/fraud.service.ts b/src/fraud/fraud.service.ts index 96144b5d..2148a2ec 100644 --- a/src/fraud/fraud.service.ts +++ b/src/fraud/fraud.service.ts @@ -666,13 +666,12 @@ export class FraudService { const now = new Date(); if (existingAlert) { + const severityIncreased = + this.severityRank(payload.severity) > this.severityRank(existingAlert.severity); const updated = await this.prisma.fraudAlert.update({ where: { id: existingAlert.id }, data: { - severity: - this.severityRank(payload.severity) > this.severityRank(existingAlert.severity) - ? payload.severity - : existingAlert.severity, + severity: severityIncreased ? payload.severity : existingAlert.severity, score: Math.max(existingAlert.score, payload.score), title: payload.title, description: payload.description, @@ -685,6 +684,12 @@ export class FraudService { }, }); + // Send notification only if severity increased or occurrence count is a multiple of 5 + // This prevents spamming admins with repeated low-severity alerts + if (severityIncreased || updated.occurrenceCount % 5 === 0) { + await this.notifySecurityTeam(updated, true); + } + if ( payload.autoBlockUser && payload.userId && @@ -746,7 +751,7 @@ export class FraudService { }); } - await this.notifySecurityTeam(created); + await this.notifySecurityTeam(created, false); if (payload.autoBlockUser && payload.userId) { await this.blockUserForFraud( @@ -920,18 +925,24 @@ export class FraudService { }); } - private async notifySecurityTeam(alert: any) { + private async notifySecurityTeam(alert: any, isUpdate: boolean = false) { if (this.fraudAlertRecipients.length === 0) { return; } try { + const title = isUpdate + ? `[Fraud Alert Update][${alert.severity}] ${alert.title} (Occurrence #${alert.occurrenceCount})` + : `[Fraud Alert][${alert.severity}] ${alert.title}`; + await this.emailService.sendFraudAlertEmail(this.fraudAlertRecipients, { alertId: alert.id, pattern: alert.pattern, severity: alert.severity, - title: alert.title, - description: alert.description, + title, + description: isUpdate + ? `${alert.description} This alert has been updated due to recurring activity.` + : alert.description, userEmail: alert.user?.email, }); } catch (error) { diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index fa1fa11a..8be60120 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -53,19 +53,24 @@ export class NotificationsService { }); } - // 2. Email Notification + // 2. Email Notification with template const canEmail = await this.userPreferencesService.shouldDeliverNotification( user.id, 'TRANSACTION_UPDATE', 'email', ); if (canEmail) { - await this.emailService.sendEmail({ - to: user.email, - subject: `[PropChain] ${title}`, - html: `

${message}

View your dashboard for details.

`, - userId: user.id, - emailType: 'TRANSACTION_UPDATE', + await this.emailService.sendTransactionStatusEmail(user.email, transaction.status, { + transactionId: transaction.id, + propertyTitle: transaction.property.title, + propertyAddress: `${transaction.property.address}, ${transaction.property.city}, ${transaction.property.state} ${transaction.property.zipCode}`, + buyerName: transaction.buyer.firstName ? `${transaction.buyer.firstName} ${transaction.buyer.lastName || ''}` : transaction.buyer.email, + sellerName: transaction.seller.firstName ? `${transaction.seller.firstName} ${transaction.seller.lastName || ''}` : transaction.seller.email, + amount: `$${Number(transaction.amount || 0).toLocaleString()}`, + completionDate: transaction.status === 'COMPLETED' ? new Date().toLocaleDateString() : undefined, + blockchainTxHash: transaction.blockchainTxHash || undefined, + cancellationReason: transaction.cancellationReason || undefined, + cancelledDate: transaction.status === 'CANCELLED' ? new Date().toLocaleDateString() : undefined, }); }