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