@things-factory/notification 8.0.0-beta.9 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/client/actions/notification-fcm.ts +148 -0
  2. package/client/bootstrap.ts +135 -0
  3. package/client/index.ts +6 -0
  4. package/client/pages/notification/notification-list-page.ts +258 -0
  5. package/client/pages/notification-rule/notification-rule-importer.ts +87 -0
  6. package/client/pages/notification-rule/notification-rule-list-page.ts +386 -0
  7. package/client/reducers/notification.ts +27 -0
  8. package/client/route.ts +10 -0
  9. package/client/tsconfig.json +13 -0
  10. package/client/viewparts/notification-badge.ts +54 -0
  11. package/client/viewparts/notification-item.ts +246 -0
  12. package/client/viewparts/notification-list.ts +160 -0
  13. package/client/viewparts/notification-sender.ts +142 -0
  14. package/client/viewparts/notification-setting-let.ts +222 -0
  15. package/dist-client/tsconfig.tsbuildinfo +1 -1
  16. package/dist-server/tsconfig.tsbuildinfo +1 -1
  17. package/docs/images/config-app-1.png +0 -0
  18. package/docs/images/config-app-2.png +0 -0
  19. package/docs/images/config-server-key.png +0 -0
  20. package/docs/images/config-service-account.png +0 -0
  21. package/docs/images/config-vapidkey-1.png +0 -0
  22. package/docs/images/config-vapidkey-2-get-public-key.png +0 -0
  23. package/docs/images/config-vapidkey-3-get-private-key.png +0 -0
  24. package/docs/images/element-notification-badge.png +0 -0
  25. package/docs/images/element-notification-list.png +0 -0
  26. package/docs/images/element-notification-setting-let.png +0 -0
  27. package/docs/images/push-test-on-chrome-1.png +0 -0
  28. package/docs/images/push-test-on-chrome-2.png +0 -0
  29. package/docs/images/push-test-on-firebase-1.png +0 -0
  30. package/docs/images/push-test-on-firebase-2.png +0 -0
  31. package/docs/images/push-test-on-firebase-3.png +0 -0
  32. package/docs/images/push-test-on-firebase-4.png +0 -0
  33. package/package.json +9 -9
  34. package/server/controllers/fcm.ts +214 -0
  35. package/server/controllers/index.ts +1 -0
  36. package/server/index.ts +5 -0
  37. package/server/middlewares/index.ts +5 -0
  38. package/server/middlewares/notification-middleware.ts +73 -0
  39. package/server/routers/notification-router.ts +67 -0
  40. package/server/routes.ts +11 -0
  41. package/server/service/index.ts +42 -0
  42. package/server/service/notification/directive-notification.ts +71 -0
  43. package/server/service/notification/index.ts +14 -0
  44. package/server/service/notification/notification-mutation.ts +119 -0
  45. package/server/service/notification/notification-query.ts +76 -0
  46. package/server/service/notification/notification-subscription.ts +44 -0
  47. package/server/service/notification/notification-type.ts +55 -0
  48. package/server/service/notification/notification.ts +105 -0
  49. package/server/service/notification-rule/event-subscriber.ts +20 -0
  50. package/server/service/notification-rule/index.ts +9 -0
  51. package/server/service/notification-rule/notification-rule-history.ts +136 -0
  52. package/server/service/notification-rule/notification-rule-mutation.ts +203 -0
  53. package/server/service/notification-rule/notification-rule-query.ts +65 -0
  54. package/server/service/notification-rule/notification-rule-type.ts +71 -0
  55. package/server/service/notification-rule/notification-rule.ts +125 -0
  56. package/server/tsconfig.json +9 -0
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/notification",
3
- "version": "8.0.0-beta.9",
3
+ "version": "8.0.2",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -28,17 +28,17 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@material/web": "^2.0.0",
31
- "@operato/layout": "^8.0.0-beta",
32
- "@things-factory/auth-base": "^8.0.0-beta.9",
33
- "@things-factory/codelingua": "^8.0.0-beta.9",
34
- "@things-factory/i18n-base": "^8.0.0-beta.9",
35
- "@things-factory/organization": "^8.0.0-beta.9",
36
- "@things-factory/setting-base": "^8.0.0-beta.9",
37
- "@things-factory/shell": "^8.0.0-beta.9",
31
+ "@operato/layout": "^8.0.0",
32
+ "@things-factory/auth-base": "^8.0.2",
33
+ "@things-factory/codelingua": "^8.0.2",
34
+ "@things-factory/i18n-base": "^8.0.2",
35
+ "@things-factory/organization": "^8.0.2",
36
+ "@things-factory/setting-base": "^8.0.2",
37
+ "@things-factory/shell": "^8.0.2",
38
38
  "clipboard": "^2.0.6",
