feat: Group chat and push notifications

- Add group chat tables (chat_groups, chat_group_members, group_messages)
- Add push subscriptions table for web push notifications
- Add group service, controller, routes
- Add push service, controller, routes
- Integrate push notifications with todos, messages, group messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-20 07:27:13 +01:00
parent 73a3c6bf95
commit d9f16ad0a6
15 changed files with 1233 additions and 4 deletions

View File

@@ -0,0 +1,149 @@
import * as groupService from '../services/group.service.js';
import { logger } from '../utils/logger.js';
export const createGroup = async (req, res, next) => {
try {
const { name, memberIds } = req.body;
const userId = req.user.id;
const group = await groupService.createGroup(name, userId, memberIds);
res.status(201).json({
success: true,
data: group,
message: 'Skupina bola vytvorená',
});
} catch (error) {
logger.error('Create group error', error);
next(error);
}
};
export const getUserGroups = async (req, res, next) => {
try {
const groups = await groupService.getUserGroups(req.user.id);
res.json({
success: true,
data: groups,
});
} catch (error) {
logger.error('Get groups error', error);
next(error);
}
};
export const getGroupDetails = async (req, res, next) => {
try {
const { groupId } = req.params;
const group = await groupService.getGroupDetails(groupId, req.user.id);
res.json({
success: true,
data: group,
});
} catch (error) {
logger.error('Get group details error', error);
next(error);
}
};
export const getGroupMessages = async (req, res, next) => {
try {
const { groupId } = req.params;
const messages = await groupService.getGroupMessages(groupId, req.user.id);
res.json({
success: true,
data: messages,
});
} catch (error) {
logger.error('Get group messages error', error);
next(error);
}
};
export const sendGroupMessage = async (req, res, next) => {
try {
const { groupId } = req.params;
const { content } = req.body;
const message = await groupService.sendGroupMessage(groupId, req.user.id, content);
res.status(201).json({
success: true,
data: message,
message: 'Správa odoslaná',
});
} catch (error) {
logger.error('Send group message error', error);
next(error);
}
};
export const addGroupMember = async (req, res, next) => {
try {
const { groupId } = req.params;
const { userId } = req.body;
await groupService.addGroupMember(groupId, userId, req.user.id);
res.json({
success: true,
message: 'Člen bol pridaný',
});
} catch (error) {
logger.error('Add member error', error);
next(error);
}
};
export const removeGroupMember = async (req, res, next) => {
try {
const { groupId, userId } = req.params;
await groupService.removeGroupMember(groupId, userId, req.user.id);
res.json({
success: true,
message: 'Člen bol odstránený',
});
} catch (error) {
logger.error('Remove member error', error);
next(error);
}
};
export const updateGroupName = async (req, res, next) => {
try {
const { groupId } = req.params;
const { name } = req.body;
const group = await groupService.updateGroupName(groupId, name, req.user.id);
res.json({
success: true,
data: group,
message: 'Názov skupiny bol aktualizovaný',
});
} catch (error) {
logger.error('Update group name error', error);
next(error);
}
};
export const deleteGroup = async (req, res, next) => {
try {
const { groupId } = req.params;
await groupService.deleteGroup(groupId, req.user.id);
res.json({
success: true,
message: 'Skupina bola odstránená',
});
} catch (error) {
logger.error('Delete group error', error);
next(error);
}
};

View File

@@ -0,0 +1,117 @@
import * as pushService from '../services/push.service.js';
import { logger } from '../utils/logger.js';
/**
* Get VAPID public key
*/
export const getVapidPublicKey = (req, res) => {
const publicKey = pushService.getVapidPublicKey();
if (!publicKey) {
return res.status(503).json({
success: false,
message: 'Push notifikácie nie sú nakonfigurované',
});
}
res.json({
success: true,
data: { publicKey },
});
};
/**
* Subscribe to push notifications
*/
export const subscribe = async (req, res, next) => {
try {
const { subscription } = req.body;
const userId = req.user.id;
if (!subscription || !subscription.endpoint || !subscription.keys) {
return res.status(400).json({
success: false,
message: 'Neplatná subscription',
});
}
await pushService.saveSubscription(userId, subscription);
res.json({
success: true,
message: 'Push notifikácie aktivované',
});
} catch (error) {
logger.error('Subscribe error', error);
next(error);
}
};
/**
* Unsubscribe from push notifications
*/
export const unsubscribe = async (req, res, next) => {
try {
const { endpoint } = req.body;
const userId = req.user.id;
if (endpoint) {
await pushService.removeSubscription(userId, endpoint);
} else {
await pushService.removeAllSubscriptions(userId);
}
res.json({
success: true,
message: 'Push notifikácie deaktivované',
});
} catch (error) {
logger.error('Unsubscribe error', error);
next(error);
}
};
/**
* Check subscription status
*/
export const getStatus = async (req, res, next) => {
try {
const userId = req.user.id;
const hasSubscription = await pushService.hasActiveSubscription(userId);
res.json({
success: true,
data: {
enabled: hasSubscription,
supported: !!pushService.getVapidPublicKey(),
},
});
} catch (error) {
logger.error('Get status error', error);
next(error);
}
};
/**
* Test push notification (for debugging)
*/
export const testPush = async (req, res, next) => {
try {
const userId = req.user.id;
const result = await pushService.sendPushNotification(userId, {
title: 'Test notifikácie',
body: 'Toto je testovacia push notifikácia z CRM',
icon: '/icon-192.png',
data: { url: '/' },
});
res.json({
success: true,
data: result,
});
} catch (error) {
logger.error('Test push error', error);
next(error);
}
};

View File

@@ -148,7 +148,7 @@ export const updateTodo = async (req, res, next) => {
// Get old todo for audit
const oldTodo = await todoService.getTodoById(todoId);
const todo = await todoService.updateTodo(todoId, data);
const todo = await todoService.updateTodo(todoId, data, userId);
// Log audit event
await logTodoUpdated(