startx 0.0.1 → 0.1.0

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 (154) hide show
  1. package/.prettierignore +0 -13
  2. package/.prettierrc.js +52 -52
  3. package/.vscode/launch.json +32 -0
  4. package/.vscode/settings.json +9 -3
  5. package/apps/core-server/.env.example +18 -24
  6. package/apps/core-server/Dockerfile +35 -61
  7. package/apps/core-server/eslint.config.ts +7 -0
  8. package/apps/core-server/package.json +41 -52
  9. package/apps/core-server/src/config/custom-type.ts +2 -40
  10. package/apps/core-server/src/events/index.ts +37 -37
  11. package/apps/core-server/src/index.ts +4 -13
  12. package/apps/core-server/src/middlewares/auth-middleware.ts +24 -7
  13. package/apps/core-server/src/middlewares/cors-middleware.ts +7 -6
  14. package/apps/core-server/src/middlewares/error-middleware.ts +7 -4
  15. package/apps/core-server/src/middlewares/logger-middleware.ts +81 -21
  16. package/apps/core-server/src/middlewares/notfound-middleware.ts +6 -14
  17. package/apps/core-server/src/middlewares/serve-static.ts +30 -24
  18. package/apps/core-server/src/routes/files/router.ts +9 -7
  19. package/apps/core-server/src/routes/server.ts +30 -36
  20. package/apps/core-server/tsdown.config.ts +4 -3
  21. package/biome.json +58 -60
  22. package/configs/eslint-config/package.json +16 -19
  23. package/configs/eslint-config/src/configs/base.ts +185 -225
  24. package/configs/eslint-config/src/configs/extend.ts +3 -0
  25. package/configs/eslint-config/src/configs/frontend.ts +81 -56
  26. package/configs/eslint-config/src/configs/node.ts +6 -6
  27. package/configs/eslint-config/src/plugin.ts +1 -0
  28. package/configs/eslint-config/src/rules/index.ts +8 -12
  29. package/configs/eslint-config/src/rules/no-json-parse-json-stringify.test.ts +30 -17
  30. package/configs/eslint-config/src/rules/no-json-parse-json-stringify.ts +52 -49
  31. package/configs/eslint-config/src/rules/no-uncaught-json-parse.ts +43 -45
  32. package/configs/tsdown-config/package.json +10 -3
  33. package/configs/typescript-config/package.json +10 -1
  34. package/configs/typescript-config/tsconfig.common.json +3 -3
  35. package/configs/vitest-config/dist/base.mjs +1 -0
  36. package/configs/vitest-config/dist/frontend.mjs +1 -0
  37. package/configs/vitest-config/dist/node.mjs +1 -0
  38. package/configs/vitest-config/package.json +12 -0
  39. package/configs/vitest-config/src/base.ts +17 -29
  40. package/configs/vitest-config/src/index.ts +1 -0
  41. package/package.json +21 -13
  42. package/packages/@repo/constants/eslint.config.ts +4 -0
  43. package/packages/@repo/constants/package.json +16 -0
  44. package/packages/@repo/constants/src/index.ts +8 -8
  45. package/packages/@repo/db/eslint.config.ts +4 -0
  46. package/packages/@repo/db/package.json +16 -8
  47. package/packages/@repo/db/src/index.ts +26 -20
  48. package/packages/@repo/db/src/schema/common.ts +45 -49
  49. package/packages/@repo/env/eslint.config.ts +4 -0
  50. package/packages/@repo/env/package.json +39 -0
  51. package/packages/@repo/env/src/default-env.ts +12 -0
  52. package/packages/@repo/env/src/define-env.ts +70 -0
  53. package/packages/@repo/env/src/index.ts +2 -0
  54. package/packages/@repo/env/src/utils.ts +52 -0
  55. package/packages/@repo/env/tsconfig.json +7 -0
  56. package/packages/@repo/lib/eslint.config.ts +4 -0
  57. package/packages/@repo/lib/package.json +34 -34
  58. package/packages/@repo/lib/src/bucket-module/file-storage.ts +50 -49
  59. package/packages/@repo/lib/src/bucket-module/index.ts +3 -0
  60. package/packages/@repo/lib/src/bucket-module/s3-storage.ts +120 -114
  61. package/packages/@repo/lib/src/bucket-module/utils.ts +10 -11
  62. package/packages/@repo/lib/src/{cookie-module.ts → cookie-module/cookie-module.ts} +48 -42
  63. package/packages/@repo/lib/src/cookie-module/index.ts +1 -0
  64. package/packages/@repo/lib/src/extra/index.ts +1 -0
  65. package/packages/@repo/lib/src/extra/pagination-module.ts +35 -0
  66. package/packages/@repo/lib/src/{token-module.ts → extra/token-module.ts} +12 -5
  67. package/packages/@repo/lib/src/file-system-module/index.ts +170 -0
  68. package/packages/@repo/lib/src/{hashing-module.ts → hashing-module/index.ts} +9 -9
  69. package/packages/@repo/lib/src/index.ts +0 -26
  70. package/packages/@repo/lib/src/mail-module/index.ts +2 -0
  71. package/packages/@repo/lib/src/mail-module/mock.ts +8 -8
  72. package/packages/@repo/lib/src/mail-module/nodemailer.ts +17 -7
  73. package/packages/@repo/lib/src/notification-module/index.ts +1 -172
  74. package/packages/@repo/lib/src/notification-module/push-notification.ts +97 -90
  75. package/packages/@repo/lib/src/{oauth2-client.ts → oauth2-module/index.ts} +107 -109
  76. package/packages/@repo/lib/src/otp-module/index.ts +91 -0
  77. package/packages/@repo/lib/src/session-module/index.ts +113 -0
  78. package/packages/@repo/lib/src/utils.ts +43 -42
  79. package/packages/@repo/lib/src/validation-module/index.ts +242 -0
  80. package/packages/@repo/logger/eslint.config.ts +4 -0
  81. package/packages/@repo/logger/package.json +40 -0
  82. package/packages/@repo/logger/src/index.ts +2 -0
  83. package/packages/@repo/logger/src/logger.ts +72 -0
  84. package/packages/@repo/{lib/src/logger-module → logger/src}/memory-profiler.ts +64 -65
  85. package/packages/@repo/logger/tsconfig.json +7 -0
  86. package/packages/@repo/mail/eslint.config.ts +4 -0
  87. package/packages/@repo/mail/package.json +10 -3
  88. package/packages/@repo/mail/src/emails/admin/OtpEmail.tsx +169 -168
  89. package/packages/@repo/mail/src/index.ts +1 -2
  90. package/packages/@repo/mail/tsconfig.json +3 -3
  91. package/packages/@repo/redis/dist/index.d.mts +3 -0
  92. package/packages/@repo/redis/dist/index.mjs +5 -0
  93. package/packages/@repo/redis/dist/lib/redis-client.d.mts +7 -0
  94. package/packages/@repo/redis/dist/lib/redis-client.mjs +25 -0
  95. package/packages/@repo/redis/dist/lib/redis-client.mjs.map +1 -0
  96. package/packages/@repo/redis/dist/lib/redis-module.d.mts +5 -0
  97. package/packages/@repo/redis/dist/lib/redis-module.mjs +6 -0
  98. package/packages/@repo/redis/dist/lib/redis-module.mjs.map +1 -0
  99. package/packages/@repo/redis/eslint.config.ts +4 -0
  100. package/packages/@repo/redis/package.json +13 -10
  101. package/packages/@repo/redis/src/index.ts +2 -2
  102. package/packages/@repo/redis/src/lib/redis-client.ts +36 -23
  103. package/packages/@repo/redis/src/lib/redis-module.ts +69 -3
  104. package/packages/cli/dist/index.mjs +203 -0
  105. package/packages/cli/eslint.config.ts +4 -0
  106. package/packages/cli/package.json +44 -0
  107. package/packages/cli/tsconfig.json +12 -0
  108. package/packages/cli/tsdown.config.ts +17 -0
  109. package/packages/ui/components.json +0 -1
  110. package/packages/ui/eslint.config.ts +4 -0
  111. package/packages/ui/package.json +16 -3
  112. package/packages/ui/postcss.config.mjs +9 -9
  113. package/packages/ui/src/components/lib/utils.ts +53 -53
  114. package/packages/ui/src/components/ui/alert-dialog.tsx +118 -116
  115. package/packages/ui/src/components/ui/avatar.tsx +52 -53
  116. package/packages/ui/src/components/ui/badge.tsx +45 -46
  117. package/packages/ui/src/components/ui/breadcrumb.tsx +108 -109
  118. package/packages/ui/src/components/ui/card.tsx +91 -92
  119. package/packages/ui/src/components/ui/carousel.tsx +243 -243
  120. package/packages/ui/src/components/ui/checkbox.tsx +32 -32
  121. package/packages/ui/src/components/ui/command.tsx +144 -155
  122. package/packages/ui/src/components/ui/dialog.tsx +124 -127
  123. package/packages/ui/src/components/ui/form.tsx +166 -165
  124. package/packages/ui/src/components/ui/input-otp.tsx +74 -76
  125. package/packages/ui/src/components/ui/input.tsx +19 -21
  126. package/packages/ui/src/components/ui/multiple-select.tsx +4 -4
  127. package/packages/ui/src/{components/lucide.tsx → lucide.ts} +3 -3
  128. package/packages/ui/tailwind.config.ts +94 -94
  129. package/packages/ui/tsconfig.json +7 -1
  130. package/pnpm-workspace.yaml +41 -1
  131. package/turbo.json +20 -27
  132. package/apps/core-server/eslint.config.mjs +0 -47
  133. package/configs/eslint-config/src/rules/no-dynamic-import-template.ts +0 -32
  134. package/configs/eslint-config/src/rules/no-plain-errors.ts +0 -50
  135. package/configs/eslint-config/tsdown.config.ts +0 -11
  136. package/packages/@repo/constants/eslint.config.mjs +0 -21
  137. package/packages/@repo/db/eslint.config.mjs +0 -21
  138. package/packages/@repo/lib/eslint.config.mjs +0 -49
  139. package/packages/@repo/lib/src/command-module.ts +0 -77
  140. package/packages/@repo/lib/src/constants.ts +0 -3
  141. package/packages/@repo/lib/src/custom-type.ts +0 -54
  142. package/packages/@repo/lib/src/env.ts +0 -13
  143. package/packages/@repo/lib/src/file-system/index.ts +0 -90
  144. package/packages/@repo/lib/src/logger-module/log-config.ts +0 -16
  145. package/packages/@repo/lib/src/logger-module/logger.ts +0 -78
  146. package/packages/@repo/lib/src/mail-module/api.ts +0 -0
  147. package/packages/@repo/lib/src/otp-module.ts +0 -98
  148. package/packages/@repo/lib/src/pagination-module.ts +0 -49
  149. package/packages/@repo/lib/src/user-session.ts +0 -117
  150. package/packages/@repo/lib/src/validation-module.ts +0 -187
  151. package/packages/@repo/mail/tsconfig.build.json +0 -14
  152. package/packages/@repo/mail/tsdown.config.ts +0 -9
  153. package/packages/@repo/redis/eslint.config.mjs +0 -8
  154. package/packages/ui/eslint.config.mjs +0 -18
