feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
continuous-integration/drone/push Build is failing
continuous-integration/drone/push Build is failing
- 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
This commit is contained in:
+98
-10
@@ -11,6 +11,49 @@ interface EmailSettings {
|
||||
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;
|
||||
|
||||
@@ -19,7 +62,6 @@ export class EmailService {
|
||||
}
|
||||
|
||||
private async getTransporter() {
|
||||
// Try to get settings from DB
|
||||
const host = await this.storage.getSystemSettings('smtp_host');
|
||||
const port = await this.storage.getSystemSettings('smtp_port');
|
||||
const user = await this.storage.getSystemSettings('smtp_user');
|
||||
@@ -27,7 +69,6 @@ export class EmailService {
|
||||
const from = await this.storage.getSystemSettings('smtp_from');
|
||||
const secure = await this.storage.getSystemSettings('smtp_secure');
|
||||
|
||||
// Fallback to Env or MailHog defaults
|
||||
const settings: EmailSettings = {
|
||||
host: host || process.env.SMTP_HOST || 'localhost',
|
||||
port: port ? parseInt(port) : (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 1025),
|
||||
@@ -45,19 +86,26 @@ export class EmailService {
|
||||
user: settings.user,
|
||||
pass: settings.pass
|
||||
} : undefined,
|
||||
ignoreTLS: !settings.secure // useful for MailHog
|
||||
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: 'Welcome to TaskFlow!',
|
||||
text: `Hi ${user.username},\n\nWelcome to TaskFlow! We're excited to have you on board.\n\nBest,\nThe TaskFlow Team`,
|
||||
html: `<h1>Welcome to TaskFlow!</h1><p>Hi ${user.username},</p><p>We're excited to have you on board.</p><p>Best,<br>The TaskFlow Team</p>`
|
||||
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;
|
||||
@@ -69,17 +117,25 @@ export class EmailService {
|
||||
|
||||
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();
|
||||
// TODO: Get base URL from settings or env
|
||||
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: 'Reset your TaskFlow Password',
|
||||
text: `Hi ${user.username},\n\nYou requested a password reset. Click the link below to reset your password:\n\n${resetLink}\n\nIf you didn't request this, please ignore this email.\n\nThis link expires in 1 hour.`,
|
||||
html: `<h1>Reset Password</h1><p>Hi ${user.username},</p><p>You requested a password reset. Click the link below to reset your password:</p><p><a href="${resetLink}">Reset Password</a></p><p>If you didn't request this, please ignore this email.</p><p>This link expires in 1 hour.</p>`
|
||||
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;
|
||||
@@ -89,6 +145,38 @@ export class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
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>';
|
||||
|
||||
Reference in New Issue
Block a user