fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
244
server/services/email.service.ts
Normal file
244
server/services/email.service.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user