@@ -0,0 +1,91 @@
1
+ import { ENV } from "@repo/env";
2
+ import { logger } from "@repo/logger";
3
+ import { AdminEmailTemplate } from "@repo/mail";
4
+ import { RedisStore } from "@repo/redis";
5
+
6
+ import { HashingModule } from "../hashing-module/index.js";
7
+ import { SMTPMailService } from "../mail-module/nodemailer.js";
8
+ import { Random } from "../utils.js";
9
+
10
+ const redisOtpStore = new RedisStore<{
11
+ email: string;
12
+ otp: string;
13
+ status: "pending" | "verified";
14
+ }>({
15
+ namespace: "otp",
16
+ });
17
+ export class OTPModule {
18
+ private static otpExpirationMs = 5 * 60 * 1000;
19
+
20
+ static async sendMailOTP({ email }: { email: string }): Promise<void> {
21
+ const normalizedEmail = email.trim().toLowerCase();
22
+
23
+ // Generate OTP as a 4-digit string (preserve leading zeros)
24
+ const otpStr = String(Random.generateNumber(4)).padStart(4, "0");
25
+ const hash = await HashingModule.hash(otpStr);
26
+
27
+ try {
28
+ await redisOtpStore.set(
29
+ normalizedEmail,
30
+ { email: normalizedEmail, otp: hash, status: "pending" },
31
+ this.otpExpirationMs
32
+ );
33
+ } catch (err) {
34
+ logger?.error("otp: redis write failed", { email: normalizedEmail, err });
35
+ throw err;
36
+ }
37
+
38
+ // Do not leak OTP in non-test environments
39
+ if (["test", "development"].includes(ENV.NODE_ENV)) {
40
+ // optionally: mock mail send for tests
41
+ logger?.info("otp: test-mode - OTP generated", { email: normalizedEmail, otp: otpStr });
42
+ return;
43
+ }
44
+ const html = await AdminEmailTemplate.getOtpEmail({ otp: otpStr });
45
+ await SMTPMailService.sendMail(
46
+ normalizedEmail,
47
+ `OTP for ${normalizedEmail}`,
48
+ `Your OTP is ${otpStr}`,
49
+ html
50
+ );
51
+ }
52
+
53
+ static async verifyMailOTP(email: string, otp: string, deleteOtp = false): Promise<boolean> {
54
+ const normalizedEmail = email.trim().toLowerCase();
55
+
56
+ // shortcut for test/dev environments — be careful with this in real dev
57
+ // if (["test"].includes(ENV.NODE_ENV)) return true;
58
+
59
+ const rows = await redisOtpStore.get(normalizedEmail);
60
+ if (!rows?.otp) return false;
61
+
62
+ const firstOtp = rows.otp;
63
+
64
+ const verified = await HashingModule.compare(otp, firstOtp);
65
+ if (!verified) return false;
66
+
67
+ if (deleteOtp) {
68
+ await redisOtpStore.del(normalizedEmail);
69
+ } else {
70
+ await redisOtpStore.set(
71
+ normalizedEmail,
72
+ { ...rows, status: "verified" },
73
+ this.otpExpirationMs
74
+ );
75
+ }
76
+ return true;
77
+ }
78
+
79
+ static async checkOTPStatus(email: string): Promise<boolean> {
80
+ const normalizedEmail = email.trim().toLowerCase();
81
+ const rows = await redisOtpStore.get(normalizedEmail);
82
+ if (!rows?.otp) return false;
83
+ return rows.status === "verified";
84
+ }
85
+
86
+ static async deleteOTP(email: string): Promise<boolean> {
87
+ const normalizedEmail = email.trim().toLowerCase();
88
+ await redisOtpStore.del(normalizedEmail);
89
+ return true;
90
+ }
91
+ }
@@ -0,0 +1,113 @@
1
+ import { RedisStore } from "@repo/redis";
2
+
3
+ import { TokenModule } from "../extra/token-module.js";
4
+ export const constants = {
5
+ sessionDuration: 60 * 60 * 6,
6
+ };
7
+
8
+ const accessTokenKey = (key: string) => `access_token:${key}`;
9
+ const refreshTokenKey = (key: string) => `refresh_token:${key}`;
10
+ const userTokensKey = (userId: string) => `session:${userId}`;
11
+ export type SessionUser = {
12
+ id: string;
13
+ email: string;
14
+ fullName: string;
15
+ countries: string[];
16
+ department: string;
17
+ currentScenario?: string;
18
+ accessToken: string;
19
+ };
20
+
21
+ const userRedisStore = new RedisStore<SessionUser>({
22
+ namespace: "user-session",
23
+ });
24
+
25
+ const userRedisTokenStore = new RedisStore<{ accessToken: string; refreshToken: string }>({
26
+ namespace: "user-tokens",
27
+ });
28
+
29
+ export class UserSession {
30
+ static async getSessionUser(token: string) {
31
+ return await userRedisStore.get(accessTokenKey(token));
32
+ }
33
+
34
+ static async startSession(payload: Omit<SessionUser, "accessToken">) {
35
+ await UserSession.endSession(payload.id);
36
+
37
+ const accessToken = TokenModule.signAccessToken({
38
+ userID: payload.id,
39
+ email: payload.email,
40
+ });
41
+
42
+ const refreshToken = TokenModule.signRefreshToken({
43
+ userID: payload.id,
44
+ email: payload.email,
45
+ });
46
+
47
+ const sessionData: SessionUser = { ...payload, accessToken };
48
+
49
+ // store access token -> user
50
+ await userRedisStore.set(accessTokenKey(accessToken), sessionData, constants.sessionDuration);
51
+
52
+ // store user -> session (optional but useful)
53
+ await userRedisStore.set(payload.id, sessionData, constants.sessionDuration);
54
+
55
+ // store refresh token -> userId
56
+ await userRedisTokenStore.set(
57
+ refreshTokenKey(refreshToken),
58
+ { accessToken, refreshToken },
59
+ constants.sessionDuration
60
+ );
61
+
62
+ // store user -> tokens
63
+ await userRedisTokenStore.set(
64
+ userTokensKey(payload.id),
65
+ { accessToken, refreshToken },
66
+ constants.sessionDuration
67
+ );
68
+
69
+ return { accessToken, refreshToken };
70
+ }
71
+
72
+ static async checkRefreshToken(refreshToken: string) {
73
+ const tokens = await userRedisTokenStore.get(refreshTokenKey(refreshToken));
74
+ return tokens ?? null;
75
+ }
76
+
77
+ static async updateAccessToken(payload: Omit<SessionUser, "accessToken">) {
78
+ const tokens = await this.getTokens(payload.id);
79
+ if (!tokens) return null;
80
+
81
+ const accessToken = tokens.accessToken;
82
+
83
+ await userRedisStore.set(
84
+ accessTokenKey(accessToken),
85
+ { ...payload, accessToken },
86
+ constants.sessionDuration
87
+ );
88
+
89
+ return accessToken;
90
+ }
91
+
92
+ static async getTokens(userId: string) {
93
+ return await userRedisTokenStore.get(userTokensKey(userId));
94
+ }
95
+
96
+ static async logout(accessToken: string) {
97
+ const session = await userRedisStore.get(accessTokenKey(accessToken));
98
+ if (!session) return null;
99
+
100
+ await this.endSession(session.id);
101
+ return null;
102
+ }
103
+
104
+ static async endSession(userId: string) {
105
+ const tokens = await this.getTokens(userId);
106
+ if (!tokens) return;
107
+
108
+ await userRedisTokenStore.del(refreshTokenKey(tokens.refreshToken));
109
+ await userRedisStore.del(accessTokenKey(tokens.accessToken));
110
+ await userRedisTokenStore.del(userTokensKey(userId));
111
+ await userRedisStore.del(userId);
112
+ }
113
+ }
@@ -1,42 +1,43 @@
1
- import crypto from "crypto";
2
- import path from "path";
3
-
4
- export function __dirname() {
5
- if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
6
- return path.resolve(process.cwd(), "../../");
7
- }
8
- return process.cwd();
9
- }
10
-
11
- /**
12
- * @description Utility class for generating random strings and numbers
13
- */
14
- export class Random {
15
- /**
16
- * @description Generate a random UUID
17
- */
18
- static generateUUID() {
19
- return crypto.randomUUID();
20
- }
21
-
22
- /**
23
- * @description Generate a random string
24
- * @param length
25
- * @param encoding (default: 'hex')
26
- */
27
- static generateString(length: number, encoding: BufferEncoding = "hex") {
28
- return crypto.randomBytes(length).toString(encoding);
29
- }
30
-
31
- /**
32
- * @description Generate a random number
33
- * @param digits (default: 6)
34
- */
35
- static generateNumber(digits: number = 6) {
36
- return crypto.randomInt(10 ** (digits - 1), 10 ** digits);
37
- }
38
-
39
- static generateBoolean() {
40
- return crypto.randomInt(0, 2) === 1;
41
- }
42
- }
1
+ import { ENV } from "@repo/env";
2
+ import crypto from "crypto";
3
+ import path from "path";
4
+
5
+ export function __dirname() {
6
+ if (ENV.NODE_ENV === "development") {
7
+ return path.resolve(process.cwd(), "../../");
8
+ }
9
+ return process.cwd();
10
+ }
11
+
12
+ /**
13
+ * @description Utility class for generating random strings and numbers
14
+ */
15
+ export class Random {
16
+ /**
17
+ * @description Generate a random UUID
18
+ */
19
+ static generateUUID() {
20
+ return crypto.randomUUID();
21
+ }
22
+
23
+ /**
24
+ * @description Generate a random string
25
+ * @param length
26
+ * @param encoding (default: 'hex')
27
+ */
28
+ static generateString(length: number, encoding: BufferEncoding = "hex") {
29
+ return crypto.randomBytes(length).toString(encoding);
30
+ }
31
+
32
+ /**
33
+ * @description Generate a random number
34
+ * @param digits (default: 6)
35
+ */
36
+ static generateNumber(digits: number = 6) {
37
+ return crypto.randomInt(10 ** (digits - 1), 10 ** digits);
38
+ }
39
+
40
+ static generateBoolean() {
41
+ return crypto.randomInt(0, 2) === 1;
42
+ }
43
+ }
@@ -0,0 +1,242 @@
1
+ import vine from "@vinejs/vine";
2
+ import type { Infer, SchemaTypes } from "@vinejs/vine/types";
3
+ import type { NextFunction, Request, Response } from "express";
4
+
5
+ import { ErrorResponse } from "../error-handlers-module/index.js";
6
+ import { logger } from "@repo/logger";
7
+
8
+ type ExpressHandler<P = unknown, ResBody = unknown, ReqBody = unknown, Query = unknown> = (
9
+ req: Request<P, ResBody, ReqBody, Query>,
10
+ res: Response,
11
+ next: NextFunction
12
+ ) => unknown;
13
+ export async function validateBody<T extends SchemaTypes>(
14
+ schema: T,
15
+ payload: unknown
16
+ ): Promise<{ data?: Infer<T>; error: string[] }> {
17
+ try {
18
+ const validator = vine.compile(schema);
19
+ const data = await validator.validate(payload);
20
+
21
+ return { data, error: [] };
22
+ } catch (err: unknown) {
23
+ if (err && typeof err === "object" && "messages" in err) {
24
+ const messages = (err as { messages: Array<{ message: string }> }).messages;
25
+ return {
26
+ error: messages.map(e => e.message),
27
+ };
28
+ }
29
+
30
+ return { error: ["Validation failed"] };
31
+ }
32
+ }
33
+ export function bodyValidator<T extends SchemaTypes>(schema: T) {
34
+ return function <F extends ExpressHandler<unknown, unknown, Infer<T>, unknown>>(
35
+ _target: unknown,
36
+ _propertyKey: string,
37
+ descriptor: TypedPropertyDescriptor<F>
38
+ ) {
39
+ const originalMethod = descriptor.value!;
40
+
41
+ descriptor.value = async function (
42
+ this: unknown,
43
+ req: Request<unknown, unknown, Infer<T>>,
44
+ res: Response,
45
+ next: NextFunction
46
+ ) {
47
+ const { error, data } = await validateBody(schema, req.body);
48
+
49
+ logger.info(`Body: ${JSON.stringify(req.body, null, 2)}`, {
50
+ logType: "requestBody",
51
+ });
52
+
53
+ if (!data || error.length) {
54
+ logger.error(error.join("\n"), { logType: "validationErrors" });
55
+ return res.status(422).json({ message: error.join("\n") });
56
+ }
57
+
58
+ req.body = data;
59
+
60
+ return originalMethod.call(this, req, res, next);
61
+ } as F;
62
+
63
+ return descriptor;
64
+ };
65
+ }
66
+ export function paramsValidator<T extends SchemaTypes>(schema: T) {
67
+ return function <F extends ExpressHandler<Infer<T>, unknown, unknown, unknown>>(
68
+ _target: unknown,
69
+ _propertyKey: string,
70
+ descriptor: TypedPropertyDescriptor<F>
71
+ ) {
72
+ const originalMethod = descriptor.value!;
73
+
74
+ descriptor.value = async function (
75
+ this: unknown,
76
+ req: Request<Infer<T>>,
77
+ res: Response,
78
+ next: NextFunction
79
+ ) {
80
+ const { error, data } = await validateBody(schema, req.params);
81
+
82
+ if (!data || error.length) {
83
+ logger.error(error.join("\n"), { logType: "validationErrors" });
84
+ return res.status(422).json({ message: error.join("\n") });
85
+ }
86
+
87
+ req.params = data;
88
+
89
+ return originalMethod.call(this, req, res, next);
90
+ } as F;
91
+
92
+ return descriptor;
93
+ };
94
+ }
95
+
96
+ export function queryValidator<T extends SchemaTypes>(schema: T) {
97
+ return function <F extends ExpressHandler<unknown, unknown, unknown, Infer<T>>>(
98
+ _target: unknown,
99
+ _propertyKey: string,
100
+ descriptor: TypedPropertyDescriptor<F>
101
+ ) {
102
+ const originalMethod = descriptor.value!;
103
+
104
+ descriptor.value = async function (
105
+ this: unknown,
106
+ req: Request<unknown, unknown, unknown, Infer<T>>,
107
+ res: Response,
108
+ next: NextFunction
109
+ ) {
110
+ const { error, data } = await validateBody(schema, req.query);
111
+
112
+ if (!data || error.length) {
113
+ logger.error(error.join("\n"), { logType: "validationErrors" });
114
+ return res.status(422).json({ message: error.join("\n") });
115
+ }
116
+
117
+ req.query = data;
118
+
119
+ return originalMethod.call(this, req, res, next);
120
+ } as F;
121
+
122
+ return descriptor;
123
+ };
124
+ }
125
+ // export function authValidator({ optional = false }: { optional?: boolean } | undefined = {}) {
126
+ // return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
127
+ // const originalMethod = descriptor.value;
128
+ // descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
129
+ // try {
130
+ // const accessToken = req.headers["authorization"]?.split(" ")[1];
131
+ // if (optional && (!accessToken || !TokenModule.verifyAccessToken(accessToken))) {
132
+ // return originalMethod.apply(this, [req, res, next]);
133
+ // }
134
+ // if (!accessToken) {
135
+ // res.status(401).json({ message: "access token missing" });
136
+ // return;
137
+ // }
138
+ // const payload = TokenModule.verifyAccessToken(accessToken);
139
+
140
+ // if (!payload) {
141
+ // res.status(401).json({ message: "invalid access token" });
142
+ // return;
143
+ // }
144
+ // req.user = {
145
+ // id: payload.userID,
146
+ // email: payload.email
147
+ // };
148
+ // return originalMethod.apply(this, [req, res, next]);
149
+ // } catch (error) {
150
+ // next(error);
151
+ // }
152
+ // };
153
+ // };
154
+ // }
155
+
156
+ export function mediaBodyValidator<T extends SchemaTypes>(schema: T, optional = false) {
157
+ return function <F extends ExpressHandler<unknown, unknown, Infer<T>, unknown>>(
158
+ _target: unknown,
159
+ _propertyKey: string,
160
+ descriptor: TypedPropertyDescriptor<F>
161
+ ) {
162
+ const originalMethod = descriptor.value!;
163
+
164
+ descriptor.value = async function (
165
+ this: unknown,
166
+ req: Request<unknown, unknown, Infer<T>>,
167
+ res: Response,
168
+ next: NextFunction
169
+ ) {
170
+ const files = req.files;
171
+
172
+ if (!files && !optional) {
173
+ logger.error("Add at least one file", { logType: "validationErrors" });
174
+ return res.status(422).json({ message: "Add at least one file" });
175
+ }
176
+
177
+ const isJSON = (str: unknown): unknown => {
178
+ if (typeof str !== "string") return str;
179
+
180
+ try {
181
+ return JSON.parse(str);
182
+ } catch {
183
+ return str;
184
+ }
185
+ };
186
+
187
+ const parsedData = Object.fromEntries(
188
+ Object.entries(req.body).map(([k, v]) => [k, isJSON(v)])
189
+ );
190
+
191
+ const { error, data } = await validateBody(schema, parsedData);
192
+
193
+ if (!data || error.length) {
194
+ logger.error(error.join("\n"), { logType: "validationErrors" });
195
+ return res.status(422).json({ message: error.join("\n") });
196
+ }
197
+
198
+ req.body = data;
199
+ req.files = files;
200
+
201
+ return originalMethod.call(this, req, res, next);
202
+ } as F;
203
+
204
+ return descriptor;
205
+ };
206
+ }
207
+ export const validateId = vine.object({
208
+ id: vine.string().uuid(),
209
+ });
210
+
211
+ export const paginationValidator = vine.object({
212
+ page: vine
213
+ .number()
214
+ .positive()
215
+ .parse(e => (!e ? 1 : e))
216
+ .optional(),
217
+ limit: vine
218
+ .number()
219
+ .positive()
220
+ .parse(e => (!e ? 10 : e))
221
+ .optional(),
222
+ query: vine
223
+ .string()
224
+ .parse(e => (!e ? "" : e))
225
+ .optional(),
226
+ });
227
+ export async function validate<T extends SchemaTypes>(
228
+ schema: T,
229
+ payload: Infer<T>
230
+ ): Promise<Infer<T>> {
231
+ const result = await validateBody(schema, payload);
232
+
233
+ if (result.error.length) {
234
+ throw new ErrorResponse(result.error.join("\n"), 422);
235
+ }
236
+
237
+ if (result.data === undefined) {
238
+ throw new ErrorResponse("Validation failed", 422);
239
+ }
240
+
241
+ return result.data;
242
+ }
@@ -0,0 +1,4 @@
1
+ import { baseConfig } from "eslint-config/base";
2
+ import { extend } from "eslint-config/extend";
3
+
4
+ export default extend(baseConfig);
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@repo/logger",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "scripts": {
7
+ "clean": "rimraf dist .turbo",
8
+ "watch:dev": "pnpm watch",
9
+ "typecheck": "tsc --noEmit",
10
+ "format": "biome format --write .",
11
+ "format:check": "biome ci .",
12
+ "lint": "eslint .",
13
+ "lint:fix": "eslint . --fix",
14
+ "watch": "tsc -p tsconfig.build.json --watch"
15
+ },
16
+ "exports": "./src/index.ts",
17
+ "types": "./src/index.ts",
18
+
19
+ "devDependencies": {
20
+ "eslint-config": "workspace:*",
21
+ "typescript-config": "workspace:*",
22
+ "vitest-config": "workspace:*"
23
+ },
24
+ "dependencies": {
25
+ "winston": "catalog:",
26
+ "dotenv": "catalog:",
27
+ "@repo/env": "workspace:*"
28
+ },
29
+ "startx": {
30
+ "tags": [
31
+ "node"
32
+ ],
33
+ "requiredDeps": [
34
+ "@repo/env"
35
+ ],
36
+ "requiredDevDeps": [
37
+ "typescript-config"
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./logger.js";
2
+ export * from "./memory-profiler.js";
@@ -0,0 +1,72 @@
1
+ import { ENV } from "@repo/env";
2
+ import path from "path";
3
+ import util from "util";
4
+ import { createLogger, format, transports, addColors, type Logform } from "winston";
5
+ const customColors = {
6
+ error: "red",
7
+ warn: "yellow",
8
+ info: "green",
9
+ http: "magenta",
10
+ debug: "blue",
11
+ };
12
+ const upperCaseLevel = format(info => {
13
+ info.level = info.level.toUpperCase();
14
+ return info;
15
+ });
16
+
17
+ addColors(customColors);
18
+
19
+ const LOG_DIR = path.join(process.cwd(), "logs");
20
+
21
+ const customPrintFormat = format.printf((info: Logform.TransformableInfo) => {
22
+ const { level, message, timestamp, stack, ...metadata } = info;
23
+
24
+ const levelStr = String(level);
25
+ let messageStr = String(stack || message);
26
+
27
+ if (Object.keys(metadata).length > 0) {
28
+ const metaString = util.inspect(metadata, { depth: null, colors: false });
29
+ messageStr += `\nExtra Details:\n${metaString}`;
30
+ }
31
+
32
+ const dateStr = timestamp
33
+ ? new Date(String(timestamp as Date)).toLocaleString("tr-TR", {
34
+ year: "numeric",
35
+ month: "2-digit",
36
+ day: "2-digit",
37
+ hour: "2-digit",
38
+ minute: "2-digit",
39
+ })
40
+ : new Date().toISOString();
41
+
42
+ return `${dateStr} :${levelStr}: ${messageStr}`;
43
+ });
44
+
45
+ interface LoggerInput {
46
+ logName: string;
47
+ }
48
+
49
+ const createWLogger = ({ logName }: LoggerInput) => {
50
+ return createLogger({
51
+ level: ENV.LOG_LEVEL,
52
+ format: format.combine(format.timestamp(), format.errors({ stack: true })),
53
+ transports: [
54
+ new transports.Console({
55
+ format: format.combine(upperCaseLevel(), format.colorize({ all: true }), customPrintFormat),
56
+ }),
57
+ new transports.File({
58
+ level: "error",
59
+ filename: path.join(LOG_DIR, logName, `${logName}-Error.log`),
60
+ format: customPrintFormat,
61
+ }),
62
+ new transports.File({
63
+ filename: path.join(LOG_DIR, logName, `${logName}-Combined.log`),
64
+ format: customPrintFormat,
65
+ }),
66
+ ],
67
+ });
68
+ };
69
+
70
+ export const logger = createWLogger({
71
+ logName: "globalLog",
72
+ });