Files
Krow-workspace/backend/command-api/src/services/notification-device-tokens.js

221 lines
5.4 KiB
JavaScript

import crypto from 'node:crypto';
export const PUSH_PROVIDERS = {
FCM: 'FCM',
APNS: 'APNS',
WEB_PUSH: 'WEB_PUSH',
};
export const PUSH_PLATFORMS = {
IOS: 'IOS',
ANDROID: 'ANDROID',
WEB: 'WEB',
};
export function hashPushToken(pushToken) {
return crypto.createHash('sha256').update(`${pushToken || ''}`).digest('hex');
}
export async function registerPushToken(client, {
tenantId,
userId,
staffId = null,
businessMembershipId = null,
vendorMembershipId = null,
provider = PUSH_PROVIDERS.FCM,
platform,
pushToken,
deviceId = null,
appVersion = null,
appBuild = null,
locale = null,
timezone = null,
notificationsEnabled = true,
metadata = {},
}) {
const tokenHash = hashPushToken(pushToken);
const result = await client.query(
`
INSERT INTO device_push_tokens (
tenant_id,
user_id,
staff_id,
business_membership_id,
vendor_membership_id,
provider,
platform,
push_token,
token_hash,
device_id,
app_version,
app_build,
locale,
timezone,
notifications_enabled,
invalidated_at,
invalidation_reason,
last_registered_at,
last_seen_at,
metadata
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NULL, NULL, NOW(), NOW(), $16::jsonb
)
ON CONFLICT (provider, token_hash) DO UPDATE
SET tenant_id = EXCLUDED.tenant_id,
user_id = EXCLUDED.user_id,
staff_id = EXCLUDED.staff_id,
business_membership_id = EXCLUDED.business_membership_id,
vendor_membership_id = EXCLUDED.vendor_membership_id,
platform = EXCLUDED.platform,
push_token = EXCLUDED.push_token,
device_id = EXCLUDED.device_id,
app_version = EXCLUDED.app_version,
app_build = EXCLUDED.app_build,
locale = EXCLUDED.locale,
timezone = EXCLUDED.timezone,
notifications_enabled = EXCLUDED.notifications_enabled,
invalidated_at = NULL,
invalidation_reason = NULL,
last_registered_at = NOW(),
last_seen_at = NOW(),
metadata = COALESCE(device_push_tokens.metadata, '{}'::jsonb) || EXCLUDED.metadata,
updated_at = NOW()
RETURNING id,
tenant_id AS "tenantId",
user_id AS "userId",
staff_id AS "staffId",
business_membership_id AS "businessMembershipId",
vendor_membership_id AS "vendorMembershipId",
provider,
platform,
device_id AS "deviceId",
notifications_enabled AS "notificationsEnabled"
`,
[
tenantId,
userId,
staffId,
businessMembershipId,
vendorMembershipId,
provider,
platform,
pushToken,
tokenHash,
deviceId,
appVersion,
appBuild,
locale,
timezone,
notificationsEnabled,
JSON.stringify(metadata || {}),
]
);
return result.rows[0];
}
export async function unregisterPushToken(client, {
tenantId,
userId,
tokenId = null,
pushToken = null,
reason = 'USER_REQUESTED',
}) {
const tokenHash = pushToken ? hashPushToken(pushToken) : null;
const result = await client.query(
`
UPDATE device_push_tokens
SET notifications_enabled = FALSE,
invalidated_at = NOW(),
invalidation_reason = $4,
updated_at = NOW()
WHERE tenant_id = $1
AND user_id = $2
AND (
($3::uuid IS NOT NULL AND id = $3::uuid)
OR
($5::text IS NOT NULL AND token_hash = $5::text)
)
RETURNING id,
provider,
platform,
device_id AS "deviceId"
`,
[tenantId, userId, tokenId, reason, tokenHash]
);
return result.rows;
}
export async function resolveNotificationTargetTokens(client, notification) {
const result = await client.query(
`
WITH recipient_users AS (
SELECT $2::text AS user_id
WHERE $2::text IS NOT NULL
UNION
SELECT bm.user_id
FROM business_memberships bm
WHERE $3::uuid IS NOT NULL
AND bm.id = $3::uuid
UNION
SELECT s.user_id
FROM staffs s
WHERE $4::uuid IS NOT NULL
AND s.id = $4::uuid
)
SELECT
dpt.id,
dpt.user_id AS "userId",
dpt.staff_id AS "staffId",
dpt.provider,
dpt.platform,
dpt.push_token AS "pushToken",
dpt.device_id AS "deviceId",
dpt.metadata
FROM device_push_tokens dpt
JOIN recipient_users ru ON ru.user_id = dpt.user_id
WHERE dpt.tenant_id = $1
AND dpt.notifications_enabled = TRUE
AND dpt.invalidated_at IS NULL
ORDER BY dpt.last_seen_at DESC, dpt.created_at DESC
`,
[
notification.tenant_id,
notification.recipient_user_id,
notification.recipient_business_membership_id,
notification.recipient_staff_id,
]
);
return result.rows;
}
export async function markPushTokenInvalid(client, tokenId, reason) {
await client.query(
`
UPDATE device_push_tokens
SET notifications_enabled = FALSE,
invalidated_at = NOW(),
invalidation_reason = $2,
updated_at = NOW()
WHERE id = $1
`,
[tokenId, reason]
);
}
export async function touchPushTokenDelivery(client, tokenId) {
await client.query(
`
UPDATE device_push_tokens
SET last_delivery_at = NOW(),
last_seen_at = NOW(),
updated_at = NOW()
WHERE id = $1
`,
[tokenId]
);
}