@imksh/create 1.0.28 → 1.0.31

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 (127) hide show
  1. package/package.json +1 -1
  2. package/src/utils/askTemplate.js +2 -1
  3. package/templates/layers/electron/base/electron/main.js +262 -0
  4. package/templates/layers/electron/base/electron/preload.js +37 -0
  5. package/templates/layers/electron/base/electron-builder.json5 +68 -0
  6. package/templates/layers/electron/javascript/_gitignore +38 -0
  7. package/templates/layers/electron/javascript/package.json +49 -0
  8. package/templates/layers/electron/javascript/src/components/layout/SidebarLayout.jsx +43 -0
  9. package/templates/layers/electron/javascript/src/components/ui/TitleBar.jsx +130 -0
  10. package/templates/layers/electron/javascript/src/config/appConfig.js +51 -0
  11. package/templates/layers/electron/javascript/src/hooks/useElectron.js +68 -0
  12. package/templates/layers/electron/javascript/src/main.jsx +17 -0
  13. package/templates/layers/electron/javascript/src/store/useUiStore.js +59 -0
  14. package/templates/layers/electron/javascript/vite.config.js +29 -0
  15. package/templates/layers/electron/typescript/_gitignore +38 -0
  16. package/templates/layers/electron/typescript/electron/main.ts +213 -0
  17. package/templates/layers/electron/typescript/electron/preload.ts +52 -0
  18. package/templates/layers/electron/typescript/package.json +52 -0
  19. package/templates/layers/electron/typescript/src/components/layout/SidebarLayout.tsx +42 -0
  20. package/templates/layers/electron/typescript/src/components/ui/TitleBar.tsx +120 -0
  21. package/templates/layers/electron/typescript/src/config/appConfig.ts +52 -0
  22. package/templates/layers/electron/typescript/src/hooks/useElectron.ts +68 -0
  23. package/templates/layers/electron/typescript/src/main.tsx +17 -0
  24. package/templates/layers/electron/typescript/src/store/useUiStore.ts +97 -0
  25. package/templates/layers/electron/typescript/src/vite-env.d.ts +13 -0
  26. package/templates/layers/electron/typescript/tsconfig.json +25 -0
  27. package/templates/layers/electron/typescript/tsconfig.node.json +11 -0
  28. package/templates/layers/electron/typescript/vite.config.ts +29 -0
  29. package/templates/layers/next/javascript/src/components/common/Breadcrumbs.jsx +44 -6
  30. package/templates/layers/next/javascript/src/components/common/Loading.jsx +1 -0
  31. package/templates/layers/next/javascript/src/components/common/LottieIcon.jsx +1 -0
  32. package/templates/layers/next/javascript/src/components/common/Pagination.jsx +170 -0
  33. package/templates/layers/next/javascript/src/components/common/SidebarModal.jsx +180 -0
  34. package/templates/layers/next/javascript/src/components/common/StatsCard.jsx +168 -0
  35. package/templates/layers/next/javascript/src/components/common/Table.jsx +191 -41
  36. package/templates/layers/next/javascript/src/components/common/Tabs.jsx +187 -0
  37. package/templates/layers/next/javascript/src/components/common/ToggleSwitch.jsx +4 -3
  38. package/templates/layers/next/javascript/src/components/layout/ProtectedRoute.jsx +2 -1
  39. package/templates/layers/next/javascript/src/components/layout/PublicLayout.jsx +3 -6
  40. package/templates/layers/next/javascript/src/components/layout/SidebarLayout.jsx +4 -17
  41. package/templates/layers/next/javascript/src/components/modals/ConfirmModal.jsx +4 -12
  42. package/templates/layers/next/javascript/src/components/modals/Modal.jsx +50 -19
  43. package/templates/layers/next/javascript/src/components/ui/Input.jsx +50 -0
  44. package/templates/layers/next/{typescript/src/components/ui/sidebar.tsx → javascript/src/components/ui/Sidebar.jsx} +65 -53
  45. package/templates/layers/next/javascript/src/components/ui/SidebarHeader.jsx +11 -6
  46. package/templates/layers/next/javascript/src/config/appConfig.js +4 -4
  47. package/templates/layers/next/javascript/src/hooks/useWindowSize.js +2 -3
  48. package/templates/layers/next/javascript/src/store/useUiStore.js +9 -4
  49. package/templates/layers/next/typescript/src/components/common/Breadcrumbs.tsx +57 -8
  50. package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -0
  51. package/templates/layers/next/typescript/src/components/common/Pagination.tsx +186 -0
  52. package/templates/layers/next/typescript/src/components/common/SidebarModal.tsx +200 -0
  53. package/templates/layers/next/typescript/src/components/common/StatsCard.tsx +232 -0
  54. package/templates/layers/next/typescript/src/components/common/Table.tsx +201 -35
  55. package/templates/layers/next/typescript/src/components/common/Tabs.tsx +190 -0
  56. package/templates/layers/next/typescript/src/components/common/ToggleSwitch.tsx +12 -8
  57. package/templates/layers/next/typescript/src/components/layout/PublicLayout.tsx +8 -6
  58. package/templates/layers/next/typescript/src/components/layout/SidebarLayout.tsx +8 -17
  59. package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +1 -0
  60. package/templates/layers/next/typescript/src/components/modals/Modal.tsx +67 -21
  61. package/templates/layers/next/typescript/src/components/ui/Button.tsx +1 -0
  62. package/templates/layers/next/typescript/src/components/ui/Input.tsx +40 -60
  63. package/templates/layers/next/{javascript/src/components/ui/sidebar.jsx → typescript/src/components/ui/Sidebar.tsx} +77 -44
  64. package/templates/layers/next/typescript/src/components/ui/SidebarHeader.tsx +16 -10
  65. package/templates/layers/next/typescript/src/config/appConfig.ts +4 -4
  66. package/templates/layers/next/typescript/src/hooks/useWindowSize.ts +2 -3
  67. package/templates/layers/next/typescript/src/store/useUiStore.ts +14 -4
  68. package/templates/layers/node/javascript/package.json +2 -1
  69. package/templates/layers/node/javascript/src/controllers/push.controller.js +35 -14
  70. package/templates/layers/node/javascript/src/models/pushSubscription.model.js +5 -0
  71. package/templates/layers/node/javascript/src/utils/pushHelper.js +59 -12
  72. package/templates/layers/node/typescript/package-lock.json +3795 -0
  73. package/templates/layers/node/typescript/package.json +3 -2
  74. package/templates/layers/node/typescript/src/controllers/push.controller.ts +35 -14
  75. package/templates/layers/node/typescript/src/models/pushSubscription.model.ts +6 -0
  76. package/templates/layers/node/typescript/src/services/auth.service.ts +3 -3
  77. package/templates/layers/node/typescript/src/utils/pushHelper.ts +66 -12
  78. package/templates/layers/node-prisma/javascript/package.json +6 -3
  79. package/templates/layers/node-prisma/javascript/prisma/schema.prisma +20 -9
  80. package/templates/layers/node-prisma/javascript/prisma.config.js +12 -0
  81. package/templates/layers/node-prisma/javascript/src/config/{db.js → prisma.js} +9 -1
  82. package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +58 -19
  83. package/templates/layers/node-prisma/javascript/src/utils/pushHelper.js +62 -14
  84. package/templates/layers/node-prisma/typescript/package-lock.json +4808 -0
  85. package/templates/layers/node-prisma/typescript/package.json +8 -4
  86. package/templates/layers/node-prisma/typescript/prisma/schema.prisma +20 -9
  87. package/templates/layers/node-prisma/typescript/prisma.config.ts +12 -0
  88. package/templates/layers/node-prisma/typescript/src/config/{db.ts → prisma.ts} +9 -1
  89. package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -22
  90. package/templates/layers/node-prisma/typescript/src/index.ts +1 -1
  91. package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +15 -5
  92. package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +134 -128
  93. package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +69 -14
  94. package/templates/layers/prisma/typescript/prisma/schema.prisma +35 -10
  95. package/templates/layers/react/javascript/src/components/common/Breadcrumbs.jsx +62 -58
  96. package/templates/layers/react/javascript/src/components/common/Pagination.jsx +164 -76
  97. package/templates/layers/react/javascript/src/components/common/SidebarModal.jsx +179 -0
  98. package/templates/layers/react/javascript/src/components/common/StatsCard.jsx +167 -0
  99. package/templates/layers/react/javascript/src/components/common/Table.jsx +248 -132
  100. package/templates/layers/react/javascript/src/components/common/Tabs.jsx +164 -112
  101. package/templates/layers/react/javascript/src/components/common/ToggleSwitch.jsx +3 -3
  102. package/templates/layers/react/javascript/src/components/layout/SidebarLayout.jsx +4 -12
  103. package/templates/layers/react/javascript/src/components/modals/Modal.jsx +53 -18
  104. package/templates/layers/react/javascript/src/components/ui/Input.jsx +49 -0
  105. package/templates/layers/react/javascript/src/components/ui/Sidebar.jsx +215 -0
  106. package/templates/layers/react/javascript/src/components/ui/SidebarHeader.jsx +25 -30
  107. package/templates/layers/react/javascript/src/config/appConfig.js +3 -3
  108. package/templates/layers/react/javascript/src/hooks/useWindowSize.js +2 -2
  109. package/templates/layers/react/typescript/src/components/common/Breadcrumbs.tsx +32 -6
  110. package/templates/layers/react/typescript/src/components/common/Pagination.tsx +151 -57
  111. package/templates/layers/react/typescript/src/components/common/SidebarModal.tsx +199 -0
  112. package/templates/layers/react/typescript/src/components/common/StatsCard.tsx +231 -0
  113. package/templates/layers/react/typescript/src/components/common/Table.tsx +197 -34
  114. package/templates/layers/react/typescript/src/components/common/Tabs.tsx +56 -12
  115. package/templates/layers/react/typescript/src/components/common/ToggleSwitch.tsx +15 -3
  116. package/templates/layers/react/typescript/src/components/layout/SidebarLayout.tsx +3 -6
  117. package/templates/layers/react/typescript/src/components/modals/Modal.tsx +151 -0
  118. package/templates/layers/react/typescript/src/components/ui/Input.tsx +58 -0
  119. package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +116 -53
  120. package/templates/layers/react/typescript/src/components/ui/SidebarHeader.tsx +5 -4
  121. package/templates/layers/react/typescript/src/config/appConfig.ts +3 -3
  122. package/templates/layers/react/typescript/src/hooks/useWindowSize.ts +2 -3
  123. package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +2 -1
  124. package/templates/layers/react-native/base/src/config/appConfig.ts +44 -0
  125. package/templates/presets/electron/preset.json +9 -0
  126. package/templates/presets/electron-ts/preset.json +9 -0
  127. package/templates/layers/react/javascript/src/components/ui/sidebar.jsx +0 -169
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "/Users/karan/workspace/Projects/boilerplate/test-app-node-ts",
2
+ "name": "node-ts-template",
3
3
  "version": "1.0.0",
