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.
Files changed (181) hide show
  1. package/.editorconfig +20 -0
  2. package/.prettierignore +24 -0
  3. package/.prettierrc.js +52 -0
  4. package/.vscode/settings.json +3 -0
  5. package/LICENSE +21 -0
  6. package/apps/core-server/.env.example +24 -0
  7. package/apps/core-server/Dockerfile +61 -0
  8. package/apps/core-server/eslint.config.mjs +47 -0
  9. package/apps/core-server/package.json +73 -0
  10. package/apps/core-server/src/config/custom-type.ts +54 -0
  11. package/apps/core-server/src/events/index.ts +37 -0
  12. package/apps/core-server/src/index.ts +19 -0
  13. package/apps/core-server/src/middlewares/auth-middleware.ts +50 -0
  14. package/apps/core-server/src/middlewares/cors-middleware.ts +6 -0
  15. package/apps/core-server/src/middlewares/error-middleware.ts +23 -0
  16. package/apps/core-server/src/middlewares/logger-middleware.ts +21 -0
  17. package/apps/core-server/src/middlewares/notfound-middleware.ts +14 -0
  18. package/apps/core-server/src/middlewares/serve-static.ts +24 -0
  19. package/apps/core-server/src/routes/files/router.ts +7 -0
  20. package/apps/core-server/src/routes/server.ts +36 -0
  21. package/apps/core-server/tsconfig.json +10 -0
  22. package/apps/core-server/tsdown.config.ts +14 -0
  23. package/biome.json +62 -0
  24. package/configs/eslint-config/package.json +60 -0
  25. package/configs/eslint-config/plugins.d.ts +1 -0
  26. package/configs/eslint-config/src/configs/base.ts +237 -0
  27. package/configs/eslint-config/src/configs/frontend.ts +62 -0
  28. package/configs/eslint-config/src/configs/node.ts +10 -0
  29. package/configs/eslint-config/src/plugin.ts +25 -0
  30. package/configs/eslint-config/src/rules/index.ts +30 -0
  31. package/configs/eslint-config/src/rules/no-argument-spread.test.ts +47 -0
  32. package/configs/eslint-config/src/rules/no-argument-spread.ts +96 -0
  33. package/configs/eslint-config/src/rules/no-dynamic-import-template.ts +32 -0
  34. package/configs/eslint-config/src/rules/no-internal-package-import.ts +40 -0
  35. package/configs/eslint-config/src/rules/no-interpolation-in-regular-string.ts +32 -0
  36. package/configs/eslint-config/src/rules/no-json-parse-json-stringify.test.ts +34 -0
  37. package/configs/eslint-config/src/rules/no-json-parse-json-stringify.ts +49 -0
  38. package/configs/eslint-config/src/rules/no-plain-errors.ts +50 -0
  39. package/configs/eslint-config/src/rules/no-skipped-tests.ts +61 -0
  40. package/configs/eslint-config/src/rules/no-top-level-relative-imports-in-backend-module.ts +27 -0
  41. package/configs/eslint-config/src/rules/no-type-unsafe-event-emitter.ts +33 -0
  42. package/configs/eslint-config/src/rules/no-uncaught-json-parse.test.ts +21 -0
  43. package/configs/eslint-config/src/rules/no-uncaught-json-parse.ts +45 -0
  44. package/configs/eslint-config/src/rules/no-untyped-config-class-field.ts +26 -0
  45. package/configs/eslint-config/src/rules/no-unused-param-catch-clause.ts +33 -0
  46. package/configs/eslint-config/src/rules/no-useless-catch-throw.test.ts +34 -0
  47. package/configs/eslint-config/src/rules/no-useless-catch-throw.ts +47 -0
  48. package/configs/eslint-config/src/utils/json.ts +21 -0
  49. package/configs/eslint-config/tsconfig.json +8 -0
  50. package/configs/eslint-config/tsdown.config.ts +11 -0
  51. package/configs/eslint-config/vitest.config.ts +3 -0
  52. package/configs/tsdown-config/package.json +14 -0
  53. package/configs/tsdown-config/src/config/tsdown.base.ts +13 -0
  54. package/configs/typescript-config/package.json +10 -0
  55. package/configs/typescript-config/tsconfig.common.json +32 -0
  56. package/configs/typescript-config/tsconfig.frontend.json +14 -0
  57. package/configs/typescript-config/tsconfig.node.json +9 -0
  58. package/configs/vitest-config/package.json +25 -0
  59. package/configs/vitest-config/src/base.ts +34 -0
  60. package/configs/vitest-config/src/frontend.ts +15 -0
  61. package/configs/vitest-config/src/node.ts +5 -0
  62. package/configs/vitest-config/tsconfig.json +7 -0
  63. package/package.json +47 -0
  64. package/packages/@repo/constants/eslint.config.mjs +21 -0
  65. package/packages/@repo/constants/package.json +19 -0
  66. package/packages/@repo/constants/src/api.ts +1 -0
  67. package/packages/@repo/constants/src/index.ts +8 -0
  68. package/packages/@repo/constants/src/time.ts +23 -0
  69. package/packages/@repo/constants/tsconfig.json +7 -0
  70. package/packages/@repo/db/eslint.config.mjs +21 -0
  71. package/packages/@repo/db/package.json +30 -0
  72. package/packages/@repo/db/src/functions.ts +122 -0
  73. package/packages/@repo/db/src/index.ts +20 -0
  74. package/packages/@repo/db/src/schema/common.ts +49 -0
  75. package/packages/@repo/db/src/schema/index.ts +1 -0
  76. package/packages/@repo/db/tsconfig.json +13 -0
  77. package/packages/@repo/lib/eslint.config.mjs +49 -0
  78. package/packages/@repo/lib/package.json +57 -0
  79. package/packages/@repo/lib/src/bucket-module/file-storage.ts +49 -0
  80. package/packages/@repo/lib/src/bucket-module/s3-storage.ts +114 -0
  81. package/packages/@repo/lib/src/bucket-module/utils.ts +11 -0
  82. package/packages/@repo/lib/src/command-module.ts +77 -0
  83. package/packages/@repo/lib/src/constants.ts +3 -0
  84. package/packages/@repo/lib/src/cookie-module.ts +42 -0
  85. package/packages/@repo/lib/src/custom-type.ts +54 -0
  86. package/packages/@repo/lib/src/env.ts +13 -0
  87. package/packages/@repo/lib/src/error-handlers-module/index.ts +11 -0
  88. package/packages/@repo/lib/src/file-system/index.ts +90 -0
  89. package/packages/@repo/lib/src/hashing-module.ts +9 -0
  90. package/packages/@repo/lib/src/index.ts +27 -0
  91. package/packages/@repo/lib/src/logger-module/log-config.ts +16 -0
  92. package/packages/@repo/lib/src/logger-module/logger.ts +78 -0
  93. package/packages/@repo/lib/src/logger-module/memory-profiler.ts +65 -0
  94. package/packages/@repo/lib/src/mail-module/api.ts +0 -0
  95. package/packages/@repo/lib/src/mail-module/mock.ts +8 -0
  96. package/packages/@repo/lib/src/mail-module/nodemailer.ts +45 -0
  97. package/packages/@repo/lib/src/notification-module/index.ts +172 -0
  98. package/packages/@repo/lib/src/notification-module/push-notification.ts +90 -0
  99. package/packages/@repo/lib/src/oauth2-client.ts +109 -0
  100. package/packages/@repo/lib/src/otp-module.ts +98 -0
  101. package/packages/@repo/lib/src/pagination-module.ts +49 -0
  102. package/packages/@repo/lib/src/token-module.ts +35 -0
  103. package/packages/@repo/lib/src/user-session.ts +117 -0
  104. package/packages/@repo/lib/src/utils.ts +42 -0
  105. package/packages/@repo/lib/src/validation-module.ts +187 -0
  106. package/packages/@repo/lib/tsconfig.json +7 -0
  107. package/packages/@repo/mail/package.json +29 -0
  108. package/packages/@repo/mail/src/emails/admin/OtpEmail.tsx +168 -0
  109. package/packages/@repo/mail/src/index.ts +13 -0
  110. package/packages/@repo/mail/tsconfig.build.json +14 -0
  111. package/packages/@repo/mail/tsconfig.json +13 -0
  112. package/packages/@repo/mail/tsdown.config.ts +9 -0
  113. package/packages/@repo/redis/eslint.config.mjs +8 -0
  114. package/packages/@repo/redis/package.json +31 -0
  115. package/packages/@repo/redis/src/index.ts +2 -0
  116. package/packages/@repo/redis/src/lib/redis-client.ts +23 -0
  117. package/packages/@repo/redis/src/lib/redis-module.ts +3 -0
  118. package/packages/@repo/redis/tsconfig.json +12 -0
  119. package/packages/ui/components.json +17 -0
  120. package/packages/ui/eslint.config.mjs +18 -0
  121. package/packages/ui/package.json +67 -0
  122. package/packages/ui/postcss.config.mjs +9 -0
  123. package/packages/ui/src/components/custom/form-wrapper.tsx +551 -0
  124. package/packages/ui/src/components/custom/grid-component.tsx +23 -0
  125. package/packages/ui/src/components/custom/hover-tool.tsx +38 -0
  126. package/packages/ui/src/components/custom/image-picker.tsx +109 -0
  127. package/packages/ui/src/components/custom/no-content.tsx +37 -0
  128. package/packages/ui/src/components/custom/page-container.tsx +24 -0
  129. package/packages/ui/src/components/custom/page-section.tsx +59 -0
  130. package/packages/ui/src/components/custom/simple-popover.tsx +29 -0
  131. package/packages/ui/src/components/custom/switch-component.tsx +20 -0
  132. package/packages/ui/src/components/custom/theme-provider.tsx +74 -0
  133. package/packages/ui/src/components/custom/typography.tsx +111 -0
  134. package/packages/ui/src/components/extensions/carousel.tsx +392 -0
  135. package/packages/ui/src/components/hooks/event/use-click.tsx +39 -0
  136. package/packages/ui/src/components/hooks/time/useDebounce.tsx +21 -0
  137. package/packages/ui/src/components/hooks/time/useInterval.tsx +35 -0
  138. package/packages/ui/src/components/hooks/time/useTimeout.tsx +19 -0
  139. package/packages/ui/src/components/hooks/time/useTimer.tsx +51 -0
  140. package/packages/ui/src/components/hooks/use-media-query.tsx +19 -0
  141. package/packages/ui/src/components/hooks/use-persistent-storage.tsx +52 -0
  142. package/packages/ui/src/components/hooks/use-update-effect.tsx +13 -0
  143. package/packages/ui/src/components/hooks/use-window-dimension.tsx +30 -0
  144. package/packages/ui/src/components/lib/utils.ts +242 -0
  145. package/packages/ui/src/components/lucide.tsx +3 -0
  146. package/packages/ui/src/components/sonner.tsx +1 -0
  147. package/packages/ui/src/components/ui/alert-dialog.tsx +116 -0
  148. package/packages/ui/src/components/ui/avatar.tsx +53 -0
  149. package/packages/ui/src/components/ui/badge.tsx +46 -0
  150. package/packages/ui/src/components/ui/breadcrumb.tsx +109 -0
  151. package/packages/ui/src/components/ui/button.tsx +96 -0
  152. package/packages/ui/src/components/ui/card.tsx +92 -0
  153. package/packages/ui/src/components/ui/carousel.tsx +243 -0
  154. package/packages/ui/src/components/ui/checkbox.tsx +32 -0
  155. package/packages/ui/src/components/ui/command.tsx +155 -0
  156. package/packages/ui/src/components/ui/dialog.tsx +127 -0
  157. package/packages/ui/src/components/ui/dropdown-menu.tsx +226 -0
  158. package/packages/ui/src/components/ui/form.tsx +165 -0
  159. package/packages/ui/src/components/ui/input-otp.tsx +76 -0
  160. package/packages/ui/src/components/ui/input.tsx +21 -0
  161. package/packages/ui/src/components/ui/label.tsx +24 -0
  162. package/packages/ui/src/components/ui/multiple-select.tsx +510 -0
  163. package/packages/ui/src/components/ui/popover.tsx +42 -0
  164. package/packages/ui/src/components/ui/select.tsx +170 -0
  165. package/packages/ui/src/components/ui/separator.tsx +28 -0
  166. package/packages/ui/src/components/ui/sheet.tsx +130 -0
  167. package/packages/ui/src/components/ui/skeleton.tsx +13 -0
  168. package/packages/ui/src/components/ui/spinner.tsx +16 -0
  169. package/packages/ui/src/components/ui/switch.tsx +28 -0
  170. package/packages/ui/src/components/ui/table.tsx +116 -0
  171. package/packages/ui/src/components/ui/tabs.tsx +54 -0
  172. package/packages/ui/src/components/ui/textarea.tsx +18 -0
  173. package/packages/ui/src/components/ui/timeline.tsx +118 -0
  174. package/packages/ui/src/components/ui/tooltip.tsx +30 -0
  175. package/packages/ui/src/components/util/n-formattor.ts +22 -0
  176. package/packages/ui/src/components/util/storage.ts +37 -0
  177. package/packages/ui/src/globals.css +87 -0
  178. package/packages/ui/tailwind.config.ts +94 -0
  179. package/packages/ui/tsconfig.json +12 -0
  180. package/pnpm-workspace.yaml +43 -0
  181. 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
+ }