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.
- package/.prettierignore +0 -13
- package/.prettierrc.js +52 -52
- package/.vscode/launch.json +32 -0
- package/.vscode/settings.json +9 -3
- package/apps/core-server/.env.example +18 -24
- package/apps/core-server/Dockerfile +35 -61
- package/apps/core-server/eslint.config.ts +7 -0
- package/apps/core-server/package.json +41 -52
- package/apps/core-server/src/config/custom-type.ts +2 -40
- package/apps/core-server/src/events/index.ts +37 -37
- package/apps/core-server/src/index.ts +4 -13
- package/apps/core-server/src/middlewares/auth-middleware.ts +24 -7
- package/apps/core-server/src/middlewares/cors-middleware.ts +7 -6
- package/apps/core-server/src/middlewares/error-middleware.ts +7 -4
- package/apps/core-server/src/middlewares/logger-middleware.ts +81 -21
- package/apps/core-server/src/middlewares/notfound-middleware.ts +6 -14
- package/apps/core-server/src/middlewares/serve-static.ts +30 -24
- package/apps/core-server/src/routes/files/router.ts +9 -7
- package/apps/core-server/src/routes/server.ts +30 -36
- package/apps/core-server/tsdown.config.ts +4 -3
- package/biome.json +58 -60
- package/configs/eslint-config/package.json +16 -19
- package/configs/eslint-config/src/configs/base.ts +185 -225
- package/configs/eslint-config/src/configs/extend.ts +3 -0
- package/configs/eslint-config/src/configs/frontend.ts +81 -56
- package/configs/eslint-config/src/configs/node.ts +6 -6
- package/configs/eslint-config/src/plugin.ts +1 -0
- package/configs/eslint-config/src/rules/index.ts +8 -12
- package/configs/eslint-config/src/rules/no-json-parse-json-stringify.test.ts +30 -17
- package/configs/eslint-config/src/rules/no-json-parse-json-stringify.ts +52 -49
- package/configs/eslint-config/src/rules/no-uncaught-json-parse.ts +43 -45
- package/configs/tsdown-config/package.json +10 -3
- package/configs/typescript-config/package.json +10 -1
- package/configs/typescript-config/tsconfig.common.json +3 -3
- package/configs/vitest-config/dist/base.mjs +1 -0
- package/configs/vitest-config/dist/frontend.mjs +1 -0
- package/configs/vitest-config/dist/node.mjs +1 -0
- package/configs/vitest-config/package.json +12 -0
- package/configs/vitest-config/src/base.ts +17 -29
- package/configs/vitest-config/src/index.ts +1 -0
- package/package.json +21 -13
- package/packages/@repo/constants/eslint.config.ts +4 -0
- package/packages/@repo/constants/package.json +16 -0
- package/packages/@repo/constants/src/index.ts +8 -8
- package/packages/@repo/db/eslint.config.ts +4 -0
- package/packages/@repo/db/package.json +16 -8
- package/packages/@repo/db/src/index.ts +26 -20
- package/packages/@repo/db/src/schema/common.ts +45 -49
- package/packages/@repo/env/eslint.config.ts +4 -0
- package/packages/@repo/env/package.json +39 -0
- package/packages/@repo/env/src/default-env.ts +12 -0
- package/packages/@repo/env/src/define-env.ts +70 -0
- package/packages/@repo/env/src/index.ts +2 -0
- package/packages/@repo/env/src/utils.ts +52 -0
- package/packages/@repo/env/tsconfig.json +7 -0
- package/packages/@repo/lib/eslint.config.ts +4 -0
- package/packages/@repo/lib/package.json +34 -34
- package/packages/@repo/lib/src/bucket-module/file-storage.ts +50 -49
- package/packages/@repo/lib/src/bucket-module/index.ts +3 -0
- package/packages/@repo/lib/src/bucket-module/s3-storage.ts +120 -114
- package/packages/@repo/lib/src/bucket-module/utils.ts +10 -11
- package/packages/@repo/lib/src/{cookie-module.ts → cookie-module/cookie-module.ts} +48 -42
- package/packages/@repo/lib/src/cookie-module/index.ts +1 -0
- package/packages/@repo/lib/src/extra/index.ts +1 -0
- package/packages/@repo/lib/src/extra/pagination-module.ts +35 -0
- package/packages/@repo/lib/src/{token-module.ts → extra/token-module.ts} +12 -5
- package/packages/@repo/lib/src/file-system-module/index.ts +170 -0
- package/packages/@repo/lib/src/{hashing-module.ts → hashing-module/index.ts} +9 -9
- package/packages/@repo/lib/src/index.ts +0 -26
- package/packages/@repo/lib/src/mail-module/index.ts +2 -0
- package/packages/@repo/lib/src/mail-module/mock.ts +8 -8
- package/packages/@repo/lib/src/mail-module/nodemailer.ts +17 -7
- package/packages/@repo/lib/src/notification-module/index.ts +1 -172
- package/packages/@repo/lib/src/notification-module/push-notification.ts +97 -90
- package/packages/@repo/lib/src/{oauth2-client.ts → oauth2-module/index.ts} +107 -109
- package/packages/@repo/lib/src/otp-module/index.ts +91 -0
- package/packages/@repo/lib/src/session-module/index.ts +113 -0
- package/packages/@repo/lib/src/utils.ts +43 -42
- package/packages/@repo/lib/src/validation-module/index.ts +242 -0
- package/packages/@repo/logger/eslint.config.ts +4 -0
- package/packages/@repo/logger/package.json +40 -0
- package/packages/@repo/logger/src/index.ts +2 -0
- package/packages/@repo/logger/src/logger.ts +72 -0
- package/packages/@repo/{lib/src/logger-module → logger/src}/memory-profiler.ts +64 -65
- package/packages/@repo/logger/tsconfig.json +7 -0
- package/packages/@repo/mail/eslint.config.ts +4 -0
- package/packages/@repo/mail/package.json +10 -3
- package/packages/@repo/mail/src/emails/admin/OtpEmail.tsx +169 -168
- package/packages/@repo/mail/src/index.ts +1 -2
- package/packages/@repo/mail/tsconfig.json +3 -3
- package/packages/@repo/redis/dist/index.d.mts +3 -0
- package/packages/@repo/redis/dist/index.mjs +5 -0
- package/packages/@repo/redis/dist/lib/redis-client.d.mts +7 -0
- package/packages/@repo/redis/dist/lib/redis-client.mjs +25 -0
- package/packages/@repo/redis/dist/lib/redis-client.mjs.map +1 -0
- package/packages/@repo/redis/dist/lib/redis-module.d.mts +5 -0
- package/packages/@repo/redis/dist/lib/redis-module.mjs +6 -0
- package/packages/@repo/redis/dist/lib/redis-module.mjs.map +1 -0
- package/packages/@repo/redis/eslint.config.ts +4 -0
- package/packages/@repo/redis/package.json +13 -10
- package/packages/@repo/redis/src/index.ts +2 -2
- package/packages/@repo/redis/src/lib/redis-client.ts +36 -23
- package/packages/@repo/redis/src/lib/redis-module.ts +69 -3
- package/packages/cli/dist/index.mjs +203 -0
- package/packages/cli/eslint.config.ts +4 -0
- package/packages/cli/package.json +44 -0
- package/packages/cli/tsconfig.json +12 -0
- package/packages/cli/tsdown.config.ts +17 -0
- package/packages/ui/components.json +0 -1
- package/packages/ui/eslint.config.ts +4 -0
- package/packages/ui/package.json +16 -3
- package/packages/ui/postcss.config.mjs +9 -9
- package/packages/ui/src/components/lib/utils.ts +53 -53
- package/packages/ui/src/components/ui/alert-dialog.tsx +118 -116
- package/packages/ui/src/components/ui/avatar.tsx +52 -53
- package/packages/ui/src/components/ui/badge.tsx +45 -46
- package/packages/ui/src/components/ui/breadcrumb.tsx +108 -109
- package/packages/ui/src/components/ui/card.tsx +91 -92
- package/packages/ui/src/components/ui/carousel.tsx +243 -243
- package/packages/ui/src/components/ui/checkbox.tsx +32 -32
- package/packages/ui/src/components/ui/command.tsx +144 -155
- package/packages/ui/src/components/ui/dialog.tsx +124 -127
- package/packages/ui/src/components/ui/form.tsx +166 -165
- package/packages/ui/src/components/ui/input-otp.tsx +74 -76
- package/packages/ui/src/components/ui/input.tsx +19 -21
- package/packages/ui/src/components/ui/multiple-select.tsx +4 -4
- package/packages/ui/src/{components/lucide.tsx → lucide.ts} +3 -3
- package/packages/ui/tailwind.config.ts +94 -94
- package/packages/ui/tsconfig.json +7 -1
- package/pnpm-workspace.yaml +41 -1
- package/turbo.json +20 -27
- package/apps/core-server/eslint.config.mjs +0 -47
- package/configs/eslint-config/src/rules/no-dynamic-import-template.ts +0 -32
- package/configs/eslint-config/src/rules/no-plain-errors.ts +0 -50
- package/configs/eslint-config/tsdown.config.ts +0 -11
- package/packages/@repo/constants/eslint.config.mjs +0 -21
- package/packages/@repo/db/eslint.config.mjs +0 -21
- package/packages/@repo/lib/eslint.config.mjs +0 -49
- package/packages/@repo/lib/src/command-module.ts +0 -77
- package/packages/@repo/lib/src/constants.ts +0 -3
- package/packages/@repo/lib/src/custom-type.ts +0 -54
- package/packages/@repo/lib/src/env.ts +0 -13
- package/packages/@repo/lib/src/file-system/index.ts +0 -90
- package/packages/@repo/lib/src/logger-module/log-config.ts +0 -16
- package/packages/@repo/lib/src/logger-module/logger.ts +0 -78
- package/packages/@repo/lib/src/mail-module/api.ts +0 -0
- package/packages/@repo/lib/src/otp-module.ts +0 -98
- package/packages/@repo/lib/src/pagination-module.ts +0 -49
- package/packages/@repo/lib/src/user-session.ts +0 -117
- package/packages/@repo/lib/src/validation-module.ts +0 -187
- package/packages/@repo/mail/tsconfig.build.json +0 -14
- package/packages/@repo/mail/tsdown.config.ts +0 -9
- package/packages/@repo/redis/eslint.config.mjs +0 -8
- 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
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* @
|
|
25
|
-
* @param
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* @
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,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,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
|
+
});
|