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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/SETUP_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=24h

# Email Configuration (NEW)
EMAIL_PROVIDER=smtp # smtp | sendgrid | ses
EMAIL_FROM=noreply@starked.edu # From address for all emails
EMAIL_HOST=smtp.example.com # SMTP host (for smtp provider)
EMAIL_PORT=587 # SMTP port
EMAIL_SECURE=false # Use TLS
EMAIL_USER=your-email-user # SMTP auth user
EMAIL_PASS=your-email-password # SMTP auth password
SENDGRID_API_KEY= # Required if EMAIL_PROVIDER=sendgrid
AWS_SES_REGION=us-east-1 # Required if EMAIL_PROVIDER=ses
FRONTEND_URL=http://localhost:3000 # Base URL for email links

# Transaction Queue Configuration
QUEUE_MAX_SIZE=10000
QUEUE_MAX_RETRIES=3
Expand Down
28 changes: 28 additions & 0 deletions backend/src/controllers/EnrollmentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Request, Response } from 'express';
import { EnrollmentService } from '../services/EnrollmentService';
import { PaymentService } from '../services/PaymentService';
import { NotificationService } from '../services/notificationService';
import { getEmailService } from '../services/emailService';
import {
Enrollment,
EnrollmentFilter,
Expand Down Expand Up @@ -340,6 +341,33 @@ export class EnrollmentController {
enrollment.userId,
certificate
);

// Send credential issued email
try {
const emailService = getEmailService();
const certData: any = certificate || {};
await emailService.sendEmail({
userId: enrollment.userId,
userEmail: (req as any).user?.email || enrollment.userId,
templateData: {
type: 'credentialIssued',
data: {
studentName: (req as any).user?.username || 'Learner',
credentialName: certData?.name || 'Course Credential',
credentialId: String(certData?.id || id),
courseName: enrollment.courseId,
issueDate: new Date().toISOString(),
txHash: String(certData?.txHash || ''),
credentialUrl: `${process.env.FRONTEND_URL || ''}/credentials/${certData?.id || id}`,
verifyUrl: `${process.env.FRONTEND_URL || ''}/verify/${certData?.id || id}`,
unsubscribeUrl: `${process.env.FRONTEND_URL || ''}/settings/notifications`,
privacyUrl: `${process.env.FRONTEND_URL || ''}/privacy`,
},
},
});
} catch (emailError) {
console.error('Failed to queue credential email:', emailError);
}
}

