Files
iistwin/server/routes/auth.invitations.routes.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

158 lines
5.8 KiB
TypeScript
Raw Permalink 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 { Router } from "express";
import crypto from 'crypto';
import { storage } from "../storage";
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { validateRequest } from "../middleware/validation.middleware";
import { createInvitationSchema } from "@shared/schema";
import { emailService } from "../services/email.service";
export function registerInvitationRoutes(router: Router): void {
// Get invitation info by token (public - no auth required)
router.get('/api/invitations/:token', async (req, res) => {
try {
const { token } = req.params;
const invitation = await storage.getInvitationByToken(token);
if (!invitation) {
return res.status(404).json({ success: false, error: 'Приглашение не найдено' });
}
if (invitation.usedAt) {
return res.status(400).json({ success: false, error: 'Приглашение уже использовано' });
}
if (new Date(invitation.expiresAt) < new Date()) {
return res.status(400).json({ success: false, error: 'Срок действия приглашения истёк' });
}
const organization = await storage.getOrganization(invitation.organizationId);
res.json({
success: true,
invitation: {
email: invitation.email,
organizationName: organization?.displayName || organization?.name,
}
});
} catch (error) {
console.error('Get invitation error:', error);
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
}
});
// Create invitation (admin only)
router.post('/api/invitations',
authenticateToken,
tenantIsolation,
requirePermission('users.manage'),
validateRequest(createInvitationSchema),
async (req: AuthenticatedRequest, res) => {
try {
const { email } = req.body;
const existingUser = await storage.getUserByEmail(email, req.organizationId!);
if (existingUser) {
return res.status(400).json({
success: false,
error: 'Пользователь с таким email уже существует в организации'
});
}
const existingInvitations = await storage.getInvitationsByOrganization(req.organizationId!);
const existingInvitation = existingInvitations.find(
inv => inv.email === email && !inv.usedAt && new Date(inv.expiresAt) > new Date()
);
if (existingInvitation) {
return res.status(400).json({ success: false, error: 'Приглашение для этого email уже существует' });
}
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
const invitation = await storage.createInvitation({
organizationId: req.organizationId!,
email,
token,
createdBy: req.user!.id,
expiresAt,
});
const organization = await storage.getOrganization(req.organizationId!);
const inviter = await storage.getUser(req.user!.id);
const baseUrl = process.env.BASE_URL || 'https://iistwin.ru';
const inviteLink = `${baseUrl}/invite/${token}`;
const inviterName = inviter ? `${inviter.firstName} ${inviter.middleName || ''} ${inviter.lastName}`.trim() : 'Администратор';
await emailService.sendInvitationLinkEmail(
email,
inviterName,
organization?.displayName || organization?.name || 'Организация',
inviteLink
);
res.status(201).json({
success: true,
invitation: {
id: invitation.id,
email: invitation.email,
token: invitation.token,
expiresAt: invitation.expiresAt,
inviteLink
}
});
} catch (error) {
console.error('Create invitation error:', error);
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
}
}
);
// Get all invitations for organization (admin only)
router.get('/api/invitations',
authenticateToken,
tenantIsolation,
requirePermission('users.manage'),
async (req: AuthenticatedRequest, res) => {
try {
const invitationsData = await storage.getInvitationsByOrganization(req.organizationId!);
const baseUrl = process.env.BASE_URL || 'https://iistwin.ru';
const invitations = invitationsData.map(inv => ({
id: inv.id,
email: inv.email,
token: inv.token,
createdAt: inv.createdAt,
expiresAt: inv.expiresAt,
usedAt: inv.usedAt,
inviteLink: `${baseUrl}/invite/${inv.token}`,
isExpired: new Date(inv.expiresAt) < new Date(),
isUsed: !!inv.usedAt
}));
res.json({ success: true, invitations });
} catch (error) {
console.error('Get invitations error:', error);
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
}
}
);
// Delete invitation (admin only)
router.delete('/api/invitations/:id',
authenticateToken,
tenantIsolation,
requirePermission('users.manage'),
async (req: AuthenticatedRequest, res) => {
try {
const id = parseInt(req.params.id);
await storage.deleteInvitation(id, req.organizationId!);
res.json({ success: true, message: 'Приглашение удалено' });
} catch (error) {
console.error('Delete invitation error:', error);
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
}
}
);
}