startx 0.0.1
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/.editorconfig +20 -0
- package/.prettierignore +24 -0
- package/.prettierrc.js +52 -0
- package/.vscode/settings.json +3 -0
- package/LICENSE +21 -0
- package/apps/core-server/.env.example +24 -0
- package/apps/core-server/Dockerfile +61 -0
- package/apps/core-server/eslint.config.mjs +47 -0
- package/apps/core-server/package.json +73 -0
- package/apps/core-server/src/config/custom-type.ts +54 -0
- package/apps/core-server/src/events/index.ts +37 -0
- package/apps/core-server/src/index.ts +19 -0
- package/apps/core-server/src/middlewares/auth-middleware.ts +50 -0
- package/apps/core-server/src/middlewares/cors-middleware.ts +6 -0
- package/apps/core-server/src/middlewares/error-middleware.ts +23 -0
- package/apps/core-server/src/middlewares/logger-middleware.ts +21 -0
- package/apps/core-server/src/middlewares/notfound-middleware.ts +14 -0
- package/apps/core-server/src/middlewares/serve-static.ts +24 -0
- package/apps/core-server/src/routes/files/router.ts +7 -0
- package/apps/core-server/src/routes/server.ts +36 -0
- package/apps/core-server/tsconfig.json +10 -0
- package/apps/core-server/tsdown.config.ts +14 -0
- package/biome.json +62 -0
- package/configs/eslint-config/package.json +60 -0
- package/configs/eslint-config/plugins.d.ts +1 -0
- package/configs/eslint-config/src/configs/base.ts +237 -0
- package/configs/eslint-config/src/configs/frontend.ts +62 -0
- package/configs/eslint-config/src/configs/node.ts +10 -0
- package/configs/eslint-config/src/plugin.ts +25 -0
- package/configs/eslint-config/src/rules/index.ts +30 -0
- package/configs/eslint-config/src/rules/no-argument-spread.test.ts +47 -0
- package/configs/eslint-config/src/rules/no-argument-spread.ts +96 -0
- package/configs/eslint-config/src/rules/no-dynamic-import-template.ts +32 -0
- package/configs/eslint-config/src/rules/no-internal-package-import.ts +40 -0
- package/configs/eslint-config/src/rules/no-interpolation-in-regular-string.ts +32 -0
- package/configs/eslint-config/src/rules/no-json-parse-json-stringify.test.ts +34 -0
- package/configs/eslint-config/src/rules/no-json-parse-json-stringify.ts +49 -0
- package/configs/eslint-config/src/rules/no-plain-errors.ts +50 -0
- package/configs/eslint-config/src/rules/no-skipped-tests.ts +61 -0
- package/configs/eslint-config/src/rules/no-top-level-relative-imports-in-backend-module.ts +27 -0
- package/configs/eslint-config/src/rules/no-type-unsafe-event-emitter.ts +33 -0
- package/configs/eslint-config/src/rules/no-uncaught-json-parse.test.ts +21 -0
- package/configs/eslint-config/src/rules/no-uncaught-json-parse.ts +45 -0
- package/configs/eslint-config/src/rules/no-untyped-config-class-field.ts +26 -0
- package/configs/eslint-config/src/rules/no-unused-param-catch-clause.ts +33 -0
- package/configs/eslint-config/src/rules/no-useless-catch-throw.test.ts +34 -0
- package/configs/eslint-config/src/rules/no-useless-catch-throw.ts +47 -0
- package/configs/eslint-config/src/utils/json.ts +21 -0
- package/configs/eslint-config/tsconfig.json +8 -0
- package/configs/eslint-config/tsdown.config.ts +11 -0
- package/configs/eslint-config/vitest.config.ts +3 -0
- package/configs/tsdown-config/package.json +14 -0
- package/configs/tsdown-config/src/config/tsdown.base.ts +13 -0
- package/configs/typescript-config/package.json +10 -0
- package/configs/typescript-config/tsconfig.common.json +32 -0
- package/configs/typescript-config/tsconfig.frontend.json +14 -0
- package/configs/typescript-config/tsconfig.node.json +9 -0
- package/configs/vitest-config/package.json +25 -0
- package/configs/vitest-config/src/base.ts +34 -0
- package/configs/vitest-config/src/frontend.ts +15 -0
- package/configs/vitest-config/src/node.ts +5 -0
- package/configs/vitest-config/tsconfig.json +7 -0
- package/package.json +47 -0
- package/packages/@repo/constants/eslint.config.mjs +21 -0
- package/packages/@repo/constants/package.json +19 -0
- package/packages/@repo/constants/src/api.ts +1 -0
- package/packages/@repo/constants/src/index.ts +8 -0
- package/packages/@repo/constants/src/time.ts +23 -0
- package/packages/@repo/constants/tsconfig.json +7 -0
- package/packages/@repo/db/eslint.config.mjs +21 -0
- package/packages/@repo/db/package.json +30 -0
- package/packages/@repo/db/src/functions.ts +122 -0
- package/packages/@repo/db/src/index.ts +20 -0
- package/packages/@repo/db/src/schema/common.ts +49 -0
- package/packages/@repo/db/src/schema/index.ts +1 -0
- package/packages/@repo/db/tsconfig.json +13 -0
- package/packages/@repo/lib/eslint.config.mjs +49 -0
- package/packages/@repo/lib/package.json +57 -0
- package/packages/@repo/lib/src/bucket-module/file-storage.ts +49 -0
- package/packages/@repo/lib/src/bucket-module/s3-storage.ts +114 -0
- package/packages/@repo/lib/src/bucket-module/utils.ts +11 -0
- package/packages/@repo/lib/src/command-module.ts +77 -0
- package/packages/@repo/lib/src/constants.ts +3 -0
- package/packages/@repo/lib/src/cookie-module.ts +42 -0
- package/packages/@repo/lib/src/custom-type.ts +54 -0
- package/packages/@repo/lib/src/env.ts +13 -0
- package/packages/@repo/lib/src/error-handlers-module/index.ts +11 -0
- package/packages/@repo/lib/src/file-system/index.ts +90 -0
- package/packages/@repo/lib/src/hashing-module.ts +9 -0
- package/packages/@repo/lib/src/index.ts +27 -0
- package/packages/@repo/lib/src/logger-module/log-config.ts +16 -0
- package/packages/@repo/lib/src/logger-module/logger.ts +78 -0
- package/packages/@repo/lib/src/logger-module/memory-profiler.ts +65 -0
- package/packages/@repo/lib/src/mail-module/api.ts +0 -0
- package/packages/@repo/lib/src/mail-module/mock.ts +8 -0
- package/packages/@repo/lib/src/mail-module/nodemailer.ts +45 -0
- package/packages/@repo/lib/src/notification-module/index.ts +172 -0
- package/packages/@repo/lib/src/notification-module/push-notification.ts +90 -0
- package/packages/@repo/lib/src/oauth2-client.ts +109 -0
- package/packages/@repo/lib/src/otp-module.ts +98 -0
- package/packages/@repo/lib/src/pagination-module.ts +49 -0
- package/packages/@repo/lib/src/token-module.ts +35 -0
- package/packages/@repo/lib/src/user-session.ts +117 -0
- package/packages/@repo/lib/src/utils.ts +42 -0
- package/packages/@repo/lib/src/validation-module.ts +187 -0
- package/packages/@repo/lib/tsconfig.json +7 -0
- package/packages/@repo/mail/package.json +29 -0
- package/packages/@repo/mail/src/emails/admin/OtpEmail.tsx +168 -0
- package/packages/@repo/mail/src/index.ts +13 -0
- package/packages/@repo/mail/tsconfig.build.json +14 -0
- package/packages/@repo/mail/tsconfig.json +13 -0
- package/packages/@repo/mail/tsdown.config.ts +9 -0
- package/packages/@repo/redis/eslint.config.mjs +8 -0
- package/packages/@repo/redis/package.json +31 -0
- package/packages/@repo/redis/src/index.ts +2 -0
- package/packages/@repo/redis/src/lib/redis-client.ts +23 -0
- package/packages/@repo/redis/src/lib/redis-module.ts +3 -0
- package/packages/@repo/redis/tsconfig.json +12 -0
- package/packages/ui/components.json +17 -0
- package/packages/ui/eslint.config.mjs +18 -0
- package/packages/ui/package.json +67 -0
- package/packages/ui/postcss.config.mjs +9 -0
- package/packages/ui/src/components/custom/form-wrapper.tsx +551 -0
- package/packages/ui/src/components/custom/grid-component.tsx +23 -0
- package/packages/ui/src/components/custom/hover-tool.tsx +38 -0
- package/packages/ui/src/components/custom/image-picker.tsx +109 -0
- package/packages/ui/src/components/custom/no-content.tsx +37 -0
- package/packages/ui/src/components/custom/page-container.tsx +24 -0
- package/packages/ui/src/components/custom/page-section.tsx +59 -0
- package/packages/ui/src/components/custom/simple-popover.tsx +29 -0
- package/packages/ui/src/components/custom/switch-component.tsx +20 -0
- package/packages/ui/src/components/custom/theme-provider.tsx +74 -0
- package/packages/ui/src/components/custom/typography.tsx +111 -0
- package/packages/ui/src/components/extensions/carousel.tsx +392 -0
- package/packages/ui/src/components/hooks/event/use-click.tsx +39 -0
- package/packages/ui/src/components/hooks/time/useDebounce.tsx +21 -0
- package/packages/ui/src/components/hooks/time/useInterval.tsx +35 -0
- package/packages/ui/src/components/hooks/time/useTimeout.tsx +19 -0
- package/packages/ui/src/components/hooks/time/useTimer.tsx +51 -0
- package/packages/ui/src/components/hooks/use-media-query.tsx +19 -0
- package/packages/ui/src/components/hooks/use-persistent-storage.tsx +52 -0
- package/packages/ui/src/components/hooks/use-update-effect.tsx +13 -0
- package/packages/ui/src/components/hooks/use-window-dimension.tsx +30 -0
- package/packages/ui/src/components/lib/utils.ts +242 -0
- package/packages/ui/src/components/lucide.tsx +3 -0
- package/packages/ui/src/components/sonner.tsx +1 -0
- package/packages/ui/src/components/ui/alert-dialog.tsx +116 -0
- package/packages/ui/src/components/ui/avatar.tsx +53 -0
- package/packages/ui/src/components/ui/badge.tsx +46 -0
- package/packages/ui/src/components/ui/breadcrumb.tsx +109 -0
- package/packages/ui/src/components/ui/button.tsx +96 -0
- package/packages/ui/src/components/ui/card.tsx +92 -0
- package/packages/ui/src/components/ui/carousel.tsx +243 -0
- package/packages/ui/src/components/ui/checkbox.tsx +32 -0
- package/packages/ui/src/components/ui/command.tsx +155 -0
- package/packages/ui/src/components/ui/dialog.tsx +127 -0
- package/packages/ui/src/components/ui/dropdown-menu.tsx +226 -0
- package/packages/ui/src/components/ui/form.tsx +165 -0
- package/packages/ui/src/components/ui/input-otp.tsx +76 -0
- package/packages/ui/src/components/ui/input.tsx +21 -0
- package/packages/ui/src/components/ui/label.tsx +24 -0
- package/packages/ui/src/components/ui/multiple-select.tsx +510 -0
- package/packages/ui/src/components/ui/popover.tsx +42 -0
- package/packages/ui/src/components/ui/select.tsx +170 -0
- package/packages/ui/src/components/ui/separator.tsx +28 -0
- package/packages/ui/src/components/ui/sheet.tsx +130 -0
- package/packages/ui/src/components/ui/skeleton.tsx +13 -0
- package/packages/ui/src/components/ui/spinner.tsx +16 -0
- package/packages/ui/src/components/ui/switch.tsx +28 -0
- package/packages/ui/src/components/ui/table.tsx +116 -0
- package/packages/ui/src/components/ui/tabs.tsx +54 -0
- package/packages/ui/src/components/ui/textarea.tsx +18 -0
- package/packages/ui/src/components/ui/timeline.tsx +118 -0
- package/packages/ui/src/components/ui/tooltip.tsx +30 -0
- package/packages/ui/src/components/util/n-formattor.ts +22 -0
- package/packages/ui/src/components/util/storage.ts +37 -0
- package/packages/ui/src/globals.css +87 -0
- package/packages/ui/tailwind.config.ts +94 -0
- package/packages/ui/tsconfig.json +12 -0
- package/pnpm-workspace.yaml +43 -0
- package/turbo.json +77 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// import db from "@repo/db";
|
|
2
|
+
// import {
|
|
3
|
+
// notificationsTable,
|
|
4
|
+
// notificationTypeEnum,
|
|
5
|
+
// userDetails,
|
|
6
|
+
// } from "@repo/db/schema";
|
|
7
|
+
// import { desc, eq, inArray, sql } from "drizzle-orm";
|
|
8
|
+
// import { FcmPayload, PushNotificationManager } from "./push-notification.js";
|
|
9
|
+
// import logger from "../logger-module/logger.js";
|
|
10
|
+
|
|
11
|
+
// export type CreateNotificationPayload = {
|
|
12
|
+
// title: string;
|
|
13
|
+
// inAppTitle?: string;
|
|
14
|
+
// userId: string;
|
|
15
|
+
// description?: string;
|
|
16
|
+
// type: (typeof notificationTypeEnum.enumValues)[number][];
|
|
17
|
+
// url?: string;
|
|
18
|
+
// image?: string;
|
|
19
|
+
// data: any;
|
|
20
|
+
// };
|
|
21
|
+
|
|
22
|
+
// export class NotificationModule {
|
|
23
|
+
// /**
|
|
24
|
+
// * Updates the user's FCM token
|
|
25
|
+
// * @param userID the id of the user
|
|
26
|
+
// * @param fcmToken the FCM token to be updated
|
|
27
|
+
// * @throws if the update fails
|
|
28
|
+
// */
|
|
29
|
+
// static async uploadFcmToken(userID: string, fcmToken: string) {
|
|
30
|
+
// try {
|
|
31
|
+
// await db
|
|
32
|
+
// .update(userDetails)
|
|
33
|
+
// .set({ fcmToken })
|
|
34
|
+
// .where(eq(userDetails.userId, userID));
|
|
35
|
+
// } catch (error) {
|
|
36
|
+
// throw error;
|
|
37
|
+
// }
|
|
38
|
+
// }
|
|
39
|
+
// /**
|
|
40
|
+
// * Mark the given notification IDs as read
|
|
41
|
+
// * @param notificationIds the IDs of the notifications to mark as read
|
|
42
|
+
// * @throws if the update fails
|
|
43
|
+
// */
|
|
44
|
+
// static async markAsRead(notificationIds: string[]) {
|
|
45
|
+
// try {
|
|
46
|
+
// await db
|
|
47
|
+
// .update(notificationsTable)
|
|
48
|
+
// .set({ read: true })
|
|
49
|
+
// .where(inArray(notificationsTable.id, notificationIds));
|
|
50
|
+
// } catch (error) {
|
|
51
|
+
// throw error;
|
|
52
|
+
// }
|
|
53
|
+
// }
|
|
54
|
+
// /**
|
|
55
|
+
// * Send a notification to a user
|
|
56
|
+
// * @param payload the notification payload
|
|
57
|
+
// * @throws if either the in-app notification or the push notification fails
|
|
58
|
+
// */
|
|
59
|
+
// static async sendNotification(
|
|
60
|
+
// payload: CreateNotificationPayload,
|
|
61
|
+
// options: { push?: boolean; inApp?: boolean } = { push: true, inApp: true }
|
|
62
|
+
// ) {
|
|
63
|
+
// try {
|
|
64
|
+
// if (options.inApp)
|
|
65
|
+
// await NotificationModule.sendInAppNotification(payload);
|
|
66
|
+
// if (options.push) await NotificationModule.sendPushNotification(payload);
|
|
67
|
+
// } catch (error) {
|
|
68
|
+
// throw error;
|
|
69
|
+
// }
|
|
70
|
+
// }
|
|
71
|
+
// /**
|
|
72
|
+
// * Sends an in-app notification by inserting a new record into the notifications table.
|
|
73
|
+
// *
|
|
74
|
+
// * @param payload - The notification payload containing details such as title, userId, description, type, url, image, and data.
|
|
75
|
+
// * @throws if the database insert operation fails
|
|
76
|
+
// */
|
|
77
|
+
|
|
78
|
+
// static async sendInAppNotification(payload: CreateNotificationPayload) {
|
|
79
|
+
// await db.insert(notificationsTable).values({
|
|
80
|
+
// title: payload.inAppTitle || payload.title,
|
|
81
|
+
// userId: payload.userId,
|
|
82
|
+
// // description: payload.description,
|
|
83
|
+
// type: payload.type,
|
|
84
|
+
// url: payload.url,
|
|
85
|
+
// image: payload.image,
|
|
86
|
+
// data: { ...payload.data, url: payload.url, type: payload.type },
|
|
87
|
+
// });
|
|
88
|
+
// }
|
|
89
|
+
|
|
90
|
+
// /**
|
|
91
|
+
// * Sends a push notification using the FCM token associated with the given user ID.
|
|
92
|
+
// *
|
|
93
|
+
// * @param payload - The notification payload containing details such as title, userId, description, type, url, image, and data.
|
|
94
|
+
// * @throws if the database query for the FCM token fails or if the FCM token is not found
|
|
95
|
+
// */
|
|
96
|
+
// static async sendPushNotification(payload: CreateNotificationPayload) {
|
|
97
|
+
// const [user] = await db
|
|
98
|
+
// .select({
|
|
99
|
+
// fcmToken: userDetails.fcmToken,
|
|
100
|
+
// allowNotifications: userDetails.notifications,
|
|
101
|
+
// })
|
|
102
|
+
// .from(userDetails)
|
|
103
|
+
// .where(eq(userDetails.userId, payload.userId));
|
|
104
|
+
// if (!user?.fcmToken) {
|
|
105
|
+
// logger.error("No FCM token found");
|
|
106
|
+
// return;
|
|
107
|
+
// }
|
|
108
|
+
// if (!user.allowNotifications) {
|
|
109
|
+
// logger.info("User does not allow notifications");
|
|
110
|
+
// return;
|
|
111
|
+
// }
|
|
112
|
+
// PushNotificationManager.sendNotification([
|
|
113
|
+
// {
|
|
114
|
+
// notification: {
|
|
115
|
+
// body: payload.description,
|
|
116
|
+
// title: removeCurlyBraces(payload.title),
|
|
117
|
+
// },
|
|
118
|
+
// token: user.fcmToken,
|
|
119
|
+
// data: {
|
|
120
|
+
// ...payload.data,
|
|
121
|
+
// url: payload.url,
|
|
122
|
+
// type: JSON.stringify(payload.type),
|
|
123
|
+
// },
|
|
124
|
+
// },
|
|
125
|
+
// ]);
|
|
126
|
+
// }
|
|
127
|
+
|
|
128
|
+
// static async sendBulkNotification(
|
|
129
|
+
// userIds: string[],
|
|
130
|
+
// payload: Omit<CreateNotificationPayload, "userId">
|
|
131
|
+
// ) {
|
|
132
|
+
// await db.insert(notificationsTable).values(
|
|
133
|
+
// userIds.map((userId) => ({
|
|
134
|
+
// title: payload.title,
|
|
135
|
+
// userId: userId,
|
|
136
|
+
// description: payload.description,
|
|
137
|
+
// type: payload.type,
|
|
138
|
+
// url: payload.url,
|
|
139
|
+
// image: payload.image,
|
|
140
|
+
// data: { ...payload.data, url: payload.url, type: payload.type },
|
|
141
|
+
// }))
|
|
142
|
+
// );
|
|
143
|
+
// const users = await db
|
|
144
|
+
// .select({
|
|
145
|
+
// fcmToken: userDetails.fcmToken,
|
|
146
|
+
// allowNotifications: userDetails.notifications,
|
|
147
|
+
// })
|
|
148
|
+
// .from(userDetails)
|
|
149
|
+
// .where(inArray(userDetails.userId, userIds));
|
|
150
|
+
// await PushNotificationManager.sendNotification(
|
|
151
|
+
// users
|
|
152
|
+
// .filter((u) => u.fcmToken && u.allowNotifications)
|
|
153
|
+
// .map((u) => ({
|
|
154
|
+
// notification: {
|
|
155
|
+
// body: payload.description,
|
|
156
|
+
// title: removeCurlyBraces(payload.title),
|
|
157
|
+
// },
|
|
158
|
+
// token: u.fcmToken!,
|
|
159
|
+
// data: {
|
|
160
|
+
// ...payload.data,
|
|
161
|
+
// url: payload.url,
|
|
162
|
+
// type: JSON.stringify(payload.type),
|
|
163
|
+
// },
|
|
164
|
+
// }))
|
|
165
|
+
// );
|
|
166
|
+
// }
|
|
167
|
+
// }
|
|
168
|
+
|
|
169
|
+
// function removeCurlyBraces(text: string) {
|
|
170
|
+
// // The regular expression captures the text between '{{' and '}}'
|
|
171
|
+
// return text.replace(/\{\{(.*?)\}\}/g, "$1");
|
|
172
|
+
// }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import firebaseAdmin from "firebase-admin";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { logger } from "../logger-module/logger.js";
|
|
5
|
+
import { __dirname } from "../utils.js";
|
|
6
|
+
|
|
7
|
+
export type FcmPayload = {
|
|
8
|
+
notification?: {
|
|
9
|
+
title: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
};
|
|
12
|
+
data?: Record<string, any>;
|
|
13
|
+
token: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type InitOptions =
|
|
17
|
+
| {
|
|
18
|
+
type: "file";
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: "env";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class PushNotificationManager {
|
|
25
|
+
private static initialized = false;
|
|
26
|
+
|
|
27
|
+
static initialize(options: InitOptions) {
|
|
28
|
+
if (this.initialized) return;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
let credential: firebaseAdmin.credential.Credential;
|
|
32
|
+
|
|
33
|
+
if (options.type === "file") {
|
|
34
|
+
credential = firebaseAdmin.credential.cert(
|
|
35
|
+
path.join(__dirname(), "/keys/firebase-messaging-secret.json"),
|
|
36
|
+
);
|
|
37
|
+
} else if (options.type === "env") {
|
|
38
|
+
credential = firebaseAdmin.credential.cert({
|
|
39
|
+
projectId: process.env.FIREBASE_PROJECT_ID,
|
|
40
|
+
clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
|
|
41
|
+
privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, "\n"),
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error("Invalid initialization options");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
firebaseAdmin.initializeApp({ credential });
|
|
48
|
+
this.initialized = true;
|
|
49
|
+
logger.info("Firebase Admin initialized");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error("Firebase initialization error:", error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static async sendNotification(payload: FcmPayload[]) {
|
|
56
|
+
if (!this.initialized) {
|
|
57
|
+
logger.warn("PushNotificationManager not initialized. Skipping notification.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const formattedPayload = payload.map((item) => {
|
|
62
|
+
const data: Record<string, string> = {};
|
|
63
|
+
|
|
64
|
+
for (const key in item.data) {
|
|
65
|
+
const value = item.data[key] as string;
|
|
66
|
+
if (value !== null) {
|
|
67
|
+
data[key] = typeof value === "string" ? value : String(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
notification: item.notification
|
|
73
|
+
? {
|
|
74
|
+
title: item.notification.title,
|
|
75
|
+
body: item.notification.body,
|
|
76
|
+
}
|
|
77
|
+
: undefined,
|
|
78
|
+
data,
|
|
79
|
+
token: item.token,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await firebaseAdmin.messaging().sendEach(formattedPayload);
|
|
85
|
+
logger.info("Successfully sent message:", response.responses);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error("Error sending message:", error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
import { logger } from "./logger-module/logger";
|
|
4
|
+
|
|
5
|
+
type AuthorizationURLParams = {
|
|
6
|
+
authorizationURL: string;
|
|
7
|
+
clientID: string;
|
|
8
|
+
redirectURI: string;
|
|
9
|
+
scopes: string[];
|
|
10
|
+
state: string;
|
|
11
|
+
aud?: string;
|
|
12
|
+
code_challenge?: string;
|
|
13
|
+
code_challenge_method?: string;
|
|
14
|
+
offline_access?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type AccessTokenParams = {
|
|
18
|
+
tokenURL: string;
|
|
19
|
+
clientID: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
redirectURI: string;
|
|
22
|
+
code: string;
|
|
23
|
+
code_verifier?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ProfileRouteParams = {
|
|
27
|
+
profileRoute: string;
|
|
28
|
+
accessToken: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class OauthClient {
|
|
32
|
+
static getAuthorizationUrl(params: AuthorizationURLParams) {
|
|
33
|
+
// constructing authorization url
|
|
34
|
+
const url = new URL(params.authorizationURL);
|
|
35
|
+
// adding our client id
|
|
36
|
+
url.searchParams.append("client_id", params.clientID);
|
|
37
|
+
// adding the endpoint to redirect after user gives consent
|
|
38
|
+
url.searchParams.append("redirect_uri", params.redirectURI);
|
|
39
|
+
// choosing the response type (code here which we'll use to redeem an accessToken)
|
|
40
|
+
url.searchParams.append("response_type", "code");
|
|
41
|
+
// required for twitter, just a random string (will be needed when redeeming accessToken from code)
|
|
42
|
+
if (params.code_challenge) {
|
|
43
|
+
url.searchParams.append("code_challenge", params.code_challenge);
|
|
44
|
+
}
|
|
45
|
+
if (params.code_challenge_method) {
|
|
46
|
+
url.searchParams.append("code_challenge_method", params.code_challenge_method);
|
|
47
|
+
}
|
|
48
|
+
if (params.offline_access) {
|
|
49
|
+
url.searchParams.append("access_type", "offline");
|
|
50
|
+
url.searchParams.append("prompt", "consent");
|
|
51
|
+
}
|
|
52
|
+
// adding scopes which specify what are we asking from user
|
|
53
|
+
if (params.scopes) {
|
|
54
|
+
url.searchParams.append("scope", params.scopes.join(" "));
|
|
55
|
+
}
|
|
56
|
+
// just a random value on state
|
|
57
|
+
url.searchParams.append("state", params.state);
|
|
58
|
+
|
|
59
|
+
// not important, sometimes necessary, takes baseUrl
|
|
60
|
+
if (params.aud) {
|
|
61
|
+
url.searchParams.append("aud", params.aud);
|
|
62
|
+
}
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static async getAccessToken(
|
|
67
|
+
params: AccessTokenParams,
|
|
68
|
+
options?: { authInBody?: boolean },
|
|
69
|
+
) {
|
|
70
|
+
try {
|
|
71
|
+
const body = new URLSearchParams();
|
|
72
|
+
body.append("grant_type", "authorization_code");
|
|
73
|
+
body.append("code", params.code);
|
|
74
|
+
body.append("redirect_uri", params.redirectURI);
|
|
75
|
+
if (params.code_verifier) {
|
|
76
|
+
body.append("code_verifier", params.code_verifier);
|
|
77
|
+
}
|
|
78
|
+
if (options?.authInBody) {
|
|
79
|
+
body.append("client_id", params.clientID);
|
|
80
|
+
body.append("client_secret", params.clientSecret);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { data } = await axios.post(params.tokenURL, body, {
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
86
|
+
},
|
|
87
|
+
auth: {
|
|
88
|
+
username: params.clientID,
|
|
89
|
+
password: params.clientSecret,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return data as Record<"access_token" | "refresh_token" | "scope" | "id_token", string>;
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
if (error.response?.data) {
|
|
95
|
+
logger.error(Error(JSON.stringify(error.response.data, null, 2)));
|
|
96
|
+
} else logger.error(error);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//
|
|
101
|
+
static async getUserInfo(params: ProfileRouteParams) {
|
|
102
|
+
const { data } = await axios.get(params.profileRoute, {
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { db, otps } from "@repo/db";
|
|
2
|
+
import { AdminEmailTemplate } from "@repo/email";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { HashingModule } from "./hashing-module.js";
|
|
6
|
+
import { SMTPMailService } from "./mail-module/nodemailer.js";
|
|
7
|
+
import { Random } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @description Handles operations related to One Time Passwords (OTPs).
|
|
11
|
+
*/
|
|
12
|
+
export class OTPModule {
|
|
13
|
+
/**
|
|
14
|
+
* @description Generates a random OTP and sends it to the user's email via email service.
|
|
15
|
+
* @param {{ email: string; name: string }} options
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
static async sendMailOTP({ email }: { email: string }): Promise<void> {
|
|
19
|
+
const otp = Random.generateNumber(4);
|
|
20
|
+
const hash = await HashingModule.hash(otp.toString());
|
|
21
|
+
// const otpDoc = await db.query.otps.findFirst({
|
|
22
|
+
// where: (data, { eq }) => eq(data.email, email),
|
|
23
|
+
// });
|
|
24
|
+
const otpDoc = await db.select().from(otps).where(eq(otps.email, email));
|
|
25
|
+
if (otpDoc) {
|
|
26
|
+
await db.update(otps).set({ otp: hash }).where(eq(otps.email, email));
|
|
27
|
+
} else {
|
|
28
|
+
await db.insert(otps).values([{ email, otp: hash }]);
|
|
29
|
+
}
|
|
30
|
+
if (["test", ""].includes(process.env.NODE_ENV)) {
|
|
31
|
+
// await MockMailService.sendMail({
|
|
32
|
+
// to: email,
|
|
33
|
+
// text: `your otp is ${otp}`,
|
|
34
|
+
// });
|
|
35
|
+
} else {
|
|
36
|
+
const html = await AdminEmailTemplate.getOtpEmail({
|
|
37
|
+
otp: otp.toString(),
|
|
38
|
+
});
|
|
39
|
+
await SMTPMailService.sendMail(email, `OTP for ${email}`, `your otp is ${otp}`, html);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @description Verifies the OTP sent to the user's email.
|
|
45
|
+
* @param {string} email
|
|
46
|
+
* @param {string} otp
|
|
47
|
+
* @param {boolean} [deleteOtp=true] whether to delete the OTP doc after verification
|
|
48
|
+
* @returns {Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
static async verifyMailOTP(email: string, otp: string, deleteOtp = false): Promise<boolean> {
|
|
51
|
+
// const EXPIRATION_CONSTANT = 5 * 60 * 1000;
|
|
52
|
+
// const expirationTime = new Date(Date.now() + EXPIRATION_CONSTANT).getTime();
|
|
53
|
+
|
|
54
|
+
if (["test", "development"].includes(process.env.NODE_ENV)) return true;
|
|
55
|
+
|
|
56
|
+
// const otpDoc = await db.query.otps.findFirst({
|
|
57
|
+
// where: (data, { eq }) => and(eq(data.email, email)),
|
|
58
|
+
// });
|
|
59
|
+
const otpDoc = await db.select().from(otps).where(eq(otps.email, email));
|
|
60
|
+
if (["test", "development"].includes(process.env.NODE_ENV)) return true;
|
|
61
|
+
if (!otpDoc.length) return false;
|
|
62
|
+
|
|
63
|
+
const firstOtp = otpDoc[0];
|
|
64
|
+
|
|
65
|
+
const verified = await HashingModule.compare(otp, firstOtp.otp);
|
|
66
|
+
if (!verified) return false;
|
|
67
|
+
if (deleteOtp) {
|
|
68
|
+
await db.delete(otps).where(eq(otps.email, email));
|
|
69
|
+
} else {
|
|
70
|
+
await db.update(otps).set({ status: "verified" }).where(eq(otps.email, email));
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @description Checks if the OTP has been verified.
|
|
77
|
+
* @param {string} email
|
|
78
|
+
* @returns {Promise<boolean>}
|
|
79
|
+
*/
|
|
80
|
+
static async checkOTPStatus(email: string): Promise<boolean> {
|
|
81
|
+
// const otpDoc = await db.query.otps.findFirst({
|
|
82
|
+
// where: (data, { eq }) => eq(data.email, email),
|
|
83
|
+
// });
|
|
84
|
+
const otpDoc = await db.select().from(otps).where(eq(otps.email, email));
|
|
85
|
+
if (!otpDoc) return false;
|
|
86
|
+
const firstOtp = otpDoc[0];
|
|
87
|
+
return firstOtp.status === "verified";
|
|
88
|
+
}
|
|
89
|
+
static async deleteOTP(email: string): Promise<boolean> {
|
|
90
|
+
const otpDoc = await db.select().from(otps).where(eq(otps.email, email));
|
|
91
|
+
// const otpDoc = await db.query.otps.findFirst({
|
|
92
|
+
// where: (data, { eq }) => eq(data.email, email),
|
|
93
|
+
// });
|
|
94
|
+
if (!otpDoc) return false;
|
|
95
|
+
await db.delete(otps).where(eq(otps.email, email));
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PgSelect } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
type PageQuery = { page?: string; limit?: string };
|
|
4
|
+
|
|
5
|
+
export class Paginator {
|
|
6
|
+
static getPage(query: PageQuery = { page: "1", limit: "10" }) {
|
|
7
|
+
const page = parseInt(query.page ?? "1", 10);
|
|
8
|
+
const limit = parseInt(query.limit ?? "20", 10);
|
|
9
|
+
const offset = (page - 1) * limit;
|
|
10
|
+
return { page, limit, offset };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static async getTotalCount(
|
|
14
|
+
promise: Promise<
|
|
15
|
+
Array< {
|
|
16
|
+
count: number;
|
|
17
|
+
}>
|
|
18
|
+
>,
|
|
19
|
+
) {
|
|
20
|
+
const result = await promise;
|
|
21
|
+
return result[0]?.count || 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static paginate<T>(
|
|
25
|
+
query: PageQuery = { page: "1", limit: "10" },
|
|
26
|
+
rows: T[],
|
|
27
|
+
total: number,
|
|
28
|
+
) {
|
|
29
|
+
const { page, limit } = Paginator.getPage(query);
|
|
30
|
+
const totalPages = Math.ceil(total / limit);
|
|
31
|
+
return {
|
|
32
|
+
data: rows,
|
|
33
|
+
pagination: {
|
|
34
|
+
total,
|
|
35
|
+
totalPages,
|
|
36
|
+
currentPage: page,
|
|
37
|
+
pageSize: limit,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Not worth using
|
|
42
|
+
static withPagination<T extends PgSelect>(
|
|
43
|
+
qb: T,
|
|
44
|
+
query: PageQuery = { page: "1", limit: "10" },
|
|
45
|
+
) {
|
|
46
|
+
const { offset, limit } = Paginator.getPage(query);
|
|
47
|
+
return qb.limit(limit).offset(offset);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as jwt from "jsonwebtoken";
|
|
2
|
+
export type AuthTokenPayload = {
|
|
3
|
+
userID: string;
|
|
4
|
+
email: string;
|
|
5
|
+
};
|
|
6
|
+
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
|
|
7
|
+
const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;
|
|
8
|
+
|
|
9
|
+
export class TokenModule {
|
|
10
|
+
static signRefreshToken(payload: AuthTokenPayload) {
|
|
11
|
+
return jwt.sign(payload, refreshTokenSecret, { expiresIn: "365d" });
|
|
12
|
+
}
|
|
13
|
+
static verifyRefreshToken(refreshToken: string) {
|
|
14
|
+
try {
|
|
15
|
+
const payload = jwt.verify(refreshToken, refreshTokenSecret) as AuthTokenPayload;
|
|
16
|
+
return payload;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(error)
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
static signAccessToken(payload: AuthTokenPayload) {
|
|
23
|
+
const expiration = process.env.NODE_ENV === "development" ? "30d" : "1d";
|
|
24
|
+
return jwt.sign(payload, accessTokenSecret, { expiresIn: expiration });
|
|
25
|
+
}
|
|
26
|
+
static verifyAccessToken(accessToken: string) {
|
|
27
|
+
try {
|
|
28
|
+
const payload = jwt.verify(accessToken, accessTokenSecret) as AuthTokenPayload;
|
|
29
|
+
return payload;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(error)
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { redisClient } from "@repo/redis";
|
|
2
|
+
|
|
3
|
+
import { constants } from "./constants.js";
|
|
4
|
+
import { TokenModule } from "./token-module.js";
|
|
5
|
+
|
|
6
|
+
const accessTokenKey = (key: string) => `access_token:${key}`;
|
|
7
|
+
const refreshTokenKey = (key: string) => `refresh_token:${key}`;
|
|
8
|
+
const userTokensKey = (userId: string) => `session:${userId}`;
|
|
9
|
+
export type SessionUser = {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
fullName: string;
|
|
13
|
+
countries: string[];
|
|
14
|
+
department: string;
|
|
15
|
+
currentScenario?: string;
|
|
16
|
+
accessToken: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function jsonParse<T>(value: string) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(value) as T;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(error)
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class UserSession {
|
|
29
|
+
static async getSessionUser(token: string) {
|
|
30
|
+
const session = await redisClient.get(accessTokenKey(token));
|
|
31
|
+
if (session) {
|
|
32
|
+
try {
|
|
33
|
+
const user = JSON.parse(session) as SessionUser;
|
|
34
|
+
return user;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.log(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return session ? jsonParse<SessionUser>(session) : null;
|
|
40
|
+
}
|
|
41
|
+
static async startSession(payload: Omit<SessionUser, "accessToken">) {
|
|
42
|
+
await UserSession.endSession(payload.id);
|
|
43
|
+
const accessToken = TokenModule.signAccessToken({
|
|
44
|
+
userID: payload.id,
|
|
45
|
+
email: payload.email,
|
|
46
|
+
});
|
|
47
|
+
const refreshToken = TokenModule.signRefreshToken({
|
|
48
|
+
userID: payload.id,
|
|
49
|
+
email: payload.email,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await redisClient.setex(
|
|
53
|
+
accessTokenKey(accessToken),
|
|
54
|
+
constants.sessionDuration,
|
|
55
|
+
JSON.stringify({ ...payload, accessToken })
|
|
56
|
+
);
|
|
57
|
+
await redisClient.setex(refreshTokenKey(refreshToken), constants.sessionDuration, payload.id);
|
|
58
|
+
await redisClient.setex(
|
|
59
|
+
userTokensKey(payload.id),
|
|
60
|
+
constants.sessionDuration,
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
accessToken,
|
|
63
|
+
refreshToken,
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
return { accessToken, refreshToken };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async checkRefreshToken(refreshToken: string) {
|
|
70
|
+
const userId = await redisClient.get(refreshTokenKey(refreshToken));
|
|
71
|
+
return userId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static async updateAccessToken(payload: Omit<SessionUser, "accessToken">) {
|
|
75
|
+
let accessToken: string;
|
|
76
|
+
const tokens = await UserSession.getTokens(payload.id);
|
|
77
|
+
if (tokens) {
|
|
78
|
+
accessToken = tokens.accessToken;
|
|
79
|
+
} else {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await redisClient.setex(
|
|
84
|
+
accessTokenKey(accessToken),
|
|
85
|
+
constants.sessionDuration,
|
|
86
|
+
JSON.stringify({ ...payload, accessToken })
|
|
87
|
+
);
|
|
88
|
+
return accessToken;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static async getTokens(userId: string) {
|
|
92
|
+
const tokens = await redisClient.get(userTokensKey(userId));
|
|
93
|
+
return tokens ? jsonParse<{ refreshToken: string; accessToken: string }>(tokens) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static async logout(accessToken: string) {
|
|
97
|
+
const payload = await redisClient.get(accessTokenKey(accessToken));
|
|
98
|
+
if (!payload) return null;
|
|
99
|
+
const user = jsonParse<SessionUser>(payload);
|
|
100
|
+
const tokens = await UserSession.getTokens(user.id);
|
|
101
|
+
if (tokens) {
|
|
102
|
+
await redisClient.del(refreshTokenKey(tokens.refreshToken));
|
|
103
|
+
await redisClient.del(accessTokenKey(accessToken));
|
|
104
|
+
await redisClient.del(userTokensKey(user.id));
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static async endSession(userId: string) {
|
|
110
|
+
const tokens = await UserSession.getTokens(userId);
|
|
111
|
+
if (tokens) {
|
|
112
|
+
await redisClient.del(refreshTokenKey(tokens.refreshToken));
|
|
113
|
+
await redisClient.del(accessTokenKey(tokens.accessToken));
|
|
114
|
+
await redisClient.del(userTokensKey(userId));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|