Files
iistwin/server/services/email.service.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

245 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MailService } from '@sendgrid/mail';
if (!process.env.SENDGRID_API_KEY) {
console.warn("SENDGRID_API_KEY environment variable not set. Email functionality will be disabled.");
}
const mailService = new MailService();
if (process.env.SENDGRID_API_KEY) {
mailService.setApiKey(process.env.SENDGRID_API_KEY);
}
const FROM_EMAIL = process.env.FROM_EMAIL || 'noreply@iistwin.ru';
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5000';
export class EmailService {
async sendVerificationEmail(
email: string,
firstName: string,
verificationToken: string,
organizationName: string
): Promise<boolean> {
if (!process.env.SENDGRID_API_KEY) {
console.log(`Mock verification email sent to ${email}`);
return true;
}
try {
const verificationUrl = `${FRONTEND_URL}/verify-email?token=${verificationToken}`;
await mailService.send({
to: email,
from: FROM_EMAIL,
subject: `Подтверждение email в ${organizationName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1976d2;">Добро пожаловать в ${organizationName}!</h2>
<p>Здравствуйте, ${firstName}!</p>
<p>Для завершения регистрации перейдите по ссылке:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${verificationUrl}"
style="background: #1976d2; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
Подтвердить email
</a>
</div>
<p style="color: #666;">Ссылка действительна в течение 24 часов.</p>
<p style="color: #666; font-size: 12px;">
Если вы не регистрировались в нашей системе, просто проигнорируйте это письмо.
</p>
</div>
`
});
return true;
} catch (error) {
console.error('SendGrid verification email error:', error);
return false;
}
}
async sendInvitationEmail(
email: string,
firstName: string,
inviterName: string,
organizationName: string,
tempPassword: string
): Promise<boolean> {
if (!process.env.SENDGRID_API_KEY) {
console.log(`Mock invitation email sent to ${email}`);
return true;
}
try {
const loginUrl = `${FRONTEND_URL}/login`;
await mailService.send({
to: email,
from: FROM_EMAIL,
subject: `Приглашение в ${organizationName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1976d2;">Вы приглашены в ${organizationName}</h2>
<p>Здравствуйте, ${firstName}!</p>
<p>${inviterName} пригласил вас присоединиться к команде.</p>
<div style="background: #f5f5f5; padding: 20px; border-radius: 5px; margin: 20px 0;">
<p><strong>Ваши данные для входа:</strong></p>
<p>Email: ${email}</p>
<p>Временный пароль: <code style="background: #e0e0e0; padding: 2px 4px; border-radius: 3px;">${tempPassword}</code></p>
</div>
<p style="color: #d32f2f;"><strong>Важно:</strong> Обязательно смените пароль после первого входа.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${loginUrl}"
style="background: #1976d2; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
Войти в систему
</a>
</div>
<p style="color: #666; font-size: 12px;">
Если вы не ожидали этого приглашения, свяжитесь с администратором ${organizationName}.
</p>
</div>
`
});
return true;
} catch (error) {
console.error('SendGrid invitation email error:', error);
return false;
}
}
async sendInvitationLinkEmail(
email: string,
inviterName: string,
organizationName: string,
inviteLink: string
): Promise<boolean> {
if (!process.env.SENDGRID_API_KEY) {
console.log(`Mock invitation link email sent to ${email}, link: ${inviteLink}`);
return true;
}
try {
await mailService.send({
to: email,
from: FROM_EMAIL,
subject: `Приглашение в ${organizationName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1976d2;">Вы приглашены в ${organizationName}</h2>
<p>Здравствуйте!</p>
<p>${inviterName} пригласил вас присоединиться к команде.</p>
<p>Для завершения регистрации перейдите по ссылке:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${inviteLink}"
style="background: #1976d2; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
Принять приглашение
</a>
</div>
<p style="color: #666;">Приглашение действительно в течение 7 дней.</p>
<p style="color: #666; font-size: 12px;">
Если вы не ожидали этого приглашения, просто проигнорируйте это письмо.
</p>
</div>
`
});
return true;
} catch (error) {
console.error('SendGrid invitation link email error:', error);
return false;
}
}
async sendPasswordResetEmail(
email: string,
firstName: string,
resetToken: string,
organizationName: string,
returnTo?: string
): Promise<boolean> {
if (!process.env.SENDGRID_API_KEY) {
const returnToParam = returnTo ? `&returnTo=${encodeURIComponent(returnTo)}` : '';
console.log(`[DEV] Password reset link for ${email}: ${FRONTEND_URL}/reset-password?token=${resetToken}${returnToParam}`);
return true;
}
try {
const returnToParam = returnTo ? `&returnTo=${encodeURIComponent(returnTo)}` : '';
const resetUrl = `${FRONTEND_URL}/reset-password?token=${resetToken}${returnToParam}`;
await mailService.send({
to: email,
from: FROM_EMAIL,
subject: `Восстановление пароля - ${organizationName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1976d2;">Восстановление пароля</h2>
<p>Здравствуйте, ${firstName}!</p>
<p>Вы запросили восстановление пароля для вашей учетной записи в ${organizationName}.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${resetUrl}"
style="background: #1976d2; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
Восстановить пароль
</a>
</div>
<p style="color: #666;">Ссылка действительна в течение 1 часа.</p>
<p style="color: #666; font-size: 12px;">
Если вы не запрашивали восстановление пароля, просто проигнорируйте это письмо.
</p>
</div>
`
});
return true;
} catch (error) {
console.error('SendGrid password reset email error:', error);
return false;
}
}
async sendSuperAdminPasswordResetEmail(
email: string,
name: string,
resetToken: string
): Promise<boolean> {
if (!process.env.SENDGRID_API_KEY) {
console.log(`[DEV] Superadmin password reset link for ${email}: ${FRONTEND_URL}/superadmin/reset-password?token=${resetToken}`);
return true;
}
try {
const resetUrl = `${FRONTEND_URL}/superadmin/reset-password?token=${resetToken}`;
await mailService.send({
to: email,
from: FROM_EMAIL,
subject: 'Восстановление пароля суперадмина',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #7c3aed;">Восстановление пароля суперадмина</h2>
<p>Здравствуйте, ${name}!</p>
<p>Вы запросили сброс пароля для панели суперадмина.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${resetUrl}"
style="background: #7c3aed; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
Задать новый пароль
</a>
</div>
<p style="color: #666;">Ссылка действительна в течение 1 часа.</p>
<p style="color: #666; font-size: 12px;">
Если вы не запрашивали восстановление пароля, просто проигнорируйте это письмо.
</p>
</div>
`
});
return true;
} catch (error) {
console.error('SendGrid superadmin password reset email error:', error);
return false;
}
}
}
export const emailService = new EmailService();