Files
iistwin/server/routes/push.routes.ts
Ильяс Султанов 0a5f4516ef fix(push.routes): req.user!.userId -> req.user!.id
У объекта пользователя из authenticateToken поле id, а не userId.

Это чинит 500 на /api/devices/active-chat и других device-эндпоинтах.
2026-07-16 20:42:55 +03:00

356 lines
13 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!.id;
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!.id;
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!.id;
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: 'Ошибка обновления статуса' });
}
});
// Update the chat currently open on the user's visible tab.
// Used by the server to suppress push notifications for active chats.
app.post('/api/devices/active-chat', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const userId = req.user!.id;
const organizationId = req.user!.organizationId;
const { type, id, visible } = req.body;
if (!['messenger', 'task'].includes(type) || typeof id !== 'number' || typeof visible !== 'boolean') {
return res.status(400).json({ error: 'Некорректные параметры активного чата' });
}
await storage.updateUserActiveChat(userId, organizationId, type, id, visible);
res.json({ success: true });
} catch (error) {
console.error('Active chat update error:', error);
res.status(500).json({ error: 'Ошибка обновления активного чата' });
}
});
// Clear active chat when the user leaves the chat page.
app.delete('/api/devices/active-chat', authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const userId = req.user!.id;
const organizationId = req.user!.organizationId;
await storage.clearUserActiveChat(userId, organizationId);
res.json({ success: true });
} catch (error) {
console.error('Active chat clear 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!.id;
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 подписки' });
}
});
}