@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.
- package/package.json +1 -1
- package/src/utils/askTemplate.js +2 -1
- package/templates/layers/electron/base/electron/main.js +262 -0
- package/templates/layers/electron/base/electron/preload.js +37 -0
- package/templates/layers/electron/base/electron-builder.json5 +68 -0
- package/templates/layers/electron/javascript/_gitignore +38 -0
- package/templates/layers/electron/javascript/package.json +49 -0
- package/templates/layers/electron/javascript/src/components/layout/SidebarLayout.jsx +43 -0
- package/templates/layers/electron/javascript/src/components/ui/TitleBar.jsx +130 -0
- package/templates/layers/electron/javascript/src/config/appConfig.js +51 -0
- package/templates/layers/electron/javascript/src/hooks/useElectron.js +68 -0
- package/templates/layers/electron/javascript/src/main.jsx +17 -0
- package/templates/layers/electron/javascript/src/store/useUiStore.js +59 -0
- package/templates/layers/electron/javascript/vite.config.js +29 -0
- package/templates/layers/electron/typescript/_gitignore +38 -0
- package/templates/layers/electron/typescript/electron/main.ts +213 -0
- package/templates/layers/electron/typescript/electron/preload.ts +52 -0
- package/templates/layers/electron/typescript/package.json +52 -0
- package/templates/layers/electron/typescript/src/components/layout/SidebarLayout.tsx +42 -0
- package/templates/layers/electron/typescript/src/components/ui/TitleBar.tsx +120 -0
- package/templates/layers/electron/typescript/src/config/appConfig.ts +52 -0
- package/templates/layers/electron/typescript/src/hooks/useElectron.ts +68 -0
- package/templates/layers/electron/typescript/src/main.tsx +17 -0
- package/templates/layers/electron/typescript/src/store/useUiStore.ts +97 -0
- package/templates/layers/electron/typescript/src/vite-env.d.ts +13 -0
- package/templates/layers/electron/typescript/tsconfig.json +25 -0
- package/templates/layers/electron/typescript/tsconfig.node.json +11 -0
- package/templates/layers/electron/typescript/vite.config.ts +29 -0
- package/templates/layers/next/javascript/src/components/common/Breadcrumbs.jsx +44 -6
- package/templates/layers/next/javascript/src/components/common/Loading.jsx +1 -0
- package/templates/layers/next/javascript/src/components/common/LottieIcon.jsx +1 -0
- package/templates/layers/next/javascript/src/components/common/Pagination.jsx +170 -0
- package/templates/layers/next/javascript/src/components/common/SidebarModal.jsx +180 -0
- package/templates/layers/next/javascript/src/components/common/StatsCard.jsx +168 -0
- package/templates/layers/next/javascript/src/components/common/Table.jsx +191 -41
- package/templates/layers/next/javascript/src/components/common/Tabs.jsx +187 -0
- package/templates/layers/next/javascript/src/components/common/ToggleSwitch.jsx +4 -3
- package/templates/layers/next/javascript/src/components/layout/ProtectedRoute.jsx +2 -1
- package/templates/layers/next/javascript/src/components/layout/PublicLayout.jsx +3 -6
- package/templates/layers/next/javascript/src/components/layout/SidebarLayout.jsx +4 -17
- package/templates/layers/next/javascript/src/components/modals/ConfirmModal.jsx +4 -12
- package/templates/layers/next/javascript/src/components/modals/Modal.jsx +50 -19
- package/templates/layers/next/javascript/src/components/ui/Input.jsx +50 -0
- package/templates/layers/next/{typescript/src/components/ui/sidebar.tsx → javascript/src/components/ui/Sidebar.jsx} +65 -53
- package/templates/layers/next/javascript/src/components/ui/SidebarHeader.jsx +11 -6
- package/templates/layers/next/javascript/src/config/appConfig.js +4 -4
- package/templates/layers/next/javascript/src/hooks/useWindowSize.js +2 -3
- package/templates/layers/next/javascript/src/store/useUiStore.js +9 -4
- package/templates/layers/next/typescript/src/components/common/Breadcrumbs.tsx +57 -8
- package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -0
- package/templates/layers/next/typescript/src/components/common/Pagination.tsx +186 -0
- package/templates/layers/next/typescript/src/components/common/SidebarModal.tsx +200 -0
- package/templates/layers/next/typescript/src/components/common/StatsCard.tsx +232 -0
- package/templates/layers/next/typescript/src/components/common/Table.tsx +201 -35
- package/templates/layers/next/typescript/src/components/common/Tabs.tsx +190 -0
- package/templates/layers/next/typescript/src/components/common/ToggleSwitch.tsx +12 -8
- package/templates/layers/next/typescript/src/components/layout/PublicLayout.tsx +8 -6
- package/templates/layers/next/typescript/src/components/layout/SidebarLayout.tsx +8 -17
- package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +1 -0
- package/templates/layers/next/typescript/src/components/modals/Modal.tsx +67 -21
- package/templates/layers/next/typescript/src/components/ui/Button.tsx +1 -0
- package/templates/layers/next/typescript/src/components/ui/Input.tsx +40 -60
- package/templates/layers/next/{javascript/src/components/ui/sidebar.jsx → typescript/src/components/ui/Sidebar.tsx} +77 -44
- package/templates/layers/next/typescript/src/components/ui/SidebarHeader.tsx +16 -10
- package/templates/layers/next/typescript/src/config/appConfig.ts +4 -4
- package/templates/layers/next/typescript/src/hooks/useWindowSize.ts +2 -3
- package/templates/layers/next/typescript/src/store/useUiStore.ts +14 -4
- package/templates/layers/node/javascript/package.json +2 -1
- package/templates/layers/node/javascript/src/controllers/push.controller.js +35 -14
- package/templates/layers/node/javascript/src/models/pushSubscription.model.js +5 -0
- package/templates/layers/node/javascript/src/utils/pushHelper.js +59 -12
- package/templates/layers/node/typescript/package-lock.json +3795 -0
- package/templates/layers/node/typescript/package.json +3 -2
- package/templates/layers/node/typescript/src/controllers/push.controller.ts +35 -14
- package/templates/layers/node/typescript/src/models/pushSubscription.model.ts +6 -0
- package/templates/layers/node/typescript/src/services/auth.service.ts +3 -3
- package/templates/layers/node/typescript/src/utils/pushHelper.ts +66 -12
- package/templates/layers/node-prisma/javascript/package.json +6 -3
- package/templates/layers/node-prisma/javascript/prisma/schema.prisma +20 -9
- package/templates/layers/node-prisma/javascript/prisma.config.js +12 -0
- package/templates/layers/node-prisma/javascript/src/config/{db.js → prisma.js} +9 -1
- package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +58 -19
- package/templates/layers/node-prisma/javascript/src/utils/pushHelper.js +62 -14
- package/templates/layers/node-prisma/typescript/package-lock.json +4808 -0
- package/templates/layers/node-prisma/typescript/package.json +8 -4
- package/templates/layers/node-prisma/typescript/prisma/schema.prisma +20 -9
- package/templates/layers/node-prisma/typescript/prisma.config.ts +12 -0
- package/templates/layers/node-prisma/typescript/src/config/{db.ts → prisma.ts} +9 -1
- package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -22
- package/templates/layers/node-prisma/typescript/src/index.ts +1 -1
- package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +15 -5
- package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +134 -128
- package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +69 -14
- package/templates/layers/prisma/typescript/prisma/schema.prisma +35 -10
- package/templates/layers/react/javascript/src/components/common/Breadcrumbs.jsx +62 -58
- package/templates/layers/react/javascript/src/components/common/Pagination.jsx +164 -76
- package/templates/layers/react/javascript/src/components/common/SidebarModal.jsx +179 -0
- package/templates/layers/react/javascript/src/components/common/StatsCard.jsx +167 -0
- package/templates/layers/react/javascript/src/components/common/Table.jsx +248 -132
- package/templates/layers/react/javascript/src/components/common/Tabs.jsx +164 -112
- package/templates/layers/react/javascript/src/components/common/ToggleSwitch.jsx +3 -3
- package/templates/layers/react/javascript/src/components/layout/SidebarLayout.jsx +4 -12
- package/templates/layers/react/javascript/src/components/modals/Modal.jsx +53 -18
- package/templates/layers/react/javascript/src/components/ui/Input.jsx +49 -0
- package/templates/layers/react/javascript/src/components/ui/Sidebar.jsx +215 -0
- package/templates/layers/react/javascript/src/components/ui/SidebarHeader.jsx +25 -30
- package/templates/layers/react/javascript/src/config/appConfig.js +3 -3
- package/templates/layers/react/javascript/src/hooks/useWindowSize.js +2 -2
- package/templates/layers/react/typescript/src/components/common/Breadcrumbs.tsx +32 -6
- package/templates/layers/react/typescript/src/components/common/Pagination.tsx +151 -57
- package/templates/layers/react/typescript/src/components/common/SidebarModal.tsx +199 -0
- package/templates/layers/react/typescript/src/components/common/StatsCard.tsx +231 -0
- package/templates/layers/react/typescript/src/components/common/Table.tsx +197 -34
- package/templates/layers/react/typescript/src/components/common/Tabs.tsx +56 -12
- package/templates/layers/react/typescript/src/components/common/ToggleSwitch.tsx +15 -3
- package/templates/layers/react/typescript/src/components/layout/SidebarLayout.tsx +3 -6
- package/templates/layers/react/typescript/src/components/modals/Modal.tsx +151 -0
- package/templates/layers/react/typescript/src/components/ui/Input.tsx +58 -0
- package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +116 -53
- package/templates/layers/react/typescript/src/components/ui/SidebarHeader.tsx +5 -4
- package/templates/layers/react/typescript/src/config/appConfig.ts +3 -3
- package/templates/layers/react/typescript/src/hooks/useWindowSize.ts +2 -3
- package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +2 -1
- package/templates/layers/react-native/base/src/config/appConfig.ts +44 -0
- package/templates/presets/electron/preset.json +9 -0
- package/templates/presets/electron-ts/preset.json +9 -0
- package/templates/layers/react/javascript/src/components/ui/sidebar.jsx +0 -169
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
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
|
|
10
|
+
if (!subscription) {
|
|
11
11
|
return next({ status: 400, message: "Invalid subscription payload" });
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const isExpo = !!subscription.token;
|
|
15
|
+
const isWeb = !!(subscription.endpoint && subscription.keys);
|
|
15
16
|
|
|
16
|
-
if (
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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("
|
|
51
|
-
err.status =
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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": "^
|
|
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": "^
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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?
|
|
39
|
-
|
|
40
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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 (
|
|
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;
|
|
8
|
-
|
|
9
|
-
if (!subscription
|
|
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
|
-
|
|
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 (
|
|
16
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
userId
|
|
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/
|
|
4
|
+
import { prisma } from "../config/prisma.js";
|
|
4
5
|
|
|
5
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
}
|