-
Notifications
You must be signed in to change notification settings - Fork 86
fix: added auth to register holder notification api #1542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3bfe9da
538b5e3
f6e00b6
917f42f
d57ad52
e485fba
04e85bd
fb7114e
ea4b4cb
52befa8
321e622
71bccd3
e250531
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,9 @@ NATS_HOST=your-ip | |||||||||
| NATS_PORT=4222 | ||||||||||
| NATS_URL=nats://your-ip:4222 | ||||||||||
|
|
||||||||||
| NATS_PASSWORD=xxxx | ||||||||||
| NATS_USER=xxxx | ||||||||||
|
|
||||||||||
| REDIS_HOST=your-ip | ||||||||||
| REDIS_PORT=6379 | ||||||||||
|
|
||||||||||
|
|
@@ -230,3 +233,16 @@ PRISMA_LOGS = error | |||||||||
|
|
||||||||||
| # DB_ALERT_ENABLE= | ||||||||||
| # DB_ALERT_EMAILS= | ||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| CLIENT_EMAIL= | ||||||||||
| PRIVATE_KEY= | ||||||||||
| PROJECT_ID= | ||||||||||
|
|
||||||||||
| CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000) | ||||||||||
| CONSUMER_CONFIG_MAX_DELIVER=4 | ||||||||||
|
Comment on lines
+243
to
+244
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix
✅ Suggested fix-CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000)
+CONSUMER_CONFIG_ACK_WAIT="10000" # milliseconds; parsed via Number(...)📝 Committable suggestion
Suggested change
🧰 Tools🪛 dotenv-linter (4.0.0)[warning] 243-243: [ValueWithoutQuotes] This value needs to be surrounded in quotes (ValueWithoutQuotes) 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| AGGREGATE_STREAM=aggregate | ||||||||||
| DID_STREAM=did-notify | ||||||||||
| PULL_CONSUMER=hub-pull-consumer | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,6 +59,9 @@ NATS_HOST='0.0.0.0' | |||||||||||||||||||
| NATS_PORT=4222 | ||||||||||||||||||||
| NATS_URL=nats://0.0.0.0:4222 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| NATS_PASSWORD=xxxx | ||||||||||||||||||||
| NATS_USER=xxxx | ||||||||||||||||||||
|
|
||||||||||||||||||||
| REDIS_HOST='0.0.0.0' | ||||||||||||||||||||
| REDIS_PORT=6379 | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -259,3 +262,15 @@ RESEND_API_KEY=re_xxxxxxxxxx | |||||||||||||||||||
| # DB_ALERT_EMAILS= | ||||||||||||||||||||
| # Boolean: to enable/disable db alerts. This needs the 'utility' microservice | ||||||||||||||||||||
| # DB_ALERT_ENABLE= | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| CLIENT_EMAIL= | ||||||||||||||||||||
| PRIVATE_KEY= | ||||||||||||||||||||
| PROJECT_ID= | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+265
to
+270
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clean up the new env block ordering/spacing. dotenv-linter flags extra blank lines and key ordering (CLIENT_EMAIL/PRIVATE_KEY before PROJECT_ID). 🧹 Suggested cleanup-
-
-PROJECT_ID=
-CLIENT_EMAIL=
-PRIVATE_KEY=
+CLIENT_EMAIL=
+PRIVATE_KEY=
+PROJECT_ID=📝 Committable suggestion
Suggested change
🧰 Tools🪛 dotenv-linter (4.0.0)[warning] 259-259: [ExtraBlankLine] Extra blank line detected (ExtraBlankLine) [warning] 261-261: [UnorderedKey] The CLIENT_EMAIL key should go before the PROJECT_ID key (UnorderedKey) [warning] 262-262: [UnorderedKey] The PRIVATE_KEY key should go before the PROJECT_ID key (UnorderedKey) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000) | ||||||||||||||||||||
| CONSUMER_CONFIG_MAX_DELIVER=4 | ||||||||||||||||||||
|
Comment on lines
+271
to
+272
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix
✅ Suggested fix-CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000)
+CONSUMER_CONFIG_ACK_WAIT="10000" # milliseconds; parsed via Number(...)📝 Committable suggestion
Suggested change
🧰 Tools🪛 dotenv-linter (4.0.0)[warning] 264-264: [ValueWithoutQuotes] This value needs to be surrounded in quotes (ValueWithoutQuotes) 🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| AGGREGATE_STREAM=aggregate | ||||||||||||||||||||
| DID_STREAM=did-notify | ||||||||||||||||||||
| PULL_CONSUMER=hub-pull-consumer | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; | ||
| import { Observable } from 'rxjs'; | ||
|
|
||
| @Injectable() | ||
| export class ClientAccessGuard implements CanActivate { | ||
| canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { | ||
| const request = context.switchToHttp().getRequest(); | ||
|
|
||
| const { user } = request; | ||
|
|
||
| if (!user || !Object.prototype.hasOwnProperty.call(user, 'client_id')) { | ||
| throw new UnauthorizedException('You do not have access'); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "@credebl/notification", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "dependencies": { | ||
| "firebase-admin": "^13.6.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export interface INatsEventMessage { | ||
| event: string; | ||
| sessionId: string; | ||
| timestamp: string; | ||
| senderCode: string; | ||
| } | ||
|
|
||
| export interface INatsUserRequestData { | ||
| did: string; | ||
| sessions: { | ||
| sessionId: string; | ||
| orgCode: string; | ||
| }[]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; | ||
| import { Consumer } from 'nats'; | ||
| import { NatsService } from './nats.service'; | ||
| import { | ||
| DID_STREAM, | ||
| ensureConsumer, | ||
| ensureDidStream, | ||
| publishToJetStream, | ||
| PULL_CONSUMER, | ||
| AGGREGATE_STREAM | ||
| } from './jetstream.setup'; | ||
| import { PendingAckStore } from './pendingAckStore'; | ||
| import { HolderNotificationRepository } from '../holder-notification.repository'; | ||
| import { Message } from 'firebase-admin/lib/messaging/messaging-api'; | ||
| import * as admin from 'firebase-admin'; | ||
| import { IHolderNotification } from '@credebl/common/interfaces/holder-notification.interfaces'; | ||
|
|
||
| const EVENT_PRESENTATION_ACK = 'presentation.ack'; | ||
| const EVENT_PRESENTATION_PURGED = 'presentation.purged'; | ||
| @Injectable() | ||
| export class JetStreamConsumer implements OnApplicationBootstrap { | ||
| constructor( | ||
| private readonly nats: NatsService, | ||
| private readonly logger: Logger, | ||
| private readonly pendingAckStore: PendingAckStore, | ||
| private readonly holderNotificationRepository: HolderNotificationRepository | ||
| ) {} | ||
|
|
||
| async onApplicationBootstrap(): Promise<void> { | ||
| if (!admin.apps.length) { | ||
| const projectId = process.env.PROJECT_ID; | ||
| const clientEmail = process.env.CLIENT_EMAIL; | ||
| const privateKey = process.env.PRIVATE_KEY; | ||
|
|
||
| if (!projectId || !clientEmail || !privateKey) { | ||
| throw new Error('Missing Firebase credentials: PROJECT_ID, CLIENT_EMAIL, and PRIVATE_KEY are required'); | ||
| } | ||
| admin.initializeApp({ | ||
| credential: admin.credential.cert({ | ||
| projectId, | ||
| clientEmail, | ||
| privateKey: privateKey.replace(/\\n/g, '\n') | ||
| }) | ||
| }); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const js = this.nats.jetstream(); // ✅ now safe | ||
|
|
||
| const jsm = this.nats.jetstreamManager(); | ||
| await ensureConsumer(jsm); | ||
| const consumer: Consumer = await js.consumers.get(AGGREGATE_STREAM, PULL_CONSUMER); | ||
|
|
||
| this.logger.log('[NATS] JetStream consumer started'); | ||
|
|
||
| this.consume(consumer).catch((err) => { | ||
| this.logger.error('[NATS] Consumer crashed', err); | ||
| }); | ||
| } | ||
|
|
||
| private async consume(consumer: Consumer): Promise<void> { | ||
| this.logger.log(`[NATS] Starting to consume messages from consumer ${consumer.info}`); | ||
| for await (const msg of await consumer.consume()) { | ||
| try { | ||
| const { subject } = msg; | ||
| this.logger.log(`[NATS] Message subject: ${subject}`); | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const [_, domain, event, orgCode, sessionId] = subject.split('.'); | ||
| const consumerName = `notify-session-${sessionId}`; | ||
|
|
||
| this.logger.log({ | ||
| domain, | ||
| event, | ||
| orgCode, | ||
| sessionId | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const payload = msg.json(); | ||
| const notificationDetail = await this.holderNotificationRepository.getHolderNotificationBySessionId(sessionId); | ||
| this.logger.debug(`[NATS] Message received ${JSON.stringify(payload)}`); | ||
| this.logger.log(`[NATS] Processing message, ${JSON.stringify({ deliveryCount: msg.info.deliveryCount })}`); | ||
| const maxDeliver = process.env.CONSUMER_CONFIG_MAX_DELIVER | ||
| ? Number(process.env.CONSUMER_CONFIG_MAX_DELIVER) | ||
| : 4; | ||
| if (maxDeliver - 1 < msg.info.deliveryCount) { | ||
| //------------ Moving message to DID stream ---------------// | ||
| await this.moveMsgToDidStream(notificationDetail, msg); | ||
|
|
||
| //------------- Sending Push Notification via FCM ----------------// | ||
| this.sendNotificationToHolder(event, orgCode, notificationDetail); | ||
| msg.ack(); | ||
|
Comment on lines
+84
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check for When 🔧 Proposed fix if (maxDeliver - 1 < msg.info.deliveryCount) {
+ if (!notificationDetail) {
+ this.logger.error(`[NATS] No notification detail found for session ID: ${sessionId}, message exhausted`);
+ msg.ack(); // Ack to prevent infinite retries since max deliver reached
+ continue;
+ }
//------------ Moving message to DID stream ---------------//
await this.moveMsgToDidStream(notificationDetail, msg);🤖 Prompt for AI Agents |
||
| } else { | ||
| // ------------- Publishing Messages via NATS ----------------// | ||
|
|
||
| const ackKey = this.pendingAckStore.save(AGGREGATE_STREAM, consumerName, msg); | ||
|
|
||
| this.logger.log(`[NATS] Notification detail fetched for session ID: ${sessionId}`); | ||
| if (!notificationDetail) { | ||
| this.logger.error(`[NATS] No notification detail found for session ID: ${sessionId}`); | ||
| msg.nak(); | ||
| continue; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| await this.nats.publish(`${notificationDetail.holderDid}`, { | ||
| payload, | ||
| ackKey, | ||
| subject: msg.subject, | ||
| event: `${domain}.${event}` | ||
| }); | ||
|
|
||
| this.logger.log(`[NATS] Message published to ${notificationDetail.holderDid} for session ${sessionId}`); | ||
| } | ||
|
|
||
| // business logic | ||
| // msg.ack(); | ||
| } catch (err) { | ||
| this.logger.error('[NATS] Processing failed', err); | ||
| msg.nak(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private sendNotificationToHolder(event: string, orgCode: string, notificationDetail: IHolderNotification): void { | ||
| let notificationTitle = ''; | ||
| if (EVENT_PRESENTATION_ACK === event) { | ||
| notificationTitle = `Your data delivered to ${orgCode}`; | ||
| } else if (EVENT_PRESENTATION_PURGED === event) { | ||
| notificationTitle = `Your data purged from ${orgCode}`; | ||
| } | ||
| this.logger.log('Now push notifications will be sent to user'); | ||
| const notificationPayload: Message = { | ||
| notification: { | ||
| title: notificationTitle, | ||
| body: `Open an app to view more details` | ||
| }, | ||
| token: notificationDetail.fcmToken | ||
| }; | ||
| // Send the notification to the specified device | ||
| admin | ||
| .messaging() | ||
| .send(notificationPayload) | ||
| .then((response) => { | ||
| this.logger.log('Successfully sent message:', response); | ||
| }) | ||
| .catch((error) => { | ||
| this.logger.error('Error sending message:', error); | ||
| }); | ||
| } | ||
|
|
||
| private async moveMsgToDidStream(notificationDetail: IHolderNotification, msg): Promise<void> { | ||
| this.logger.log('[NATS] Moving message to DID stream for FCM notification'); | ||
| //const notifyStream = process.env.STREAM_NOTIFY ?? "notify"; | ||
| const subjectName = `${DID_STREAM}.${notificationDetail.holderDid}`; | ||
| const jsm = this.nats.jetstreamManager(); | ||
| const jsc = this.nats.jetstream(); | ||
| await ensureDidStream(jsm); | ||
|
|
||
| const decoder = new TextDecoder(); // or 'UTF-8', 'windows-1251', etc. | ||
| const msgData = decoder.decode(msg.data); | ||
|
|
||
| //await ensureStreamExists(streamName); | ||
| // Publish to JetStream for guaranteed delivery | ||
| const jsAck = await publishToJetStream(subjectName, msgData, jsc); | ||
| this.logger.log(`[NATS] Message moved to DID stream: ${JSON.stringify(jsAck)}`); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.