fix(number-fields): избегаем потери точности длинных чисел

- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
2026-07-07 21:03:40 +03:00
commit 1f5ecb6da4
1089 changed files with 237546 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
import { storage } from '../storage';
import { hashPassword, verifyPassword } from '../utils/password';
import { generateSuperAdminToken, generateResetToken } from '../utils/jwt';
import { emailService } from './email.service';
export interface SuperAdminLoginResult {
success: boolean;
token?: string;
superAdmin?: { id: number; email: string; name: string };
error?: string;
}
interface ResetTokenEntry {
superAdminId: number;
expiresAt: number;
}
const resetTokenStore = new Map<string, ResetTokenEntry>();
const RESET_TOKEN_TTL_MS = 60 * 60 * 1000;
export class SuperAdminService {
async login(email: string, password: string): Promise<SuperAdminLoginResult> {
try {
const superAdmin = await storage.getSuperAdminByEmail(email);
if (!superAdmin) {
return { success: false, error: 'Неверный email или пароль' };
}
const isValid = await verifyPassword(password, superAdmin.passwordHash);
if (!isValid) {
return { success: false, error: 'Неверный email или пароль' };
}
await storage.updateSuperAdminLastLogin(superAdmin.id);
const token = generateSuperAdminToken({
superAdminId: superAdmin.id,
email: superAdmin.email,
isSuperAdmin: true,
});
return {
success: true,
token,
superAdmin: { id: superAdmin.id, email: superAdmin.email, name: superAdmin.name },
};
} catch (error) {
console.error('SuperAdmin login error:', error);
return { success: false, error: 'Ошибка при входе' };
}
}
async seedFirstSuperAdmin(email: string, password: string, name: string): Promise<{ success: boolean; error?: string }> {
try {
const count = await storage.getSuperAdminCount();
if (count > 0) {
return { success: false, error: 'Суперадмин уже существует' };
}
const passwordHash = await hashPassword(password);
await storage.createSuperAdmin({ email, passwordHash, name });
return { success: true };
} catch (error) {
console.error('SuperAdmin seed error:', error);
return { success: false, error: 'Ошибка при создании суперадмина' };
}
}
async createAdditionalSuperAdmin(email: string, password: string, name: string): Promise<{ success: boolean; superAdmin?: { id: number; email: string; name: string }; error?: string }> {
try {
const existing = await storage.getSuperAdminByEmail(email);
if (existing) {
return { success: false, error: 'Суперадмин с таким email уже существует' };
}
const passwordHash = await hashPassword(password);
const created = await storage.createSuperAdmin({ email, passwordHash, name });
return { success: true, superAdmin: { id: created.id, email: created.email, name: created.name } };
} catch (error) {
console.error('SuperAdmin create error:', error);
return { success: false, error: 'Ошибка при создании суперадмина' };
}
}
async deleteSuperAdmin(idToDelete: number, currentAdminId: number): Promise<{ success: boolean; error?: string }> {
try {
if (idToDelete === currentAdminId) {
return { success: false, error: 'Нельзя удалить собственный аккаунт' };
}
const target = await storage.getSuperAdminById(idToDelete);
if (!target) {
return { success: false, error: 'Суперадмин не найден' };
}
const count = await storage.getSuperAdminCount();
if (count <= 1) {
return { success: false, error: 'Нельзя удалить последнего суперадмина' };
}
await storage.deleteSuperAdmin(idToDelete);
return { success: true };
} catch (error) {
console.error('SuperAdmin delete error:', error);
return { success: false, error: 'Ошибка при удалении суперадмина' };
}
}
async requestPasswordReset(email: string): Promise<{ success: boolean; error?: string }> {
try {
const superAdmin = await storage.getSuperAdminByEmail(email);
if (!superAdmin) {
return { success: true };
}
const token = generateResetToken();
resetTokenStore.set(token, {
superAdminId: superAdmin.id,
expiresAt: Date.now() + RESET_TOKEN_TTL_MS,
});
const sent = await emailService.sendSuperAdminPasswordResetEmail(
superAdmin.email,
superAdmin.name,
token
);
if (!sent) {
resetTokenStore.delete(token);
console.error(`SuperAdmin password reset: email delivery failed for ${superAdmin.email}`);
return { success: false, error: 'Не удалось отправить письмо. Попробуйте позже.' };
}
return { success: true };
} catch (error) {
console.error('SuperAdmin requestPasswordReset error:', error);
return { success: false, error: 'Ошибка при отправке письма' };
}
}
async confirmPasswordReset(token: string, newPassword: string): Promise<{ success: boolean; error?: string }> {
try {
const entry = resetTokenStore.get(token);
if (!entry) {
return { success: false, error: 'Неверная или устаревшая ссылка для сброса пароля' };
}
if (Date.now() > entry.expiresAt) {
resetTokenStore.delete(token);
return { success: false, error: 'Срок действия ссылки истёк. Запросите новую.' };
}
const passwordHash = await hashPassword(newPassword);
await storage.updateSuperAdminPassword(entry.superAdminId, passwordHash);
resetTokenStore.delete(token);
return { success: true };
} catch (error: any) {
console.error('SuperAdmin confirmPasswordReset error:', error);
const msg = error?.message?.includes('Пароль') ? error.message : 'Ошибка при смене пароля';
return { success: false, error: msg };
}
}
}
export const superAdminService = new SuperAdminService();