У объекта пользователя из authenticateToken поле id, а не userId. Это чинит 500 на /api/devices/active-chat и других device-эндпоинтах.
356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
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 подписки' });
|
||
}
|
||
});
|
||
}
|