39
39
  "firebase": "^9.14.0",
40
40
  "firebase-admin": "^11.3.0",
41
41
  "google": "^2.1.0"
42
42
  },
43
- "gitHead": "86b1dfa26292926a2d5447fdc23bfa5a9e983245"
43
+ "gitHead": "39d60f56e142561233ddf6d47b539c637971357c"
44
44
  }
@@ -0,0 +1,214 @@
1
+ import fetch from 'node-fetch'
2
+
3
+ import { config, logger } from '@things-factory/env'
4
+
5
+ import { initializeApp, cert } from 'firebase-admin/app'
6
+ import { getMessaging } from 'firebase-admin/messaging'
7
+
8
+ const notificationConfig = config.get('notification') || {}
9
+ const { fcm, vapidKey } = notificationConfig
10
+ const { serviceAccount, appConfig, serverKey } = fcm || {}
11
+ const { publicKey } = vapidKey || {}
12
+
13
+ var messaging
14
+
15
+ if (serviceAccount) {
16
+ try {
17
+ const app = initializeApp({
18
+ credential: cert(serviceAccount)
19
+ })
20
+
21
+ messaging = getMessaging(app)
22
+ } catch (err) {
23
+ logger.error('incorrect notification configuration')
24
+ logger.error(err)
25
+ }
26
+ }
27
+
28
+ export function getConfig() {
29
+ return messaging
30
+ ? {
31
+ appConfig,
32
+ vapidPublicKey: publicKey
33
+ }
34
+ : {}
35
+ }
36
+
37
+ /**
38
+ * sendNotification
39
+ *
40
+ * @param receiver user.id who will receive notification
41
+ * @param message message object to be sent
42
+ */
43
+ export async function sendNotification({ receivers, message }) {
44
+ if (!messaging) {
45
+ return
46
+ }
47
+
48
+ notify({
49
+ receivers,
50
+ ...message
51
+ })
52
+ }
53
+
54
+ export async function register(user, { subscription }) {
55
+ if (!messaging || !subscription || !user) {
56
+ return false
57
+ }
58
+
59
+ try {
60
+ var notification_key = await getDeviceGroup(user.id)
61
+
62
+ if (!notification_key) {
63
+ await postDeviceGroup({
64
+ operation: 'create',
65
+ notification_key_name: user.id,
66
+ registration_ids: [subscription]
67
+ })
68
+ } else {
69
+ await postDeviceGroup({
70
+ operation: 'add',
71
+ notification_key_name: user.id,
72
+ notification_key,
73
+ registration_ids: [subscription]
74
+ })
75
+ }
76
+
77
+ return true
78
+ } catch (err) {
79
+ return
80
+ }
81
+ }
82
+
83
+ export async function unregister(user, { subscription }) {
84
+ if (!messaging || !subscription || !user) {
85
+ return false
86
+ }
87
+
88
+ try {
89
+ var notification_key = await getDeviceGroup(user.id)
90
+
91
+ if (notification_key) {
92
+ await postDeviceGroup({
93
+ operation: 'remove',
94
+ notification_key_name: user.id,
95
+ notification_key,
96
+ registration_ids: [subscription]
97
+ })
98
+ }
99
+
100
+ return true
101
+ } catch (err) {
102
+ return
103
+ }
104
+ }
105
+
106
+ export async function notify({ receivers, privileges, tokens, topic, title, body, data, image, actions }) {
107
+ if (!messaging) {
108
+ return
109
+ }
110
+
111
+ // Caution: non-string attributes are not allowed in FCM payload validation
112
+ var notification = {
113
+ title: title || '',
114
+ body: body || '',
115
+ image: image || ''
116
+ }
117
+
118
+ if (tokens && tokens instanceof Array && tokens.length > 0) {
119
+ if (tokens.length > 1) {
120
+ const response = await messaging.sendMulticast({
121
+ notification,
122
+ data,
123
+ tokens
124
+ })
125
+
126
+ if (response.failureCount > 0) {
127
+ const failedTokens = []
128
+ response.responses.forEach((resp, idx) => {
129
+ if (!resp.success) {
130
+ failedTokens.push(tokens[idx])
131
+ }
132
+ })
133
+ logger.error('List of tokens that caused failures: ' + failedTokens)
134
+ }
135
+ } else {
136
+ const token = tokens[0]
137
+
138
+ try {
139
+ const response = await messaging.send({
140
+ notification,
141
+ data,
142
+ token
143
+ })
144
+ logger.log('Successfully sent message:', response)
145
+ } catch (err) {
146
+ logger.error('Error sending message:', err)
147
+ }
148
+ }
149
+ }
150
+
151
+ if (topic) {
152
+ try {
153
+ const response = await messaging.send({
154
+ notification,
155
+ data,
156
+ topic
157
+ })
158
+ logger.log('Successfully sent message:', response)
159
+ } catch (err) {
160
+ logger.error('Error sending message:', err)
161
+ }
162
+ }
163
+
164
+ if (receivers && receivers instanceof Array) {
165
+ for (let receiver of receivers) {
166
+ const notification_key = await getDeviceGroup(receiver)
167
+
168
+ if (notification_key) {
169
+ await messaging.sendToDeviceGroup(notification_key, {
170
+ notification,
171
+ data
172
+ })
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ async function postDeviceGroup(body) {
179
+ var response = await fetch('https://fcm.googleapis.com/fcm/notification', {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Content-Type': 'application/json',
183
+ Authorization: `key=${serverKey}`,
184
+ project_id: appConfig.messagingSenderId
185
+ },
186
+ body: JSON.stringify(body)
187
+ })
188
+
189
+ if (response.ok) {
190
+ const { notification_key } = await response.json()
191
+ return notification_key
192
+ } else {
193
+ console.error('postDeviceGroup-notok', response.status, await response.text())
194
+ }
195
+ }
196
+
197
+ async function getDeviceGroup(notificationKeyName) {
198
+ const response = await fetch(
199
+ `https://fcm.googleapis.com/fcm/notification?notification_key_name=${notificationKeyName}`,
200
+ {
201
+ method: 'GET',
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ Authorization: `key=${serverKey}`,
205
+ project_id: appConfig.messagingSenderId
206
+ }
207
+ }
208
+ )
209
+
210
+ if (response.ok) {
211
+ const { notification_key } = await response.json()
212
+ return notification_key
213
+ }
214
+ }
@@ -0,0 +1 @@
1
+ export * from './fcm'
@@ -0,0 +1,5 @@
1
+ export * from './service'
2
+ export * from './middlewares'
3
+ export * from './controllers'
4
+
5
+ import './routes'
@@ -0,0 +1,5 @@
1
+ import { notificationMiddleware } from './notification-middleware'
2
+
3
+ export function initMiddlewares(app) {
4
+ app.use(notificationMiddleware)
5
+ }
@@ -0,0 +1,73 @@
1
+ import { pubsub } from '@things-factory/shell'
2
+ import { logger } from '@things-factory/env'
3
+
4
+ import { notify } from '../controllers/fcm'
5
+
6
+ const debug = require('debug')('things-factory:notification:notification-middleware')
7
+
8
+ function notifier(context) {
9
+ return async function ({
10
+ receivers,
11
+ privileges,
12
+ tokens,
13
+ topic,
14
+ type,
15
+ subject = 'info',
16
+ title,
17
+ body,
18
+ image,
19
+ url,
20
+ actions,
21
+ timestamp = Date.now(),
22
+ mode = 'background'
23
+ }) {
24
+ const { domain, user } = context.state
25
+
26
+ try {
27
+ if (mode === 'background') {
28
+ /* for send webpush notification message */
29
+
30
+ await notify({
31
+ receivers,
32
+ privileges,
33
+ tokens,
34
+ topic,
35
+ title,
36
+ body,
37
+ data: {
38
+ type,
39
+ url,
40
+ timestamp: String(timestamp)
41
+ },
42
+ image,
43
+ actions
44
+ })
45
+ } else {
46
+ /* for send publish notification to clients */
47
+ // TODO CONFIRM data format
48
+ await pubsub.publish('notification', {
49
+ notification: {
50
+ domain,
51
+ subject,
52
+ type,
53
+ title,
54
+ body,
55
+ image,
56
+ url,
57
+ timestamp
58
+ }
59
+ })
60
+ }
61
+ } catch (err) {
62
+ logger.error(err)
63
+ }
64
+ }
65
+ }
66
+
67
+ export async function notificationMiddleware(context: any, next: any) {
68
+ if (!context.state.notify) {
69
+ context.state.notify = notifier(context)
70
+ }
71
+
72
+ await next()
73
+ }
@@ -0,0 +1,67 @@
1
+ import Router from 'koa-router'
2
+ import { ILike } from 'typeorm'
3
+
4
+ import { User } from '@things-factory/auth-base'
5
+ import { getRepository } from '@things-factory/shell'
6
+
7
+ import { getConfig, register, unregister } from '../controllers/fcm'
8
+
9
+ const debug = require('debug')('things-factory:notification:notification-router')
10
+
11
+ export const notificationRouter = new Router({
12
+ prefix: '/notification'
13
+ })
14
+
15
+ notificationRouter.get('/config', async (context, next) => {
16
+ const config = getConfig()
17
+
18
+ context.type = 'application/json'
19
+ context.body = config
20
+ })
21
+
22
+ notificationRouter.post('/register', async (context, next) => {
23
+ const { user } = context.state
24
+ const { body } = context.request
25
+ context.body = await register(user, body)
26
+ context.status = 200
27
+ })
28
+
29
+ notificationRouter.post('/unregister', async (context, next) => {
30
+ const { user } = context.state
31
+ const { body } = context.request
32
+ context.body = await unregister(user, body)
33
+ context.status = 200
34
+ })
35
+
36
+ notificationRouter.post('/notify', async (context, next) => {
37
+ const { user, domain, notify } = context.state
38
+ const repository = getRepository(User)
39
+
40
+ var { receivers, ...options } = context.request.body
41
+
42
+ debug('post:/notify', receivers, receivers.split(','), options)
43
+
44
+ // TODO filter only users having current domain privilege
45
+ receivers = (
46
+ await Promise.all(
47
+ receivers
48
+ .split(',')
49
+ .map(email => email.trim())
50
+ .map(async email => {
51
+ var receiver: User = await repository.findOneBy({ email: ILike(email) })
52
+ return receiver && receiver.id
53
+ })
54
+ )
55
+ ).filter(receiver => !!receiver)
56
+
57
+ debug('post:/notify', receivers, user.id)
58
+
59
+ await notify({
60
+ receivers: receivers.length > 0 ? receivers : [user.id],
61
+ ...options
62
+ })
63
+
64
+ context.body = {
65
+ success: true
66
+ }
67
+ })
@@ -0,0 +1,11 @@
1
+ import { notificationRouter } from './routers/notification-router'
2
+
3
+ process.on('bootstrap-module-global-public-route' as any, (app, globalPublicRouter) => {})
4
+
5
+ process.on('bootstrap-module-global-private-route' as any, (app, globalPrivateRouter) => {})
6
+
7
+ process.on('bootstrap-module-domain-public-route' as any, (app, domainPublicRouter) => {})
8
+
9
+ process.on('bootstrap-module-domain-private-route' as any, (app, domainPrivateRouter) => {
10
+ domainPrivateRouter.use('', notificationRouter.routes(), notificationRouter.allowedMethods())
11
+ })
@@ -0,0 +1,42 @@
1
+ /* EXPORT ENTITY TYPES */
2
+ export * from './notification-rule/notification-rule'
3
+ export * from './notification/notification'
4
+
5
+ /* IMPORT ENTITIES AND RESOLVERS */
6
+ import {
7
+ entities as NotificationRuleEntities,
8
+ resolvers as NotificationRuleResolvers,
9
+ subscribers as NotificationRuleSubscribers
10
+ } from './notification-rule'
11
+ import {
12
+ entities as NotificationEntities,
13
+ typeDefs as NotificationTypeDefs,
14
+ resolvers as NotificationResolvers,
15
+ directives as NotificationDirectives
16
+ } from './notification'
17
+
18
+ export const entities = [
19
+ /* ENTITIES */
20
+ ...NotificationRuleEntities,
21
+ ...NotificationEntities
22
+ ]
23
+
24
+ export const subscribers = [
25
+ /* SUBSCRIBERS */
26
+ ...NotificationRuleSubscribers
27
+ ]
28
+
29
+ export const schema = {
30
+ typeDefs: {
31
+ ...NotificationTypeDefs
32
+ },
33
+ resolverClasses: [
34
+ /* RESOLVER CLASSES */
35
+ ...NotificationRuleResolvers,
36
+ ...NotificationResolvers,
37
+ ...NotificationResolvers
38
+ ],
39
+ directives: {
40
+ ...NotificationDirectives
41
+ }
42
+ }
@@ -0,0 +1,71 @@
1
+ import { defaultFieldResolver, GraphQLSchema } from 'graphql'
2
+ import gql from 'graphql-tag'
3
+
4
+ import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
5
+
6
+ const debug = require('debug')('things-factory:notification:directive-notification')
7
+
8
+ const DIRECTIVE = 'notification'
9
+
10
+ export const notificationDirectiveTypeDefs = gql`
11
+ directive @notification(
12
+ receivers: String
13
+ topic: String
14
+ subject: String # info
15
+ title: String
16
+ body: String
17
+ image: String
18
+ url: String
19
+ # timestamp = String(Date.now())
20
+ mode: String # background
21
+ ) on FIELD_DEFINITION
22
+ `
23
+ export const directiveNotification = (schema: GraphQLSchema) =>
24
+ mapSchema(schema, {
25
+ [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {
26
+ const notificationDirective = getDirective(schema, fieldConfig, DIRECTIVE)?.[0]
27
+ if (notificationDirective) {
28
+ const { resolve = defaultFieldResolver, args } = fieldConfig
29
+
30
+ if (!args) {
31
+ throw new Error(`Unexpected Error. args should be defined in @notification directive for field ${fieldName}.`)
32
+ }
33
+
34
+ const {
35
+ receivers,
36
+ topic,
37
+ subject = 'info',
38
+ title,
39
+ body,
40
+ image,
41
+ url,
42
+ mode = 'background'
43
+ } = notificationDirective
44
+ const timestamp = Date.now()
45
+
46
+ fieldConfig.resolve = async function (source, args, context, info) {
47
+ const result = await resolve.call(this, source, args, context, info)
48
+
49
+ const { domain, user, notify } = context.state
50
+
51
+ if (!notify) {
52
+ debug('notify not set')
53
+ } else {
54
+ try {
55
+ await notify({
56
+ receivers: [user.id],
57
+ notificationDirective,
58
+ timestamp: new Date()
59
+ })
60
+ } catch (err) {
61
+ debug(err)
62
+ }
63
+ }
64
+
65
+ return result
66
+ }
67
+
68
+ return fieldConfig
69
+ }
70
+ }
71
+ })
@@ -0,0 +1,14 @@
1
+ import { Notification } from './notification'
2
+ import { NotificationQuery } from './notification-query'
3
+ import { NotificationMutation } from './notification-mutation'
4
+ import { NotificationSubscription } from './notification-subscription'
5
+ import { notificationDirectiveTypeDefs, directiveNotification } from './directive-notification'
6
+
7
+ export const typeDefs = {
8
+ notificationDirectiveTypeDefs
9
+ }
10
+ export const entities = [Notification]
11
+ export const resolvers = [NotificationQuery, NotificationMutation, NotificationSubscription]
12
+ export const directives = {
13
+ notification: directiveNotification
14
+ }