Files
iistwin/server/services/superadmin.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

170 lines
6.1 KiB
TypeScript
Raw 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 { 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();