<?php
namespace ApplicationBundle\Controller\Api;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use CompanyGroupBundle\Entity\NotificationLog;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* InternalNotificationApiController
*
* Internal API endpoints called by the Node socket server.
* All routes are protected by X-Api-Key header checked against
* the socket_shared_secret parameter.
*/
class InternalNotificationApiController extends AbstractController
{
private function checkApiKey(Request $request): bool
{
$secret = $this->getParameter('socket_shared_secret');
if (!$secret) return true; // not configured yet — allow during migration
$key = $request->headers->get('X-Api-Key', '');
return hash_equals($secret, $key);
}
/**
* GET /api/internal/tokens?userIds=1,2,3
*
* Returns FCM device tokens for the requested user IDs,
* fetched from entity_token_storage.fire_base_token.
*/
public function getTokensAction(Request $request): JsonResponse
{
if (!$this->checkApiKey($request)) {
return new JsonResponse(['error' => 'Unauthorized'], 401);
}
$rawIds = $request->query->get('userIds', '');
if (!$rawIds) {
return new JsonResponse(['tokens' => []]);
}
$userIds = array_filter(
array_map('intval', explode(',', $rawIds)),
fn($id) => $id > 0
);
if (empty($userIds)) {
return new JsonResponse(['tokens' => []]);
}
$cgEm = $this->getDoctrine()->getManager('company_group');
try {
$rows = $cgEm->getConnection()->fetchAllAssociative(
'SELECT user_id, fire_base_token
FROM entity_token_storage
WHERE user_id IN (' . implode(',', $userIds) . ')
AND fire_base_token IS NOT NULL
AND fire_base_token != \'\'',
[]
);
} catch (\Exception $e) {
return new JsonResponse(['tokens' => [], 'error' => $e->getMessage()]);
}
// Group by userId: userId => [token1, token2, ...]
$result = [];
foreach ($rows as $row) {
$uid = (int) $row['user_id'];
if (!isset($result[$uid])) $result[$uid] = [];
$result[$uid][] = $row['fire_base_token'];
}
return new JsonResponse(['tokens' => $result]);
}
/**
* POST /api/internal/fcm_log
*
* Accepts FCM delivery results from Node and persists them to notification_log.
* Also purges invalidated tokens from entity_token_storage.
*/
public function fcmLogAction(Request $request): JsonResponse
{
if (!$this->checkApiKey($request)) {
return new JsonResponse(['error' => 'Unauthorized'], 401);
}
$data = json_decode($request->getContent(), true) ?: [];
$eventCode = $data['eventCode'] ?? '';
$userIds = $data['userIds'] ?? [];
$responses = $data['responses'] ?? [];
$cgEm = $this->getDoctrine()->getManager('company_group');
// Write per-user log entries
foreach ($userIds as $i => $uid) {
$response = $responses[$i] ?? null;
$success = $response && $response['success'];
$errCode = $response['errorCode'] ?? null;
$log = new NotificationLog();
$log->setEventCode($eventCode);
$log->setChannel('fcm');
$log->setRecipientUserId((int) $uid);
$log->setStatus($success ? NotificationLog::STATUS_SENT : NotificationLog::STATUS_FAILED);
$log->setError($errCode);
$cgEm->persist($log);
// Remove stale token from DB if FCM said it's unregistered
if (
!$success &&
in_array($errCode, [
'messaging/registration-token-not-registered',
'messaging/invalid-registration-token',
])
) {
try {
$cgEm->getConnection()->executeStatement(
'UPDATE entity_token_storage SET fire_base_token = NULL WHERE user_id = :uid',
['uid' => (int) $uid]
);
} catch (\Exception $e) {}
}
}
try {
$cgEm->flush();
} catch (\Exception $e) {}
return new JsonResponse(['ok' => true]);
}
/**
* GET /api/notifications/unread_count
*
* Returns unread notification count for the currently logged-in user.
* Called by frontend JS after a socket push to refresh the badge.
*/
public function unreadCountAction(Request $request): JsonResponse
{
$session = $request->getSession();
$userId = (int) $session->get(UserConstants::USER_ID, 0);
$companyId = (int) $session->get(UserConstants::USER_COMPANY_ID, 0);
if ($userId <= 0) {
return new JsonResponse(['count' => 0]);
}
$cgEm = $this->getDoctrine()->getManager('company_group');
try {
$count = (int) $cgEm->getConnection()->fetchOne(
'SELECT COUNT(*) FROM entity_notification
WHERE user_id = :uid AND company_id = :cid AND seen_flag = 0
AND (expire_ts = 0 OR expire_ts > :now)',
['uid' => $userId, 'cid' => $companyId, 'now' => time()]
);
} catch (\Exception $e) {
$count = 0;
}
return new JsonResponse(['count' => $count]);
}
/**
* GET /api/notifications/recent?limit=15
*
* Returns the most recent notifications for the logged-in user as JSON.
* Used to pre-populate the header dropdown on every page load.
*/
public function recentListAction(Request $request): JsonResponse
{
$session = $request->getSession();
$userId = (int) $session->get(UserConstants::USER_ID, 0);
if ($userId <= 0) {
return new JsonResponse(['items' => [], 'unreadCount' => 0]);
}
$limit = min(20, max(1, (int) $request->query->get('limit', 15)));
$cgEm = $this->getDoctrine()->getManager('company_group');
try {
$rows = $cgEm->getConnection()->fetchAllAssociative(
'SELECT id, title, body, target_route, target_url, notification_ts, seen_flag
FROM entity_notification
WHERE user_id = :uid
ORDER BY notification_ts DESC, id DESC
LIMIT ' . $limit,
['uid' => $userId]
);
$unreadCount = (int) $cgEm->getConnection()->fetchOne(
'SELECT COUNT(*) FROM entity_notification WHERE user_id = :uid AND seen_flag = 0',
['uid' => $userId]
);
} catch (\Exception $e) {
return new JsonResponse(['items' => [], 'unreadCount' => 0]);
}
$items = [];
foreach ($rows as $row) {
$items[] = [
'id' => (int) $row['id'],
'title' => $row['title'] ?: 'Notification',
'body' => $row['body'] ?: '',
'targetRoute' => $row['target_route'] ?: '',
'targetUrl' => $row['target_url'] ?: '',
'notificationTs' => (int) $row['notification_ts'],
'seenFlag' => (int) $row['seen_flag'],
];
}
return new JsonResponse(['items' => $items, 'unreadCount' => $unreadCount]);
}
/**
* POST /api/notifications/mark_read
*
* Marks one or all notifications as read for the current user.
*/
public function markReadAction(Request $request): JsonResponse
{
$session = $request->getSession();
$userId = (int) $session->get(UserConstants::USER_ID, 0);
if ($userId <= 0) {
return new JsonResponse(['ok' => false, 'error' => 'Not logged in']);
}
$id = (int) ($request->request->get('id') ?: $request->query->get('id', 0));
$all = (bool) ($request->request->get('all') ?: $request->query->get('all', 0));
$cgEm = $this->getDoctrine()->getManager('company_group');
try {
if ($all) {
$cgEm->getConnection()->executeStatement(
'UPDATE entity_notification SET seen_flag = 1, read_flag = 1
WHERE user_id = :uid',
['uid' => $userId]
);
} elseif ($id > 0) {
$cgEm->getConnection()->executeStatement(
'UPDATE entity_notification SET seen_flag = 1, read_flag = 1
WHERE id = :id AND user_id = :uid',
['id' => $id, 'uid' => $userId]
);
}
} catch (\Exception $e) {
return new JsonResponse(['ok' => false, 'error' => $e->getMessage()]);
}
return new JsonResponse(['ok' => true]);
}
}