fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
321
server/routes/push.routes.ts
Normal file
321
server/routes/push.routes.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
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 подписки' });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user