feat(attendance): add notification delivery and NFC security foundation
This commit is contained in:
220
backend/command-api/src/services/notification-device-tokens.js
Normal file
220
backend/command-api/src/services/notification-device-tokens.js
Normal file
@@ -0,0 +1,220 @@
|
||||
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]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user