@imksh/create 1.0.28 → 1.0.30
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/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 +2 -1
- 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/react-native/base/src/components/ui/BiometricLock.tsx +2 -1
- package/templates/layers/react-native/base/src/config/appConfig.ts +44 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"generate:vapid": "node ./src/utils/generateVapidKeys.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@prisma/client": "^
|
|
15
|
+
"@prisma/client": "^7.0.0",
|
|
16
16
|
"bcrypt": "^5.1.1",
|
|
17
17
|
"compression": "^1.7.4",
|
|
18
18
|
"cookie-parser": "^1.4.6",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"cloudinary": "^2.0.0",
|
|
28
28
|
"nodemailer": "^6.9.7",
|
|
29
29
|
"socket.io": "^4.7.2",
|
|
30
|
-
"web-push": "^3.6.6"
|
|
30
|
+
"web-push": "^3.6.6",
|
|
31
|
+
"pg": "^8.11.3",
|
|
32
|
+
"@prisma/adapter-pg": "^7.0.0",
|
|
33
|
+
"expo-server-sdk": "^3.10.0"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@types/bcrypt": "^5.0.2",
|
|
@@ -41,8 +44,9 @@
|
|
|
41
44
|
"@types/nodemailer": "^6.4.14",
|
|
42
45
|
"@types/web-push": "^3.6.3",
|
|
43
46
|
"@types/node": "^20.9.0",
|
|
44
|
-
"prisma": "^
|
|
47
|
+
"prisma": "^7.0.0",
|
|
45
48
|
"tsx": "^4.23.1",
|
|
46
|
-
"typescript": "^5.2.2"
|
|
49
|
+
"typescript": "^5.2.2",
|
|
50
|
+
"@types/pg": "^8.11.0"
|
|
47
51
|
}
|
|
48
52
|
}
|
|
@@ -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,35 +1,74 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
import { prisma } from "../config/
|
|
2
|
+
import { prisma } from "../config/prisma.js";
|
|
3
3
|
import logger from "../utils/logger.js";
|
|
4
4
|
|
|
5
|
-
export const subscribe = async (
|
|
5
|
+
export const subscribe = async (
|
|
6
|
+
req: Request,
|
|
7
|
+
res: Response,
|
|
8
|
+
next: NextFunction,
|
|
9
|
+
) => {
|
|
6
10
|
try {
|
|
7
11
|
const subscription = req.body;
|
|
8
|
-
const userId = req.user?._id || req.user?.id;
|
|
9
|
-
|
|
10
|
-
if (!subscription
|
|
12
|
+
const userId = req.user?._id || req.user?.id;
|
|
13
|
+
|
|
14
|
+
if (!subscription) {
|
|
11
15
|
return next({ status: 400, message: "Invalid subscription payload" });
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
// Determine type: EXPO if token is present, else WEB
|
|
19
|
+
const isExpo = !!subscription.token;
|
|
20
|
+
const isWeb = !!(subscription.endpoint && subscription.keys);
|
|
15
21
|
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
await prisma.pushSubscription.update({ where: { endpoint: subscription.endpoint }, data: { userId } })
|
|
19
|
-
}
|
|
20
|
-
return res.status(200).json({ success: true, message: "Already subscribed." });
|
|
22
|
+
if (!isExpo && !isWeb) {
|
|
23
|
+
return next({ status: 400, message: "Missing token (EXPO) or endpoint/keys (WEB)" });
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
userId
|
|
26
|
+
if (isExpo) {
|
|
27
|
+
const existingSub = await prisma.pushSubscription.findUnique({
|
|
28
|
+
where: { token: subscription.token },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (existingSub) {
|
|
32
|
+
if (userId && existingSub.userId !== userId) {
|
|
33
|
+
await prisma.pushSubscription.update({
|
|
34
|
+
where: { token: subscription.token },
|
|
35
|
+
data: { userId },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return res.status(200).json({ success: true, message: "Already subscribed." });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await prisma.pushSubscription.create({
|
|
42
|
+
data: {
|
|
43
|
+
type: "EXPO",
|
|
44
|
+
token: subscription.token,
|
|
45
|
+
userId: userId || null,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
const existingSub = await prisma.pushSubscription.findUnique({
|
|
50
|
+
where: { endpoint: subscription.endpoint },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (existingSub) {
|
|
54
|
+
if (userId && existingSub.userId !== userId) {
|
|
55
|
+
await prisma.pushSubscription.update({
|
|
56
|
+
where: { endpoint: subscription.endpoint },
|
|
57
|
+
data: { userId },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return res.status(200).json({ success: true, message: "Already subscribed." });
|
|
30
61
|
}
|
|
31
|
-
|
|
32
|
-
|
|
62
|
+
|
|
63
|
+
await prisma.pushSubscription.create({
|
|
64
|
+
data: {
|
|
65
|
+
type: "WEB",
|
|
66
|
+
endpoint: subscription.endpoint,
|
|
67
|
+
keys: subscription.keys,
|
|
68
|
+
userId: userId || null,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
33
72
|
|
|
34
73
|
res.status(201).json({ success: true, message: "Subscription created." });
|
|
35
74
|
} catch (error) {
|
|
@@ -38,14 +77,20 @@ export const subscribe = async (req: Request, res: Response, next: NextFunction)
|
|
|
38
77
|
}
|
|
39
78
|
};
|
|
40
79
|
|
|
41
|
-
export const unsubscribe = async (
|
|
80
|
+
export const unsubscribe = async (
|
|
81
|
+
req: Request,
|
|
82
|
+
res: Response,
|
|
83
|
+
next: NextFunction,
|
|
84
|
+
) => {
|
|
42
85
|
try {
|
|
43
86
|
const { endpoint } = req.body;
|
|
44
87
|
if (!endpoint) {
|
|
45
88
|
return next({ status: 400, message: "Endpoint required" });
|
|
46
89
|
}
|
|
47
90
|
|
|
48
|
-
await prisma.pushSubscription
|
|
91
|
+
await prisma.pushSubscription
|
|
92
|
+
.delete({ where: { endpoint } })
|
|
93
|
+
.catch(() => {});
|
|
49
94
|
|
|
50
95
|
res.status(200).json({ success: true, message: "Unsubscribed." });
|
|
51
96
|
} catch (error) {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
import { verifyToken } from "../utils/jwt.js";
|
|
3
3
|
import logger from "../utils/logger.js";
|
|
4
|
-
import { prisma } from "../config/
|
|
4
|
+
import { prisma } from "../config/prisma.js";
|
|
5
5
|
|
|
6
|
-
export const protect = async (
|
|
6
|
+
export const protect = async (
|
|
7
|
+
req: Request,
|
|
8
|
+
res: Response,
|
|
9
|
+
next: NextFunction,
|
|
10
|
+
) => {
|
|
7
11
|
let token: string | undefined;
|
|
8
12
|
|
|
9
13
|
if (req.cookies?.token) {
|
|
@@ -16,7 +20,9 @@ export const protect = async (req: Request, res: Response, next: NextFunction) =
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
if (!token) {
|
|
19
|
-
return res
|
|
23
|
+
return res
|
|
24
|
+
.status(401)
|
|
25
|
+
.json({ message: "Not authorized, no token provided" });
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
try {
|
|
@@ -30,7 +36,9 @@ export const protect = async (req: Request, res: Response, next: NextFunction) =
|
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
if (!user) {
|
|
33
|
-
return res
|
|
39
|
+
return res
|
|
40
|
+
.status(401)
|
|
41
|
+
.json({ message: "Not authorized, user not found" });
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
const safeUser: any = { ...user };
|
|
@@ -40,6 +48,8 @@ export const protect = async (req: Request, res: Response, next: NextFunction) =
|
|
|
40
48
|
return next();
|
|
41
49
|
} catch (error: any) {
|
|
42
50
|
logger.error("Auth middleware error", error);
|
|
43
|
-
return res
|
|
51
|
+
return res
|
|
52
|
+
.status(401)
|
|
53
|
+
.json({ message: "Not authorized, token verification failed" });
|
|
44
54
|
}
|
|
45
55
|
};
|
|
@@ -1,155 +1,161 @@
|
|
|
1
1
|
import bcrypt from "bcrypt";
|
|
2
|
-
import { prisma } from "../config/
|
|
2
|
+
import { prisma } from "../config/prisma.js";
|
|
3
3
|
import { sendOtpEmail } from "../utils/emailOtp.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
uploadSingleToCloudinary,
|
|
6
|
+
deleteFromCloudinary,
|
|
7
|
+
} from "../utils/cloudinaryUpload.js";
|
|
5
8
|
import logger from "../utils/logger.js";
|
|
6
9
|
export const signup = async ({ name, email, phone, password }: any) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const salt = await bcrypt.genSalt(10);
|
|
17
|
-
const hashedPassword = await bcrypt.hash(password, salt);
|
|
18
|
-
const photoURL = `https://placehold.co/600x400?text=${name.charAt(0).toUpperCase()}`;
|
|
19
|
-
|
|
20
|
-
const newUser = await prisma.user.create({
|
|
21
|
-
data: {
|
|
22
|
-
name,
|
|
23
|
-
email: email.toLowerCase(),
|
|
24
|
-
phone,
|
|
25
|
-
password: hashedPassword,
|
|
26
|
-
avatarUrl: photoURL,
|
|
27
|
-
avatarPublicId: "",
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
return newUser;
|
|
10
|
+
const existingUser = await prisma.user.findUnique({
|
|
11
|
+
where: { email },
|
|
12
|
+
});
|
|
13
|
+
if (existingUser) {
|
|
14
|
+
const err: any = new Error("User already exists");
|
|
15
|
+
err.status = 409;
|
|
16
|
+
throw err;
|
|
32
17
|
}
|
|
33
|
-
export const login = async ({ email, password }: any) => {
|
|
34
|
-
const user = await prisma.user.findUnique({
|
|
35
|
-
where: { email },
|
|
36
|
-
});
|
|
37
|
-
if (!user) {
|
|
38
|
-
const err: any = new Error("User not found");
|
|
39
|
-
err.status = 404;
|
|
40
|
-
throw err;
|
|
41
|
-
}
|
|
42
18
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
19
|
+
const salt = await bcrypt.genSalt(10);
|
|
20
|
+
const hashedPassword = await bcrypt.hash(password, salt);
|
|
21
|
+
const photoURL = `https://placehold.co/600x400?text=${name.charAt(0).toUpperCase()}`;
|
|
22
|
+
|
|
23
|
+
const newUser = await prisma.user.create({
|
|
24
|
+
data: {
|
|
25
|
+
name,
|
|
26
|
+
email: email.toLowerCase(),
|
|
27
|
+
phone,
|
|
28
|
+
password: hashedPassword,
|
|
29
|
+
avatarUrl: photoURL,
|
|
30
|
+
avatarPublicId: "",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return newUser;
|
|
35
|
+
};
|
|
36
|
+
export const login = async ({ email, password }: any) => {
|
|
37
|
+
const user = await prisma.user.findUnique({
|
|
38
|
+
where: { email },
|
|
39
|
+
});
|
|
40
|
+
if (!user) {
|
|
41
|
+
const err: any = new Error("User not found");
|
|
42
|
+
err.status = 404;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
const isValid = await bcrypt.compare(password, user.password);
|
|
47
|
+
if (!isValid) {
|
|
48
|
+
const err: any = new Error("Invalid Credentials");
|
|
49
|
+
err.status = 401;
|
|
50
|
+
throw err;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
return user;
|
|
54
|
+
};
|
|
52
55
|
export const generateOtp = async (email: string) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const user = await prisma.user.findUnique({
|
|
57
|
+
where: { email },
|
|
58
|
+
});
|
|
59
|
+
if (!user) {
|
|
60
|
+
const err: any = new Error("User not found");
|
|
61
|
+
err.status = 404;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
const existingOtp = await prisma.otp.findUnique({
|
|
66
|
+
where: { email },
|
|
67
|
+
});
|
|
68
|
+
if (existingOtp) {
|
|
69
|
+
await prisma.otp.delete({
|
|
63
70
|
where: { email },
|
|
64
71
|
});
|
|
65
|
-
|
|
66
|
-
await prisma.otp.delete({
|
|
67
|
-
where: { email },
|
|
68
|
-
});
|
|
69
|
-
}
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const otp = Math.floor(100000 + Math.random() * 900000).toString();
|
|
75
|
+
const hashedOtp = await bcrypt.hash(otp, 10);
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
await prisma.otp.create({
|
|
78
|
+
data: {
|
|
79
|
+
email,
|
|
80
|
+
otp: hashedOtp,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
await sendOtpEmail(email, otp);
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
84
87
|
export const verifyOtp = async ({ email, otp }: any) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const existingOtp = await prisma.otp.findUnique({
|
|
95
|
-
where: { email },
|
|
96
|
-
});
|
|
97
|
-
if (!existingOtp) {
|
|
98
|
-
const err: any = new Error("OTP match failed");
|
|
99
|
-
err.status = 400;
|
|
100
|
-
throw err;
|
|
101
|
-
}
|
|
88
|
+
const user = await prisma.user.findUnique({
|
|
89
|
+
where: { email },
|
|
90
|
+
});
|
|
91
|
+
if (!user) {
|
|
92
|
+
const err: any = new Error("User not found");
|
|
93
|
+
err.status = 404;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
const existingOtp = await prisma.otp.findUnique({
|
|
98
|
+
where: { email },
|
|
99
|
+
});
|
|
100
|
+
if (!existingOtp) {
|
|
101
|
+
const err: any = new Error("OTP match failed");
|
|
102
|
+
err.status = 400;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
const isMatched = await bcrypt.compare(otp, existingOtp.otp);
|
|
107
|
+
if (!isMatched) {
|
|
108
|
+
const err: any = new Error("OTP match failed");
|
|
109
|
+
err.status = 400;
|
|
110
|
+
throw err;
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
return user;
|
|
114
|
+
};
|
|
112
115
|
export const resetPassword = async (user: any, newPassword: any) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
117
|
+
await prisma.user.update({
|
|
118
|
+
where: { id: user.id },
|
|
119
|
+
data: { password: hashedPassword },
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
export const updateProfile = async (
|
|
124
|
+
userId: any,
|
|
125
|
+
{ name, phone, file }: any,
|
|
126
|
+
) => {
|
|
127
|
+
const user = await prisma.user.findUnique({
|
|
128
|
+
where: { id: userId },
|
|
129
|
+
});
|
|
130
|
+
if (!user) {
|
|
131
|
+
const err: any = new Error("User not found");
|
|
132
|
+
err.status = 404;
|
|
133
|
+
throw err;
|
|
119
134
|
}
|
|
120
|
-
export const updateProfile = async (userId: any, { name, phone, file }: any) => {
|
|
121
|
-
const user = await prisma.user.findUnique({
|
|
122
|
-
where: { id: userId },
|
|
123
|
-
});
|
|
124
|
-
if (!user) {
|
|
125
|
-
const err: any = new Error("User not found");
|
|
126
|
-
err.status = 404;
|
|
127
|
-
throw err;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const updateData: any = {};
|
|
131
|
-
if (name) updateData.name = name;
|
|
132
|
-
if (phone) updateData.phone = phone;
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
136
|
+
const updateData: any = {};
|
|
137
|
+
if (name) updateData.name = name;
|
|
138
|
+
if (phone) updateData.phone = phone;
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
140
|
+
if (file) {
|
|
141
|
+
const uploadResult = await uploadSingleToCloudinary(file, {
|
|
142
|
+
folder: "codelab/avatars",
|
|
143
|
+
});
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
if (user.avatarPublicId) {
|
|
146
|
+
await deleteFromCloudinary(user.avatarPublicId).catch((err) =>
|
|
147
|
+
logger.error("Old avatar delete error", err),
|
|
148
|
+
);
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
data: updateData,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
return updatedUser;
|
|
151
|
+
updateData.avatarUrl = uploadResult.url;
|
|
152
|
+
updateData.avatarPublicId = uploadResult.publicId;
|
|
155
153
|
}
|
|
154
|
+
|
|
155
|
+
const updatedUser = await prisma.user.update({
|
|
156
|
+
where: { id: userId },
|
|
157
|
+
data: updateData,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return updatedUser;
|
|
161
|
+
};
|
|
@@ -1,21 +1,76 @@
|
|
|
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
|
-
import { prisma } from "../config/
|
|
4
|
+
import { prisma } from "../config/prisma.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
|
-
|
|
18
|
-
|
|
22
|
+
const subscriptions = await prisma.pushSubscription.findMany({
|
|
23
|
+
where: { userId },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (subscriptions.length === 0) return;
|
|
27
|
+
|
|
28
|
+
const webSubs = subscriptions.filter(s => s.type === "WEB");
|
|
29
|
+
const expoSubs = subscriptions.filter(s => s.type === "EXPO");
|
|
30
|
+
|
|
31
|
+
// Web Push
|
|
32
|
+
if (webSubs.length > 0) {
|
|
33
|
+
const payload = JSON.stringify({ title, body, url });
|
|
34
|
+
await Promise.allSettled(
|
|
35
|
+
webSubs.map(async (sub) => {
|
|
36
|
+
if (!sub.endpoint || !sub.keys) return;
|
|
37
|
+
const pushSub = { endpoint: sub.endpoint, keys: sub.keys as any };
|
|
38
|
+
try {
|
|
39
|
+
await webpush.sendNotification(pushSub, payload);
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
if (err.statusCode === 404 || err.statusCode === 410) {
|
|
42
|
+
await prisma.pushSubscription.delete({ where: { id: sub.id } }).catch(() => {});
|
|
43
|
+
} else {
|
|
44
|
+
logger.error("Web Push Error", err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Expo Push
|
|
52
|
+
if (expoSubs.length > 0) {
|
|
53
|
+
const messages: ExpoPushMessage[] = [];
|
|
54
|
+
for (const sub of expoSubs) {
|
|
55
|
+
if (!sub.token || !Expo.isExpoPushToken(sub.token)) continue;
|
|
56
|
+
messages.push({
|
|
57
|
+
to: sub.token,
|
|
58
|
+
sound: "default",
|
|
59
|
+
title,
|
|
60
|
+
body,
|
|
61
|
+
data: { url },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const chunks = expo.chunkPushNotifications(messages);
|
|
66
|
+
for (const chunk of chunks) {
|
|
67
|
+
try {
|
|
68
|
+
await expo.sendPushNotificationsAsync(chunk);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error("Error sending push notification chunk", error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
19
74
|
} catch (error) {
|
|
20
75
|
logger.error("Error sending push notification to user", error);
|
|
21
76
|
}
|
|
@@ -7,6 +7,7 @@ import { Colors } from "@/constants/Colors";
|
|
|
7
7
|
import { useColorScheme } from "nativewind";
|
|
8
8
|
import { Txt } from "./common/Typography";
|
|
9
9
|
import Button from "./ui/buttons/Button";
|
|
10
|
+
import appConfig from "@/config/appConfig";
|
|
10
11
|
|
|
11
12
|
export default function BiometricLock({ children }: { children: React.ReactNode }) {
|
|
12
13
|
const { isBiometricEnabled, isAppLocked, setIsAppLocked, lockDuration } = useAuthStore();
|
|
@@ -80,7 +81,7 @@ export default function BiometricLock({ children }: { children: React.ReactNode
|
|
|
80
81
|
</View>
|
|
81
82
|
<Txt variant="3xl" className="font-extrabold tracking-tight mb-2">App Locked</Txt>
|
|
82
83
|
<Txt variant="base" className="opacity-70 text-center px-8">
|
|
83
|
-
Please authenticate to continue using
|
|
84
|
+
Please authenticate to continue using {appConfig.app.name}
|
|
84
85
|
</Txt>
|
|
85
86
|
</View>
|
|
86
87
|
|