4
4
  "main": "src/index.ts",
5
5
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "multer": "^1.4.5-lts.1",
26
26
  "nodemailer": "^6.9.7",
27
27
  "socket.io": "^4.7.2",
28
- "web-push": "^3.6.6"
28
+ "web-push": "^3.6.6",
29
+ "expo-server-sdk": "^3.10.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/bcrypt": "^5.0.2",
@@ -7,26 +7,47 @@ export const subscribe = async (req: Request, res: Response, next: NextFunction)
7
7
  const subscription = req.body;
8
8
  const userId = req.user?._id || req.user?.id; // supports both mongoose and prisma IDs
9
9
 
10
- if (!subscription || !subscription.endpoint) {
10
+ if (!subscription) {
11
11
  return next({ status: 400, message: "Invalid subscription payload" });
12
12
  }
13
13
 
14
- const existingSub = await PushSubscription.findOne({ endpoint: subscription.endpoint });
14
+ const isExpo = !!subscription.token;
15
+ const isWeb = !!(subscription.endpoint && subscription.keys);
15
16
 
16
- if (existingSub) {
17
- if (userId) {
18
- existingSub.user = userId; await existingSub.save();
19
- }
20
- return res.status(200).json({ success: true, message: "Already subscribed." });
17
+ if (!isExpo && !isWeb) {
18
+ return next({ status: 400, message: "Missing token (EXPO) or endpoint/keys (WEB)" });
21
19
  }
22
20
 
23
-
24
- await PushSubscription.create({
25
- endpoint: subscription.endpoint,
26
- keys: subscription.keys,
27
- user: userId || undefined
28
- });
29
-
21
+ if (isExpo) {
22
+ const existingSub = await PushSubscription.findOne({ expoPushToken: subscription.token });
23
+ if (existingSub) {
24
+ if (userId && String(existingSub.user) !== String(userId)) {
25
+ existingSub.user = userId; await existingSub.save();
26
+ }
27
+ return res.status(200).json({ success: true, message: "Already subscribed." });
28
+ }
29
+
30
+ await PushSubscription.create({
31
+ type: "EXPO",
32
+ expoPushToken: subscription.token,
33
+ user: userId || undefined
34
+ });
35
+ } else {
36
+ const existingSub = await PushSubscription.findOne({ endpoint: subscription.endpoint });
37
+ if (existingSub) {
38
+ if (userId && String(existingSub.user) !== String(userId)) {
39
+ existingSub.user = userId; await existingSub.save();
40
+ }
41
+ return res.status(200).json({ success: true, message: "Already subscribed." });
42
+ }
43
+
44
+ await PushSubscription.create({
45
+ type: "WEB",
46
+ endpoint: subscription.endpoint,
47
+ keys: subscription.keys,
48
+ user: userId || undefined
49
+ });
50
+ }
30
51
 
31
52
  res.status(201).json({ success: true, message: "Subscription created." });
32
53
  } catch (error) {
@@ -2,6 +2,7 @@ import mongoose, { Document } from "mongoose";
2
2
 
3
3
  // Handles both Web Push (endpoint + keys) and Expo Push (expoPushToken)
4
4
  export interface IPushSubscription extends Document {
5
+ type: "WEB" | "EXPO";
5
6
  endpoint?: string;
6
7
  keys?: {
7
8
  p256dh: string;
@@ -17,6 +18,11 @@ export interface IPushSubscription extends Document {
17
18
 
18
19
  const pushSubscriptionSchema = new mongoose.Schema<IPushSubscription>(
19
20
  {
21
+ type: {
22
+ type: String,
23
+ enum: ["WEB", "EXPO"],
24
+ default: "WEB",
25
+ },
20
26
  // Web Push fields
21
27
  endpoint: {
22
28
  type: String,
@@ -46,9 +46,9 @@ export const signup = async ({ name, email, phone, password, otp }: any) => {
46
46
  };
47
47
  export const login = async ({ email, password }: any) => {
48
48
  const user = await User.findOne({ email });
49
- if (!user) {
50
- const err: any = new Error("User not found");
51
- err.status = 404;
49
+ if (!user || !user.password) {
50
+ const err: any = new Error("Invalid Credentials");
51
+ err.status = 401;
52
52
  throw err;
53
53
  }
54
54
  const isValid = await bcrypt.compare(password, user.password);
@@ -1,20 +1,74 @@
1
1
  import webpush from "../config/webPush.js";
2
+ import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
2
3
  import logger from "./logger.js";
3
4
  import PushSubscription from "../models/pushSubscription.model.js";
4
5
 
5
- export const sendPushToUser = async (userId: string, title: string, body: string, url: string = "/") => {
6
+ const expo = new Expo();
7
+
8
+ export interface PushMessagePayload {
9
+ title: string;
10
+ body: string;
11
+ data?: Record<string, unknown>;
12
+ url?: string;
13
+ }
14
+
15
+ export const sendPushToUser = async (
16
+ userId: string,
17
+ title: string,
18
+ body: string,
19
+ url: string = "/",
20
+ ) => {
6
21
  try {
7
- const payload = JSON.stringify({ title, body, url });
8
- const subs = await PushSubscription.find({ user: userId });
9
- for (const sub of subs) {
10
- await webpush.sendNotification({ endpoint: sub.endpoint, keys: sub.keys }, payload).catch(err => {
11
- if (err.statusCode === 404 || err.statusCode === 410) {
12
- sub.deleteOne();
13
- } else {
14
- logger.error("Push Error", err);
15
- }
16
- });
17
- }
22
+ const subscriptions = await PushSubscription.find({ user: userId });
23
+
24
+ if (subscriptions.length === 0) return;
25
+
26
+ const webSubs = subscriptions.filter(s => s.type === "WEB");
27
+ const expoSubs = subscriptions.filter(s => s.type === "EXPO");
28
+
29
+ // Web Push
30
+ if (webSubs.length > 0) {
31
+ const payload = JSON.stringify({ title, body, url });
32
+ await Promise.allSettled(
33
+ webSubs.map(async (sub) => {
34
+ if (!sub.endpoint || !sub.keys) return;
35
+ const pushSub = { endpoint: sub.endpoint, keys: sub.keys as any };
36
+ try {
37
+ await webpush.sendNotification(pushSub, payload);
38
+ } catch (err: any) {
39
+ if (err.statusCode === 404 || err.statusCode === 410) {
40
+ await sub.deleteOne();
41
+ } else {
42
+ logger.error("Web Push Error", err);
43
+ }
44
+ }
45
+ })
46
+ );
47
+ }
48
+
49
+ // Expo Push
50
+ if (expoSubs.length > 0) {
51
+ const messages: ExpoPushMessage[] = [];
52
+ for (const sub of expoSubs) {
53
+ if (!sub.expoPushToken || !Expo.isExpoPushToken(sub.expoPushToken)) continue;
54
+ messages.push({
55
+ to: sub.expoPushToken,
56
+ sound: "default",
57
+ title,
58
+ body,
59
+ data: { url },
60
+ });
61
+ }
62
+
63
+ const chunks = expo.chunkPushNotifications(messages);
64
+ for (const chunk of chunks) {
65
+ try {
66
+ await expo.sendPushNotificationsAsync(chunk);
67
+ } catch (error) {
68
+ logger.error("Error sending push notification chunk", error);
69
+ }
70
+ }
71
+ }
18
72
  } catch (error) {
19
73
  logger.error("Error sending push notification to user", error);
20
74
  }
@@ -11,7 +11,7 @@
11
11
  "generate:vapid": "node ./src/utils/generateVapidKeys.js"
12
12
  },
13
13
  "dependencies": {
14
- "@prisma/client": "^5.6.0",
14
+ "@prisma/client": "^7.0.0",
15
15
  "bcrypt": "^5.1.1",
16
16
  "compression": "^1.7.4",
17
17
  "cookie-parser": "^1.4.6",
@@ -26,10 +26,13 @@
26
26
  "cloudinary": "^2.0.0",
27
27
  "nodemailer": "^6.9.7",
28
28
  "socket.io": "^4.7.2",
29
- "web-push": "^3.6.6"
29
+ "web-push": "^3.6.6",
30
+ "pg": "^8.11.3",
31
+ "@prisma/adapter-pg": "^7.0.0",
32
+ "expo-server-sdk": "^3.10.0"
30
33
  },
31
34
  "devDependencies": {
32
35
  "nodemon": "^3.0.1",
33
- "prisma": "^5.6.0"
36
+ "prisma": "^7.0.0"
34
37
  }
35
38
  }
@@ -4,7 +4,6 @@ generator client {
4
4
 
5
5
  datasource db {
6
6
  provider = "postgresql"
7
- url = env("DATABASE_URL")
8
7
  }
9
8
 
10
9
  model User {
@@ -29,13 +28,25 @@ model Otp {
29
28
  }
30
29
 
31
30
  model PushSubscription {
32
- id String @id @default(uuid())
33
- endpoint String @unique
34
- p256dh String
35
- auth String
36
- role String @default("user")
31
+ id String @id @default(uuid())
32
+ type PushSubscriptionType @default(WEB)
33
+
34
+ // For Web Push
35
+ endpoint String? @unique
36
+ keys Json?
37
+
38
+ // For Expo
39
+ token String? @unique
40
+
37
41
  userId String?
38
- user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
39
- createdAt DateTime @default(now())
40
- updatedAt DateTime @updatedAt
42
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
43
+
44
+ createdAt DateTime @default(now())
45
+ updatedAt DateTime @updatedAt
46
+ deletedAt DateTime?
47
+ }
48
+
49
+ enum PushSubscriptionType {
50
+ WEB
51
+ EXPO
41
52
  }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from '@prisma/config'
2
+ import 'dotenv/config'
3
+
4
+ export default defineConfig({
5
+ schema: 'prisma/schema.prisma',
6
+ migrations: {
7
+ path: 'prisma/migrations',
8
+ },
9
+ datasource: {
10
+ url: process.env.DATABASE_URL,
11
+ },
12
+ })
@@ -1,7 +1,15 @@
1
1
  import { PrismaClient } from "@prisma/client";
2
+ import { Pool } from "pg";
3
+ import { PrismaPg } from "@prisma/adapter-pg";
4
+ import dotenv from "dotenv";
2
5
  import logger from "../utils/logger.js";
3
6
 
4
- export const prisma = new PrismaClient();
7
+ dotenv.config();
8
+
9
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
10
+ const adapter = new PrismaPg(pool);
11
+
12
+ export const prisma = new PrismaClient({ adapter });
5
13
 
6
14
  const connectDB = async () => {
7
15
  try {
@@ -1,34 +1,73 @@
1
1
  import { prisma } from "../config/db.js";
2
2
  import logger from "../utils/logger.js";
3
3
 
4
- export const subscribe = async (req, res, next) => {
4
+ export const subscribe = async (
5
+ req,
6
+ res,
7
+ next,
8
+ ) => {
5
9
  try {
6
10
  const subscription = req.body;
7
- const userId = req.user?._id || req.user?.id; // supports both mongoose and prisma IDs
8
-
9
- if (!subscription || !subscription.endpoint) {
11
+ const userId = req.user?._id || req.user?.id;
12
+
13
+ if (!subscription) {
10
14
  return next({ status: 400, message: "Invalid subscription payload" });
11
15
  }
12
16
 
13
- const existingSub = await prisma.pushSubscription.findUnique({ where: { endpoint: subscription.endpoint } });
17
+ // Determine type: EXPO if token is present, else WEB
18
+ const isExpo = !!subscription.token;
19
+ const isWeb = !!(subscription.endpoint && subscription.keys);
14
20
 
15
- if (existingSub) {
16
- if (userId) {
17
- await prisma.pushSubscription.update({ where: { endpoint: subscription.endpoint }, data: { userId } })
18
- }
19
- return res.status(200).json({ success: true, message: "Already subscribed." });
21
+ if (!isExpo && !isWeb) {
22
+ return next({ status: 400, message: "Missing token (EXPO) or endpoint/keys (WEB)" });
20
23
  }
21
24
 
22
-
23
- await prisma.pushSubscription.create({
24
- data: {
25
- endpoint: subscription.endpoint,
26
- p256dh: subscription.keys.p256dh,
27
- auth: subscription.keys.auth,
28
- userId: userId || null
25
+ if (isExpo) {
26
+ const existingSub = await prisma.pushSubscription.findUnique({
27
+ where: { token: subscription.token },
28
+ });
29
+
30
+ if (existingSub) {
31
+ if (userId && existingSub.userId !== userId) {
32
+ await prisma.pushSubscription.update({
33
+ where: { token: subscription.token },
34
+ data: { userId },
35
+ });
36
+ }
37
+ return res.status(200).json({ success: true, message: "Already subscribed." });
38
+ }
39
+
40
+ await prisma.pushSubscription.create({
41
+ data: {
42
+ type: "EXPO",
43
+ token: subscription.token,
44
+ userId: userId || null,
45
+ },
46
+ });
47
+ } else {
48
+ const existingSub = await prisma.pushSubscription.findUnique({
49
+ where: { endpoint: subscription.endpoint },
50
+ });
51
+
52
+ if (existingSub) {
53
+ if (userId && existingSub.userId !== userId) {
54
+ await prisma.pushSubscription.update({
55
+ where: { endpoint: subscription.endpoint },
56
+ data: { userId },
57
+ });
58
+ }
59
+ return res.status(200).json({ success: true, message: "Already subscribed." });
29
60
  }
30
- });
31
-
61
+
62
+ await prisma.pushSubscription.create({
63
+ data: {
64
+ type: "WEB",
65
+ endpoint: subscription.endpoint,
66
+ keys: subscription.keys,
67
+ userId: userId || null,
68
+ },
69
+ });
70
+ }
32
71
 
33
72
  res.status(201).json({ success: true, message: "Subscription created." });
34
73
  } catch (error) {
@@ -1,21 +1,69 @@
1
1
  import webpush from "../config/webPush.js";
2
+ import { Expo } from "expo-server-sdk";
2
3
  import logger from "./logger.js";
3
- import { prisma } from "../config/db.js";
4
+ import { prisma } from "../config/prisma.js";
4
5
 
5
- export const sendPushToUser = async (userId, title, body, url = "/") => {
6
+ const expo = new Expo();
7
+
8
+ export const sendPushToUser = async (
9
+ userId,
10
+ title,
11
+ body,
12
+ url = "/",
13
+ ) => {
6
14
  try {
7
- const payload = JSON.stringify({ title, body, url });
8
- const subs = await prisma.pushSubscription.findMany({ where: { userId } });
9
- for (const sub of subs) {
10
- const pushSub = { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } };
11
- await webpush.sendNotification(pushSub, payload).catch(err => {
12
- if (err.statusCode === 404 || err.statusCode === 410) {
13
- prisma.pushSubscription.delete({ where: { id: sub.id } }).catch(()=>{});
14
- } else {
15
- logger.error("Push Error", err);
16
- }
17
- });
18
- }
15
+ const subscriptions = await prisma.pushSubscription.findMany({
16
+ where: { userId },
17
+ });
18
+
19
+ if (subscriptions.length === 0) return;
20
+
21
+ const webSubs = subscriptions.filter(s => s.type === "WEB");
22
+ const expoSubs = subscriptions.filter(s => s.type === "EXPO");
23
+
24
+ // Web Push
25
+ if (webSubs.length > 0) {
26
+ const payload = JSON.stringify({ title, body, url });
27
+ await Promise.allSettled(
28
+ webSubs.map(async (sub) => {
29
+ if (!sub.endpoint || !sub.keys) return;
30
+ const pushSub = { endpoint: sub.endpoint, keys: sub.keys };
31
+ try {
32
+ await webpush.sendNotification(pushSub, payload);
33
+ } catch (err) {
34
+ if (err.statusCode === 404 || err.statusCode === 410) {
35
+ await prisma.pushSubscription.delete({ where: { id: sub.id } }).catch(() => {});
36
+ } else {
37
+ logger.error("Web Push Error", err);
38
+ }
39
+ }
40
+ })
41
+ );
42
+ }
43
+
44
+ // Expo Push
45
+ if (expoSubs.length > 0) {
46
+ const messages = [];
47
+ for (const sub of expoSubs) {
48
+ if (!sub.token || !Expo.isExpoPushToken(sub.token)) continue;
49
+ messages.push({
50
+ to: sub.token,
51
+ sound: "default",
52
+ title,
53
+ body,
54
+ data: { url },
55
+ });
56
+ }
57
+
58
+ const chunks = expo.chunkPushNotifications(messages);
59
+ for (const chunk of chunks) {
60
+ try {
61
+ await expo.sendPushNotificationsAsync(chunk);
62
+ } catch (error) {
63
+ logger.error("Error sending push notification chunk", error);
64
+ }
65
+ }
66
+ }
19
67
  } catch (error) {
20
68
  logger.error("Error sending push notification to user", error);
21
69
  }