Files
iistwin/server/routes/push.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

322 lines
12 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 { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
export function registerPushRoutes(app: import("express").Express): void {
// =====================
// Device Tokens for Push Notifications
// =====================
/**
* @swagger
* /api/devices/register:
* post:
* tags: [Devices]
* summary: Зарегистрировать устройство для push-уведомлений
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [token, platform]
* properties:
* token:
* type: string
* description: Pushy device token
* platform:
* type: string
* enum: [android, ios]
* responses:
* 200:
* description: Устройство зарегистрировано
* /api/devices/unregister:
* post:
* tags: [Devices]
* summary: Удалить устройство из push-уведомлений
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [token]
* properties:
* token:
* type: string
* responses:
* 200:
* description: Удалено
*/
// Register a device token
app.post('/api/devices/register', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const { token, platform, deviceInfo } = req.body;
if (!token || !platform) {
return res.status(400).json({ error: 'Токен и платформа обязательны' });
}
if (!['android', 'ios', 'web'].includes(platform)) {
return res.status(400).json({ error: 'Неверная платформа. Допустимые значения: android, ios, web' });
}
const userId = req.user!.userId;
const organizationId = req.user!.organizationId;
await storage.registerDeviceToken(userId, organizationId, token, platform, deviceInfo);
await storage.updateUserPresence(userId, organizationId, true);
res.json({ success: true });
} catch (error) {
console.error('Register device token error:', error);
res.status(500).json({ error: 'Ошибка регистрации устройства' });
}
});
// Unregister a device token
app.post('/api/devices/unregister', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({ error: 'Токен обязателен' });
}
const userId = req.user!.userId;
const organizationId = req.user!.organizationId;
await storage.unregisterDeviceToken(userId, organizationId, token);
res.json({ success: true });
} catch (error) {
console.error('Unregister device token error:', error);
res.status(500).json({ error: 'Ошибка удаления токена устройства' });
}
});
// Heartbeat endpoint to update presence
app.post('/api/devices/heartbeat', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const userId = req.user!.userId;
const organizationId = req.user!.organizationId;
await storage.heartbeatUserPresence(userId, organizationId);
res.json({ success: true });
} catch (error) {
console.error('Heartbeat error:', error);
res.status(500).json({ error: 'Ошибка обновления статуса' });
}
});
// Get user's registered devices
app.get('/api/devices', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const userId = req.user!.userId;
const organizationId = req.user!.organizationId;
const tokens = await storage.getDeviceTokensByUser(userId, organizationId);
res.json({ success: true, devices: tokens.map(t => ({
id: t.id,
platform: t.platform,
lastActiveAt: t.lastActiveAt,
deviceInfo: t.deviceInfo,
}))});
} catch (error) {
console.error('Get devices error:', error);
res.status(500).json({ error: 'Ошибка получения списка устройств' });
}
});
/**
* @swagger
* /api/web-push/vapid-key:
* get:
* tags: [Devices]
* summary: Получить VAPID публичный ключ для Web Push
* description: Возвращает VAPID публичный ключ, необходимый для создания подписки Web Push на стороне клиента (PushManager.subscribe).
* responses:
* 200:
* description: VAPID публичный ключ
* content:
* application/json:
* schema:
* type: object
* properties:
* vapidPublicKey:
* type: string
* description: Base64url-encoded VAPID публичный ключ
* /api/web-push/subscribe:
* post:
* tags: [Devices]
* summary: Подписать браузер на Web Push уведомления
* description: Регистрирует Web Push подписку текущего пользователя. Subscription-объект получается из `PushManager.subscribe()` браузерного API. Требует авторизации.
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [subscription]
* properties:
* subscription:
* type: object
* description: Объект подписки из PushManager.subscribe()
* required: [endpoint]
* properties:
* endpoint:
* type: string
* description: URL эндпоинта push-сервиса браузера
* keys:
* type: object
* properties:
* p256dh:
* type: string
* description: Публичный ключ шифрования
* auth:
* type: string
* description: Секрет аутентификации
* responses:
* 200:
* description: Подписка зарегистрирована
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* 400:
* description: Subscription обязательна
* 401:
* description: Не авторизован
* /api/web-push/unsubscribe:
* post:
* tags: [Devices]
* summary: Отписать браузер от Web Push уведомлений
* description: Удаляет Web Push подписку по endpoint. Требует авторизации.
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [endpoint]
* properties:
* endpoint:
* type: string
* description: URL эндпоинта push-сервиса браузера (из subscription.endpoint)
* responses:
* 200:
* description: Подписка удалена
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* 400:
* description: Endpoint обязателен
* 401:
* description: Не авторизован
*/
// Web Push - Get VAPID public key
app.get('/api/web-push/vapid-key', async (_req, res) => {
try {
const { webPushService } = await import('../services/web-push.service');
res.json({ vapidPublicKey: webPushService.getVapidPublicKey() });
} catch (error) {
console.error('Get VAPID key error:', error);
res.status(500).json({ error: 'Ошибка получения VAPID ключа' });
}
});
// Web Push - Register subscription
app.post('/api/devices/register-web-push', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const { subscription } = req.body;
if (!subscription || !subscription.endpoint) {
return res.status(400).json({ error: 'Subscription обязательна' });
}
const userId = req.user!.id;
const organizationId = req.user!.organizationId;
const { webPushService } = await import('../services/web-push.service');
await webPushService.registerSubscription(userId, organizationId, subscription);
res.json({ success: true });
} catch (error) {
console.error('Register web push error:', error);
res.status(500).json({ error: 'Ошибка регистрации web push подписки' });
}
});
// Web Push - Unregister subscription
app.post('/api/devices/unregister-web-push', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const { endpoint } = req.body;
if (!endpoint) {
return res.status(400).json({ error: 'Endpoint обязателен' });
}
const { webPushService } = await import('../services/web-push.service');
await webPushService.unregisterSubscription(endpoint);
res.json({ success: true });
} catch (error) {
console.error('Unregister web push error:', error);
res.status(500).json({ error: 'Ошибка удаления web push подписки' });
}
});
// Web Push - Subscribe (canonical path)
app.post('/api/web-push/subscribe', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const { subscription } = req.body;
if (!subscription || !subscription.endpoint) {
return res.status(400).json({ error: 'Subscription обязательна' });
}
const userId = req.user!.id;
const organizationId = req.user!.organizationId;
const { webPushService } = await import('../services/web-push.service');
await webPushService.registerSubscription(userId, organizationId, subscription);
res.json({ success: true });
} catch (error) {
console.error('Web push subscribe error:', error);
res.status(500).json({ error: 'Ошибка регистрации web push подписки' });
}
});
// Web Push - Unsubscribe (canonical path)
app.post('/api/web-push/unsubscribe', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const { endpoint } = req.body;
if (!endpoint) {
return res.status(400).json({ error: 'Endpoint обязателен' });
}
const { webPushService } = await import('../services/web-push.service');
await webPushService.unregisterSubscription(endpoint);
res.json({ success: true });
} catch (error) {
console.error('Web push unsubscribe error:', error);
res.status(500).json({ error: 'Ошибка удаления web push подписки' });
}
});
}