- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
245 lines
10 KiB
TypeScript
245 lines
10 KiB
TypeScript
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();
|