Files
paul 9819d8db0b
continuous-integration/drone/push Build is failing
feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
- implemented /notifications page with overdue/upcoming alerts
- Fixed sidebar scrolling in collapsed mode
- Moved notification button to sidebar footer
- Enhanced Achievements page with streak stats and tooltips
- Improved XP history to show task titles
- Added missing translations (en/de)
- Removed top bar header
- Fixed Docker environment routing
2025-12-17 10:06:36 +01:00

185 lines
6.7 KiB
TypeScript

import nodemailer from 'nodemailer';
import { IStorage } from './storage';
import { User } from '../shared/schema';
interface EmailSettings {
host: string;
port: number;
user?: string;
pass?: string;
from: string;
secure: boolean;
}
import { generateEmailHtml } from './email-template';
const TRANSLATIONS = {
en: {
welcome: {
subject: 'Welcome to TaskFlow!',
title: 'Welcome to TaskFlow!',
body: (name: string) => `Hi ${name}, we're excited to have you on board.`,
},
reset: {
subject: 'Reset your TaskFlow Password',
title: 'Reset Password',
body: (name: string) => `Hi ${name}, you requested a password reset. Click the button below to proceed.`,
action: 'Reset Password',
},
'2fa': {
subject: 'Your 2FA Verification Code',
title: 'Verification Code',
body: (name: string) => `Hi ${name}, your verification code is below.`,
}
},
de: {
welcome: {
subject: 'Willkommen bei TaskFlow!',
title: 'Willkommen bei TaskFlow!',
body: (name: string) => `Hallo ${name}, wir freuen uns, Sie an Bord zu haben.`,
},
reset: {
subject: 'Passwort zurücksetzen',
title: 'Passwort zurücksetzen',
body: (name: string) => `Hallo ${name}, Sie haben das Zurücksetzen Ihres Passworts angefordert. Klicken Sie auf den Button unten, um fortzufahren.`,
action: 'Passwort zurücksetzen',
},
'2fa': {
subject: 'Ihr 2FA-Verifizierungscode',
title: 'Verifizierungscode',
body: (name: string) => `Hallo ${name}, Ihr Verifizierungscode finden Sie unten.`,
}
}
} as const;
type Language = 'en' | 'de';
export class EmailService {
private storage: IStorage;
constructor(storage: IStorage) {
this.storage = storage;
}
private async getTransporter() {
const host = await this.storage.getSystemSettings('smtp_host');
const port = await this.storage.getSystemSettings('smtp_port');
const user = await this.storage.getSystemSettings('smtp_user');
const pass = await this.storage.getSystemSettings('smtp_pass');
const from = await this.storage.getSystemSettings('smtp_from');
const secure = await this.storage.getSystemSettings('smtp_secure');
const settings: EmailSettings = {
host: host || process.env.SMTP_HOST || 'localhost',
port: port ? parseInt(port) : (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 1025),
user: user || process.env.SMTP_USER,
pass: pass || process.env.SMTP_PASS,
from: from || process.env.SMTP_FROM || '"TaskFlow" <noreply@taskflow.local>',
secure: secure === 'true'
};
return nodemailer.createTransport({
host: settings.host,
port: settings.port,
secure: settings.secure,
auth: settings.user ? {
user: settings.user,
pass: settings.pass
} : undefined,
ignoreTLS: !settings.secure
});
}
async sendWelcomeEmail(user: User) {
try {
const lang = (user.language as Language) || 'en';
const t = TRANSLATIONS[lang] || TRANSLATIONS.en;
const transporter = await this.getTransporter();
const html = generateEmailHtml(lang, {
title: t.welcome.title,
body: t.welcome.body(user.username)
});
const info = await transporter.sendMail({
from: await this.getFromAddress(),
to: user.email,
subject: t.welcome.subject,
text: t.welcome.body(user.username), // basic text fallback
html: html
});
console.log(`[Email] Welcome email sent to ${user.email}: ${info.messageId}`);
return true;
} catch (error) {
console.error(`[Email] Failed to send welcome email to ${user.email}:`, error);
return false;
}
}
async sendPasswordResetEmail(user: User, token: string) {
try {
const lang = (user.language as Language) || 'en';
const t = TRANSLATIONS[lang] || TRANSLATIONS.en;
const transporter = await this.getTransporter();
const baseUrl = process.env.APP_URL || 'http://localhost:5001';
const resetLink = `${baseUrl}/reset-password?token=${token}`;
const html = generateEmailHtml(lang, {
title: t.reset.title,
body: t.reset.body(user.username),
actionUrl: resetLink,
actionText: t.reset.action
});
const info = await transporter.sendMail({
from: await this.getFromAddress(),
to: user.email,
subject: t.reset.subject,
text: `${t.reset.body(user.username)}\n\n${resetLink}`,
html: html
});
console.log(`[Email] Password reset email sent to ${user.email}: ${info.messageId}`);
return true;
} catch (error) {
console.error(`[Email] Failed to send reset email to ${user.email}:`, error);
return false;
}
}
async isConfigured(): Promise<boolean> {
const host = await this.storage.getSystemSettings('smtp_host');
return !!(host || process.env.SMTP_HOST);
}
async send2FACode(user: User, code: string) {
try {
const lang = (user.language as Language) || 'en';
const t = TRANSLATIONS[lang] || TRANSLATIONS.en; // Fallback to EN if lang not found
const transporter = await this.getTransporter();
const html = generateEmailHtml(lang, {
title: t['2fa'].title,
body: t['2fa'].body(user.username),
code: code
});
const info = await transporter.sendMail({
from: await this.getFromAddress(),
to: user.email,
subject: t['2fa'].subject,
text: `${t['2fa'].body(user.username)}\nCode: ${code}`,
html: html
});
console.log(`[Email] 2FA code sent to ${user.email}: ${info.messageId}`);
return true;
} catch (error) {
console.error(`[Email] Failed to send 2FA code to ${user.email}:`, error);
return false;
}
}
private async getFromAddress() {
const from = await this.storage.getSystemSettings('smtp_from');
return from || process.env.SMTP_FROM || '"TaskFlow" <noreply@taskflow.local>';
}
}