Merge pull request 'Email verification Backend' (#72) from Backend_Fix into main

Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
2025-10-24 23:34:35 +00:00
13 changed files with 843 additions and 101 deletions
+582 -41
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -50,25 +50,25 @@
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"pg": "^8.16.3", "pg": "^8.16.3",
"redis": "^5.8.1", "redis": "^5.8.1",
"sharp": "^0.34.4",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typeorm": "^0.3.26", "typeorm": "^0.3.26",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"winston": "^3.17.0", "winston": "^3.17.0"
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/multer": "^2.0.0",
"@types/nodemailer": "^7.0.1",
"@types/uuid": "^10.0.0",
"@jest/globals": "^30.0.5", "@jest/globals": "^30.0.5",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^24.3.3", "@types/node": "^24.3.3",
"@types/nodemailer": "^7.0.1",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/redis": "^4.0.10", "@types/redis": "^4.0.10",
"@types/socket.io": "^3.0.1", "@types/socket.io": "^3.0.1",
@@ -76,6 +76,7 @@
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/swagger-jsdoc": "^6.0.4", "@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/uuid": "^10.0.0",
"jest": "^30.0.5", "jest": "^30.0.5",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"rimraf": "^5.0.10", "rimraf": "^5.0.10",
@@ -385,11 +385,12 @@ router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) =>
// Hard delete deck (admin only) // Hard delete deck (admin only)
router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => { router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => {
try { try {
const adminUserId = (req as any).user.userId;
const deckId = req.params.id; const deckId = req.params.id;
logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId }); logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId });
const result = await container.deleteDeckCommandHandler.execute({ id: deckId, soft: false }); const result = await container.deleteDeckCommandHandler.execute({ userid: adminUserId, authLevel: 1, id: deckId, soft: false });
logRequest('Admin deck hard delete successful', req, res, { deckId, success: result }); logRequest('Admin deck hard delete successful', req, res, { deckId, success: result });
res.json({ success: result }); res.json({ success: result });
} catch (error) { } catch (error) {
@@ -1,6 +1,7 @@
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import sharp from 'sharp';
import { logError, logAuth, logStartup } from './Logger'; import { logError, logAuth, logStartup } from './Logger';
import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper'; import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper';
@@ -28,9 +29,14 @@ export class EmailService {
private transporter!: nodemailer.Transporter; private transporter!: nodemailer.Transporter;
private config: EmailConfig; private config: EmailConfig;
private templatesPath: string; private templatesPath: string;
private logoPath: string;
private resizedLogoBuffer?: Buffer;
constructor() { constructor() {
this.templatesPath = path.join(__dirname, '../../Templates'); this.templatesPath = path.join(__dirname, '../../Templates');
this.logoPath = path.join(__dirname, '../../../assets/Logo.png');
// Load logo asynchronously after initialization
this.loadLogoBase64().catch(err => console.error('[EmailService] Error loading logo:', err));
this.config = { this.config = {
host: process.env.EMAIL_HOST || 'smtp.gmail.com', host: process.env.EMAIL_HOST || 'smtp.gmail.com',
@@ -63,6 +69,32 @@ export class EmailService {
} }
} }
/**
* Load and resize logo for email attachments - 32x32 pixels
*/
private async loadLogoBase64(): Promise<void> {
try {
if (fs.existsSync(this.logoPath)) {
const logoBuffer = fs.readFileSync(this.logoPath);
// Resize to 60x60 pixels with high quality and centered
this.resizedLogoBuffer = await sharp(logoBuffer)
.resize(60, 60, {
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 1 },
position: 'center'
})
.png()
.toBuffer();
console.log(`[EmailService] ✅ Logo loaded and resized to 60x60`);
}
} catch (error) {
console.error(`[EmailService] ❌ Failed to load/resize logo:`, error);
logError('Failed to load logo for emails', error instanceof Error ? error : new Error(String(error)));
}
}
/** /**
* Send email with template * Send email with template
* @param options - Email options including template and data * @param options - Email options including template and data
@@ -73,19 +105,29 @@ export class EmailService {
let textContent = options.text; let textContent = options.text;
if (options.template) { if (options.template) {
const templateResult = await this.loadTemplate(options.template, options.templateData || {}); const templateResult = await this.loadTemplate(options.template, options.templateData);
htmlContent = templateResult.html; htmlContent = templateResult.html;
textContent = templateResult.text; textContent = templateResult.text;
} }
const mailOptions = { const mailOptions: any = {
from: this.config.from, from: this.config.from,
to: options.to, to: options.to,
subject: options.subject, subject: options.subject,
html: htmlContent, html: htmlContent,
text: textContent text: textContent,
attachments: []
}; };
// Add logo as CID attachment if available
if (this.resizedLogoBuffer) {
mailOptions.attachments.push({
filename: 'logo.png',
content: this.resizedLogoBuffer,
cid: 'logo@serpentrace' // Content-ID for referencing in HTML
});
}
const result = await this.transporter.sendMail(mailOptions); const result = await this.transporter.sendMail(mailOptions);
logAuth('Email sent successfully', undefined, { logAuth('Email sent successfully', undefined, {
messageId: result.messageId, messageId: result.messageId,
@@ -22,18 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
padding-bottom: 20px;
}
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
} }
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2c5aa0; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.greeting { .greeting {
font-size: 16px; font-size: 16px;
@@ -98,9 +111,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{companyName}}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Antwort auf Ihre {{contactTypeString}}</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">🐍 SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Antwort auf Ihre {{contactTypeString}}</div>
<div class="greeting"> <div class="greeting">
Hallo {{contactName}}, Hallo {{contactName}},
@@ -22,18 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
padding-bottom: 20px;
}
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
} }
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2c5aa0; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.greeting { .greeting {
font-size: 16px; font-size: 16px;
@@ -98,9 +111,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{companyName}}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Válasz az Ön {{contactTypeString}} üzenetére</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">🐍 SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Válasz az ön {{contactTypeString}}</div>
<div class="greeting"> <div class="greeting">
Kedves {{contactName}}! Kedves {{contactName}}!
@@ -22,18 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
padding-bottom: 20px;
}
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
} }
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2c5aa0; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.greeting { .greeting {
font-size: 16px; font-size: 16px;
@@ -98,9 +111,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{companyName}}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Response to Your {{contactTypeString}}</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">🐍 SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Response to Your {{contactTypeString}}</div>
<div class="greeting"> <div class="greeting">
Hello {{contactName}}, Hello {{contactName}},
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #FF9800;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #E65100; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -123,9 +135,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo"><img src="../../assets/Logo.png" alt="{{ companyName }}" /> {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Passwort zurücksetzen</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Passwort zurücksetzen</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #FF9800;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #E65100; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -123,9 +135,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo"><img src="../../assets/Logo.png" alt="{{ companyName }}" /> {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Jelszó visszaállítás kérése</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Jelszó visszaállítás kérése</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #FF9800;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #E65100; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -123,9 +135,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo"><img src="../../assets/Logo.png" alt="{{ companyName }}" /> {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Password Reset Request</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Password Reset Request</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2E7D32; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -115,9 +127,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo"><img src="../../assets/Logo.png" alt="{{ companyName }}" /> {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Konto verifizieren</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Konto verifizieren</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2E7D32; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -115,9 +127,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo"><img src="../../assets/Logo.png" alt="{{ companyName }}" /> {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Fiók megerősítése</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Fiók megerősítése</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,32 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
vertical-align: middle;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2E7D32; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -115,9 +128,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo"><img src="../../assets/Logo.png" alt="{{ companyName }}" /> {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Account Verification</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Account Verification</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">