res.json({
Expand Down
47 changes: 47 additions & 0 deletions backend/src/controllers/PaymentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Request, Response } from 'express';
import { PaymentService } from '../services/PaymentService';
import { StellarPaymentService } from '../services/StellarPaymentService';
import { NotificationService } from '../services/notificationService';
import { getEmailService } from '../services/emailService';
import {
Payment,
PaymentIntent,
Expand Down Expand Up @@ -142,6 +143,52 @@ export class PaymentController {
transaction
);

// Send payment receipt email
try {
const emailService = getEmailService();
await emailService.sendEmail({
userId: transaction.userId,
userEmail: (req as any).user?.email || transaction.userId,
templateData: {
type: 'paymentReceipt',
data: {
studentName: (req as any).user?.username || 'Learner',
transactionId: transaction.id || paymentIntentId,
amount: transaction.amount?.toString() || '0',
currency: transaction.currency || 'USD',
courseName: transaction.courseId || 'Course',
paymentMethod: 'Stellar',
paymentDate: new Date().toISOString(),
txHash: signedTransactionXDR?.substring(0, 64) || '',
receiptUrl: `${process.env.FRONTEND_URL || ''}/receipts/${transaction.id || paymentIntentId}`,
unsubscribeUrl: `${process.env.FRONTEND_URL || ''}/settings/notifications`,
privacyUrl: `${process.env.FRONTEND_URL || ''}/privacy`,
},
},
});

// Send enrollment confirmation email
await emailService.sendEmail({
userId: transaction.userId,
userEmail: (req as any).user?.email || transaction.userId,
templateData: {
type: 'enrollmentConfirmation',
data: {
studentName: (req as any).user?.username || 'Learner',
courseName: transaction.courseId || 'Course',
instructorName: (req as any).user?.instructorName || 'StarkEd Instructor',
enrollmentId: transaction.enrollmentId || paymentIntentId,
startDate: new Date().toISOString(),
courseUrl: `${process.env.FRONTEND_URL || ''}/courses/${transaction.courseId || ''}`,
unsubscribeUrl: `${process.env.FRONTEND_URL || ''}/settings/notifications`,
privacyUrl: `${process.env.FRONTEND_URL || ''}/privacy`,
},
},
});
} catch (emailError) {
console.error('Failed to queue payment receipt email:', emailError);
}

res.json({
success: true,
data: {
Expand Down
29 changes: 29 additions & 0 deletions backend/src/controllers/assignmentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FileUploadService } from '../services/fileUploadService';
import { GradingService } from '../services/gradingService';
import { PlagiarismService } from '../services/plagiarismService';
import { NotificationService } from '../services/notificationService';
import { getEmailService } from '../services/emailService';
import { validateAssignment, validateSubmission } from '../utils/validation';
import { logger } from '../utils/logger';

Expand Down Expand Up @@ -358,6 +359,34 @@ export class AssignmentController {
// Notify student
await (this.notificationService as any).notifyGradeCreated(req.user.id, grade);

// Send assignment graded email
try {
const emailService = getEmailService();
const gradeData: any = grade || {};
await emailService.sendEmail({
userId: submission.studentId,
userEmail: (req as any).user?.email || submission.studentId,
templateData: {
type: 'assignmentGraded',
data: {
studentName: (req as any).user?.username || 'Learner',
assignmentTitle: gradingData.assignmentTitle || assignment.title || 'Assignment',
courseName: gradingData.courseName || assignment.courseId || 'Course',
earnedPoints: gradingData.earnedPoints || 0,
totalPoints: gradingData.totalPoints || 100,
percentage: gradingData.percentage || Math.round(((gradingData.earnedPoints || 0) / (gradingData.totalPoints || 100)) * 100),
letterGrade: gradeData.letterGrade || 'N/A',
feedback: gradingData.feedback || undefined,
assignmentUrl: `${process.env.FRONTEND_URL || ''}/assignments/${submission.assignmentId}`,
unsubscribeUrl: `${process.env.FRONTEND_URL || ''}/settings/notifications`,
privacyUrl: `${process.env.FRONTEND_URL || ''}/privacy`,
},
},
});
} catch (emailError) {
logger.error('Failed to queue assignment graded email:', emailError);
}

logger.info(`Submission graded: ${submissionId} by ${req.user.id}`);
res.status(201).json(grade);
} catch (error) {
Expand Down
91 changes: 91 additions & 0 deletions backend/src/controllers/userController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import { userService } from '../services/userService';
import { getEmailService } from '../services/emailService';
import logger from '../utils/logger';

export const userController = {
Expand Down Expand Up @@ -51,6 +52,13 @@ export const userController = {
const settingsData = req.body;

const updatedSettings = await userService.updateSettings(userId, settingsData);

// If email preferences were updated, sync with email service
if (settingsData.emailPreferences) {
const emailService = getEmailService();
emailService.setUserPreferences(userId, settingsData.emailPreferences);
}

res.json(updatedSettings);
} catch (error) {
logger.error('Error in updateSettings controller', error);
Expand Down Expand Up @@ -78,5 +86,88 @@ export const userController = {
logger.error('Error in getStats controller', error);
res.status(500).json({ error: 'Internal server error' });
}
},

/**
* Update password — triggers password-changed security email.
*/
changePassword: async (req: Request, res: Response) => {
try {
const { address } = req.params;
const { newPassword } = req.body;

// In production, validate current password and hash the new one
logger.info(`Password change requested for ${address}`);

// Send password changed security email (cannot be opted out)
try {
const emailService = getEmailService();
await emailService.sendEmail({
userId: address,
userEmail: req.body.email || address,
templateData: {
type: 'passwordChanged',
data: {
studentName: req.body.username || 'User',
changeDate: new Date().toISOString(),
ipAddress: req.ip || 'Unknown',
securityUrl: `${process.env.FRONTEND_URL || ''}/security`,
unsubscribeUrl: `${process.env.FRONTEND_URL || ''}/settings/notifications`,
privacyUrl: `${process.env.FRONTEND_URL || ''}/privacy`,
},
},
});
} catch (emailError) {
logger.error('Failed to queue password changed email:', emailError);
}

res.json({ success: true, message: 'Password changed successfully' });
} catch (error) {
logger.error('Error in changePassword controller', error);
res.status(500).json({ error: 'Internal server error' });
}
},

/**
* Handle new login — triggers new-login security alert email.
*/
onLogin: async (req: Request, res: Response) => {
try {
const { address } = req.params;
const userAgent = req.headers['user-agent'] || 'Unknown';
const ip = req.ip || 'Unknown';

logger.info(`Login detected for ${address}`);

// Send new login security alert email (cannot be opted out)
try {
const emailService = getEmailService();
await emailService.sendEmail({
userId: address,
userEmail: req.body.email || address,
templateData: {
type: 'newLoginAlert',
data: {
studentName: req.body.username || 'User',
loginDate: new Date().toISOString(),
userAgent,
ipAddress: ip,
location: req.body.location || 'Unknown',
unrecognizedDevice: req.body.unrecognizedDevice || false,
securityUrl: `${process.env.FRONTEND_URL || ''}/security`,
unsubscribeUrl: `${process.env.FRONTEND_URL || ''}/settings/notifications`,
privacyUrl: `${process.env.FRONTEND_URL || ''}/privacy`,
},
},
});
} catch (emailError) {
logger.error('Failed to queue new login alert email:', emailError);
}

res.json({ success: true, message: 'Login recorded' });
} catch (error) {
logger.error('Error in onLogin controller', error);
res.status(500).json({ error: 'Internal server error' });
}
}
};
9 changes: 9 additions & 0 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ dotenv.config();
// Connect to Redis
connectRedis();

// Register email queue handler for async email delivery
try {
const { registerEmailQueueHandler } = require('./services/emailService');
registerEmailQueueHandler();
console.log('📧 Email queue handler registered');
} catch (err) {
console.warn('Warning: Could not register email queue handler:', err.message);
}

// Helper for default-exported route modules
const resolveRoute = (routeModule) => routeModule.default || routeModule;

Expand Down
24 changes: 23 additions & 1 deletion backend/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,26 @@ export interface UserStats {
totalCredentials: number;
totalAchievements: number;
reputation: number;
}
}

/**
* Email notification preferences per event type.
* Security emails (passwordChanged, newLoginAlert) cannot be opted out.
*/
export interface EmailPreferences {
enrollmentConfirmation: boolean;
credentialIssued: boolean;
paymentReceipt: boolean;
assignmentGraded: boolean;
passwordChanged: boolean;
newLoginAlert: boolean;
}

export const DEFAULT_EMAIL_PREFERENCES: EmailPreferences = {
enrollmentConfirmation: true,
credentialIssued: true,
paymentReceipt: true,
assignmentGraded: true,
passwordChanged: true,
newLoginAlert: true,
};
Loading
Loading