strapi-plugin-notifier 1.0.0

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.
@@ -0,0 +1,467 @@
1
+ const register = async ({ strapi }) => {
2
+ strapi.admin?.services?.permission?.actionProvider?.registerMany([
3
+ {
4
+ section: "plugins",
5
+ displayName: "Read settings",
6
+ uid: "settings.read",
7
+ pluginName: "notifier"
8
+ },
9
+ {
10
+ section: "plugins",
11
+ displayName: "Update settings",
12
+ uid: "settings.update",
13
+ pluginName: "notifier"
14
+ }
15
+ ]);
16
+ strapi.cron.add({
17
+ notifierCleanup: {
18
+ task: async ({ strapi: s }) => {
19
+ const settings2 = await s.plugin("notifier").service("settings").getEffective();
20
+ const { maxDays, maxPerUser } = settings2.retention;
21
+ await s.plugin("notifier").service("notification").cleanupOld(maxDays, maxPerUser);
22
+ },
23
+ options: {
24
+ rule: "0 3 * * *"
25
+ // 3 AM daily — configurable in cron settings if needed
26
+ }
27
+ }
28
+ });
29
+ };
30
+ const bootstrap = async ({ strapi }) => {
31
+ const stored = await strapi.store({ environment: "", type: "plugin", name: "notifier" }).get({ key: "settings" });
32
+ if (!stored) {
33
+ const effective = await strapi.plugin("notifier").service("settings").getEffective();
34
+ await strapi.store({ environment: "", type: "plugin", name: "notifier" }).set({ key: "settings", value: effective });
35
+ }
36
+ };
37
+ const kind = "collectionType";
38
+ const collectionName = "notifier_notifications";
39
+ const info = {
40
+ singularName: "notification",
41
+ pluralName: "notifications",
42
+ displayName: "Notification",
43
+ description: "Admin-panel notifications managed by strapi-plugin-notifier"
44
+ };
45
+ const options = {
46
+ draftAndPublish: false
47
+ };
48
+ const pluginOptions = {
49
+ "content-manager": {
50
+ visible: false
51
+ },
52
+ "content-type-builder": {
53
+ visible: false
54
+ }
55
+ };
56
+ const attributes = {
57
+ title: {
58
+ type: "string",
59
+ required: true
60
+ },
61
+ message: {
62
+ type: "text"
63
+ },
64
+ type: {
65
+ type: "enumeration",
66
+ "enum": [
67
+ "info",
68
+ "success",
69
+ "warning",
70
+ "error"
71
+ ],
72
+ "default": "info"
73
+ },
74
+ read: {
75
+ type: "boolean",
76
+ "default": false,
77
+ required: true
78
+ },
79
+ url: {
80
+ type: "string"
81
+ },
82
+ recipientId: {
83
+ type: "integer"
84
+ },
85
+ recipientRole: {
86
+ type: "string"
87
+ }
88
+ };
89
+ const schema = {
90
+ kind,
91
+ collectionName,
92
+ info,
93
+ options,
94
+ pluginOptions,
95
+ attributes
96
+ };
97
+ const notification$3 = { schema };
98
+ const contentTypes = { notification: notification$3 };
99
+ const UID = "plugin::notifier.notification";
100
+ const accessFilter = (userId, roleCodes = []) => ({
101
+ $or: [
102
+ { recipientId: userId },
103
+ ...roleCodes.length ? [{ recipientRole: { $in: roleCodes } }] : [],
104
+ { recipientId: null, recipientRole: null }
105
+ ]
106
+ });
107
+ const notification$2 = ({ strapi }) => ({
108
+ findByRecipient(userId, roleCodes, { page = 1, pageSize = 20 } = {}) {
109
+ return strapi.db.query(UID).findMany({
110
+ where: accessFilter(userId, roleCodes),
111
+ orderBy: { createdAt: "desc" },
112
+ limit: pageSize,
113
+ offset: (page - 1) * pageSize
114
+ });
115
+ },
116
+ countByRecipient(userId, roleCodes) {
117
+ return strapi.db.query(UID).count({ where: accessFilter(userId, roleCodes) });
118
+ },
119
+ countUnread(userId, roleCodes) {
120
+ return strapi.db.query(UID).count({
121
+ where: { ...accessFilter(userId, roleCodes), read: false }
122
+ });
123
+ },
124
+ async markAsRead(id, userId, roleCodes) {
125
+ const existing = await strapi.db.query(UID).findOne({ where: { id, ...accessFilter(userId, roleCodes) } });
126
+ if (!existing) return null;
127
+ return strapi.db.query(UID).update({ where: { id }, data: { read: true } });
128
+ },
129
+ markAllAsRead(userId, roleCodes) {
130
+ return strapi.db.query(UID).updateMany({
131
+ where: { ...accessFilter(userId, roleCodes), read: false },
132
+ data: { read: true }
133
+ });
134
+ },
135
+ async delete(id, userId, roleCodes) {
136
+ const existing = await strapi.db.query(UID).findOne({ where: { id, ...accessFilter(userId, roleCodes) } });
137
+ if (!existing) return null;
138
+ return strapi.db.query(UID).delete({ where: { id } });
139
+ },
140
+ clearAll(userId, roleCodes) {
141
+ return strapi.db.query(UID).deleteMany({ where: accessFilter(userId, roleCodes) });
142
+ },
143
+ create({ title, message, type = "info", url, recipientId, recipientRole }) {
144
+ return strapi.db.query(UID).create({
145
+ data: { title, message, type, url, read: false, recipientId, recipientRole }
146
+ });
147
+ },
148
+ /** Retention cleanup: remove notifications older than maxDays and enforce per-user cap. */
149
+ async cleanupOld(maxDays, maxPerUser) {
150
+ if (maxDays > 0) {
151
+ const cutoff = /* @__PURE__ */ new Date();
152
+ cutoff.setDate(cutoff.getDate() - maxDays);
153
+ await strapi.db.query(UID).deleteMany({
154
+ where: { createdAt: { $lt: cutoff.toISOString() } }
155
+ });
156
+ }
157
+ if (maxPerUser > 0) {
158
+ const rows = await strapi.db.query(UID).findMany({
159
+ select: ["recipientId"],
160
+ where: { recipientId: { $notNull: true } }
161
+ });
162
+ const ids = [...new Set(rows.map((r) => r.recipientId))];
163
+ for (const recipientId of ids) {
164
+ const total = await strapi.db.query(UID).count({ where: { recipientId } });
165
+ if (total > maxPerUser) {
166
+ const excess = await strapi.db.query(UID).findMany({
167
+ where: { recipientId },
168
+ orderBy: { createdAt: "asc" },
169
+ limit: total - maxPerUser,
170
+ select: ["id"]
171
+ });
172
+ const excessIds = excess.map((e) => e.id);
173
+ await strapi.db.query(UID).deleteMany({ where: { id: { $in: excessIds } } });
174
+ }
175
+ }
176
+ }
177
+ }
178
+ });
179
+ const notifier = ({ strapi }) => {
180
+ const svc = () => strapi.plugin("notifier").service("notification");
181
+ return {
182
+ send({ title, message, type, url, to }) {
183
+ return svc().create({
184
+ title,
185
+ message,
186
+ type,
187
+ url,
188
+ recipientId: to?.userId,
189
+ recipientRole: to?.role
190
+ });
191
+ },
192
+ broadcast(opts) {
193
+ return svc().create({ ...opts });
194
+ },
195
+ toRole(role, opts) {
196
+ return svc().create({ ...opts, recipientRole: role });
197
+ },
198
+ toUser(userId, opts) {
199
+ return svc().create({ ...opts, recipientId: userId });
200
+ }
201
+ };
202
+ };
203
+ const DEFAULT_SETTINGS = {
204
+ retention: {
205
+ maxDays: 90,
206
+ maxPerUser: 500
207
+ },
208
+ delivery: {
209
+ defaultRecipient: "broadcast",
210
+ allowedRoles: []
211
+ },
212
+ ui: {
213
+ pollInterval: 3e4,
214
+ pageSize: 20,
215
+ defaultFilter: "all",
216
+ badge: {
217
+ enabled: true,
218
+ color: "#ee5e52"
219
+ },
220
+ theme: {
221
+ bellSize: "1.5em",
222
+ accent: {
223
+ info: "#4945ff",
224
+ success: "#328048",
225
+ warning: "#d97706",
226
+ error: "#d02b20"
227
+ }
228
+ }
229
+ }
230
+ };
231
+ const mergeWithDefaults = (overrides = {}) => ({
232
+ retention: { ...DEFAULT_SETTINGS.retention, ...overrides.retention ?? {} },
233
+ delivery: { ...DEFAULT_SETTINGS.delivery, ...overrides.delivery ?? {} },
234
+ ui: {
235
+ ...DEFAULT_SETTINGS.ui,
236
+ ...overrides.ui ?? {},
237
+ badge: { ...DEFAULT_SETTINGS.ui.badge, ...overrides.ui?.badge ?? {} },
238
+ theme: {
239
+ ...DEFAULT_SETTINGS.ui.theme,
240
+ ...overrides.ui?.theme ?? {},
241
+ accent: { ...DEFAULT_SETTINGS.ui.theme.accent, ...overrides.ui?.theme?.accent ?? {} }
242
+ }
243
+ }
244
+ });
245
+ const STORE_KEY = "settings";
246
+ const getStore = (strapi) => strapi.store({ environment: "", type: "plugin", name: "notifier" });
247
+ const settings$2 = ({ strapi }) => ({
248
+ /** Returns settings from the plugin store, falling back to defaults. */
249
+ async get() {
250
+ const stored = await getStore(strapi).get({ key: STORE_KEY });
251
+ return stored ?? { ...DEFAULT_SETTINGS };
252
+ },
253
+ /**
254
+ * Returns the effective settings: defaults → config/plugins.ts → plugin store (highest priority).
255
+ * This is the source of truth used by all server-side logic.
256
+ */
257
+ async getEffective() {
258
+ const fromPluginsConfig = strapi.config.get("plugin::notifier", {});
259
+ const fromStore = await getStore(strapi).get({ key: STORE_KEY });
260
+ const withPluginsConfig = mergeWithDefaults(fromPluginsConfig);
261
+ if (!fromStore) return withPluginsConfig;
262
+ return {
263
+ retention: { ...withPluginsConfig.retention, ...fromStore.retention ?? {} },
264
+ delivery: { ...withPluginsConfig.delivery, ...fromStore.delivery ?? {} },
265
+ ui: {
266
+ ...withPluginsConfig.ui,
267
+ ...fromStore.ui ?? {},
268
+ badge: { ...withPluginsConfig.ui.badge, ...fromStore.ui?.badge ?? {} },
269
+ theme: {
270
+ ...withPluginsConfig.ui.theme,
271
+ ...fromStore.ui?.theme ?? {},
272
+ accent: {
273
+ ...withPluginsConfig.ui.theme.accent,
274
+ ...fromStore.ui?.theme?.accent ?? {}
275
+ }
276
+ }
277
+ }
278
+ };
279
+ },
280
+ /** Persists a full or partial settings update to the plugin store. */
281
+ async update(patch) {
282
+ const current = await this.get();
283
+ const next = {
284
+ retention: { ...current.retention, ...patch.retention ?? {} },
285
+ delivery: { ...current.delivery, ...patch.delivery ?? {} },
286
+ ui: {
287
+ ...current.ui,
288
+ ...patch.ui ?? {},
289
+ badge: { ...current.ui.badge, ...patch.ui?.badge ?? {} },
290
+ theme: {
291
+ ...current.ui.theme,
292
+ ...patch.ui?.theme ?? {},
293
+ accent: { ...current.ui.theme.accent, ...patch.ui?.theme?.accent ?? {} }
294
+ }
295
+ }
296
+ };
297
+ await getStore(strapi).set({ key: STORE_KEY, value: next });
298
+ return next;
299
+ },
300
+ /** Resets plugin store settings, reverting to config/plugins.ts + built-in defaults. */
301
+ async reset() {
302
+ await getStore(strapi).delete({ key: STORE_KEY });
303
+ }
304
+ });
305
+ const services = { notification: notification$2, notifier, settings: settings$2 };
306
+ const getUserRoleCodes = async (strapi, userId) => {
307
+ const user = await strapi.db.query("admin::user").findOne({
308
+ where: { id: userId },
309
+ populate: ["roles"]
310
+ });
311
+ return user?.roles?.map((r) => r.code) ?? [];
312
+ };
313
+ const notification$1 = ({ strapi }) => ({
314
+ async find(ctx) {
315
+ const adminUser = ctx.state.user;
316
+ if (!adminUser) return ctx.unauthorized();
317
+ const settings2 = await strapi.plugin("notifier").service("settings").getEffective();
318
+ const page = Math.max(1, Number(ctx.query.page ?? 1));
319
+ const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize ?? settings2.ui.pageSize)));
320
+ const roleCodes = await getUserRoleCodes(strapi, adminUser.id);
321
+ const svc = strapi.plugin("notifier").service("notification");
322
+ const [data, total] = await Promise.all([
323
+ svc.findByRecipient(adminUser.id, roleCodes, { page, pageSize }),
324
+ svc.countByRecipient(adminUser.id, roleCodes)
325
+ ]);
326
+ ctx.body = {
327
+ data,
328
+ pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) }
329
+ };
330
+ },
331
+ async markAsRead(ctx) {
332
+ const adminUser = ctx.state.user;
333
+ if (!adminUser) return ctx.unauthorized();
334
+ const id = Number(ctx.params.id);
335
+ const roleCodes = await getUserRoleCodes(strapi, adminUser.id);
336
+ const notification2 = await strapi.plugin("notifier").service("notification").markAsRead(id, adminUser.id, roleCodes);
337
+ if (!notification2) return ctx.notFound();
338
+ ctx.body = { data: notification2 };
339
+ },
340
+ async markAllAsRead(ctx) {
341
+ const adminUser = ctx.state.user;
342
+ if (!adminUser) return ctx.unauthorized();
343
+ const roleCodes = await getUserRoleCodes(strapi, adminUser.id);
344
+ await strapi.plugin("notifier").service("notification").markAllAsRead(adminUser.id, roleCodes);
345
+ ctx.body = { ok: true };
346
+ },
347
+ async clear(ctx) {
348
+ const adminUser = ctx.state.user;
349
+ if (!adminUser) return ctx.unauthorized();
350
+ const id = Number(ctx.params.id);
351
+ const roleCodes = await getUserRoleCodes(strapi, adminUser.id);
352
+ const deleted = await strapi.plugin("notifier").service("notification").delete(id, adminUser.id, roleCodes);
353
+ if (!deleted) return ctx.notFound();
354
+ ctx.body = { ok: true };
355
+ },
356
+ async clearAll(ctx) {
357
+ const adminUser = ctx.state.user;
358
+ if (!adminUser) return ctx.unauthorized();
359
+ const roleCodes = await getUserRoleCodes(strapi, adminUser.id);
360
+ await strapi.plugin("notifier").service("notification").clearAll(adminUser.id, roleCodes);
361
+ ctx.body = { ok: true };
362
+ }
363
+ });
364
+ const config$1 = ({ strapi }) => ({
365
+ async find(ctx) {
366
+ const settings2 = await strapi.plugin("notifier").service("settings").getEffective();
367
+ ctx.body = { data: settings2.ui };
368
+ }
369
+ });
370
+ const settings$1 = ({ strapi }) => ({
371
+ async find(ctx) {
372
+ const settings2 = await strapi.plugin("notifier").service("settings").getEffective();
373
+ ctx.body = { data: settings2 };
374
+ },
375
+ async update(ctx) {
376
+ const patch = ctx.request.body;
377
+ const updated = await strapi.plugin("notifier").service("settings").update(patch);
378
+ ctx.body = { data: updated };
379
+ },
380
+ async reset(ctx) {
381
+ await strapi.plugin("notifier").service("settings").reset();
382
+ const defaults = await strapi.plugin("notifier").service("settings").getEffective();
383
+ ctx.body = { data: defaults };
384
+ }
385
+ });
386
+ const controllers = { notification: notification$1, config: config$1, settings: settings$1 };
387
+ const notification = {
388
+ type: "admin",
389
+ routes: [
390
+ {
391
+ method: "GET",
392
+ path: "/notifications",
393
+ handler: "notification.find",
394
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
395
+ },
396
+ {
397
+ method: "PUT",
398
+ path: "/notifications/read-all",
399
+ handler: "notification.markAllAsRead",
400
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
401
+ },
402
+ {
403
+ method: "DELETE",
404
+ path: "/notifications",
405
+ handler: "notification.clearAll",
406
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
407
+ },
408
+ {
409
+ method: "PUT",
410
+ path: "/notifications/:id/read",
411
+ handler: "notification.markAsRead",
412
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
413
+ },
414
+ {
415
+ method: "DELETE",
416
+ path: "/notifications/:id",
417
+ handler: "notification.clear",
418
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
419
+ }
420
+ ]
421
+ };
422
+ const config = {
423
+ type: "admin",
424
+ routes: [
425
+ {
426
+ method: "GET",
427
+ path: "/config",
428
+ handler: "config.find",
429
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
430
+ }
431
+ ]
432
+ };
433
+ const settings = {
434
+ type: "admin",
435
+ routes: [
436
+ {
437
+ method: "GET",
438
+ path: "/settings",
439
+ handler: "settings.find",
440
+ config: { policies: ["admin::hasPermissions"], config: { actions: ["plugin::notifier.settings.read"] } }
441
+ },
442
+ {
443
+ method: "PUT",
444
+ path: "/settings",
445
+ handler: "settings.update",
446
+ config: { policies: ["admin::hasPermissions"], config: { actions: ["plugin::notifier.settings.update"] } }
447
+ },
448
+ {
449
+ method: "DELETE",
450
+ path: "/settings",
451
+ handler: "settings.reset",
452
+ config: { policies: ["admin::hasPermissions"], config: { actions: ["plugin::notifier.settings.update"] } }
453
+ }
454
+ ]
455
+ };
456
+ const adminRoutes = [notification, config, settings];
457
+ const index = {
458
+ register,
459
+ bootstrap,
460
+ contentTypes,
461
+ services,
462
+ controllers,
463
+ routes: adminRoutes
464
+ };
465
+ export {
466
+ index as default
467
+ };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "strapi-plugin-notifier",
3
+ "version": "1.0.0",
4
+ "description": "A highly configurable notification inbox plugin for the Strapi v5 admin panel",
5
+ "keywords": [
6
+ "strapi",
7
+ "strapi-plugin",
8
+ "notifications",
9
+ "inbox",
10
+ "admin"
11
+ ],
12
+ "homepage": "https://github.com/datrine/strapi-plugin-notifier#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/datrine/strapi-plugin-notifier/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/datrine/strapi-plugin-notifier.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "datrine",
22
+ "type": "commonjs",
23
+ "exports": {
24
+ "./strapi-admin": {
25
+ "source": "./admin/src/index.ts",
26
+ "import": "./dist/admin/index.mjs",
27
+ "require": "./dist/admin/index.js",
28
+ "default": "./dist/admin/index.js"
29
+ },
30
+ "./strapi-server": {
31
+ "source": "./server/src/index.ts",
32
+ "import": "./dist/server/index.mjs",
33
+ "require": "./dist/server/index.js",
34
+ "default": "./dist/server/index.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "main": "./dist/server/index.js",
39
+ "scripts": {
40
+ "build": "npx strapi-plugin build",
41
+ "watch": "npx strapi-plugin watch",
42
+ "watch:link": "npx strapi-plugin watch:link",
43
+ "verify": "npx strapi-plugin verify",
44
+ "prepublishOnly": "npm run build"
45
+ },
46
+ "devDependencies": {
47
+ "@radix-ui/react-dialog": "^1.1.16",
48
+ "@radix-ui/react-popover": "^1.1.16",
49
+ "@radix-ui/react-toolbar": "^1.1.12",
50
+ "@strapi/admin": "^5.0.0",
51
+ "@strapi/design-system": "^2.2.0",
52
+ "@strapi/icons": "^2.2.0",
53
+ "@strapi/sdk-plugin": "^5.2.6",
54
+ "@strapi/strapi": "^5.0.0",
55
+ "@types/react": "^18.0.0",
56
+ "@types/react-dom": "^18.0.0",
57
+ "react": "^18.0.0",
58
+ "react-dom": "^18.0.0",
59
+ "react-router-dom": "^6.0.0",
60
+ "styled-components": "^6.4.2",
61
+ "typescript": "^5.0.0"
62
+ },
63
+ "peerDependencies": {
64
+ "@strapi/design-system": "^2.0.0",
65
+ "@strapi/icons": "^2.0.0",
66
+ "@strapi/strapi": "^5.0.0",
67
+ "framer-motion": "*",
68
+ "react": "^18.0.0",
69
+ "react-dom": "^18.0.0"
70
+ },
71
+ "engines": {
72
+ "node": ">=18.0.0 <=22.x.x",
73
+ "npm": ">=6.0.0"
74
+ },
75
+ "module": "./dist/server/index.mjs",
76
+ "source": "./server/src/index.ts",
77
+ "strapi": {
78
+ "kind": "plugin",
79
+ "displayName": "Notifier",
80
+ "description": "A highly configurable notification inbox plugin for the Strapi v5 admin panel"
81
+ },
82
+ "dependencies": {
83
+ "browserslist": "^4.28.2",
84
+ "caniuse-lite": "^1.0.30001797"
85
+ }
86
+ }