@kava/kava-api-core 1.0.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/bun.lock +160 -0
- package/dist/auth.util.d.ts +23 -0
- package/dist/auth.util.js +175 -0
- package/dist/auth.util.js.map +1 -0
- package/dist/context.util.d.ts +7 -0
- package/dist/context.util.js +11 -0
- package/dist/context.util.js.map +1 -0
- package/dist/controller.util.d.ts +118 -0
- package/dist/controller.util.js +144 -0
- package/dist/controller.util.js.map +1 -0
- package/dist/conversion.util.d.ts +8 -0
- package/dist/conversion.util.js +52 -0
- package/dist/conversion.util.js.map +1 -0
- package/dist/db.util.d.ts +80 -0
- package/dist/db.util.js +166 -0
- package/dist/db.util.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.util.d.ts +30 -0
- package/dist/logger.util.js +117 -0
- package/dist/logger.util.js.map +1 -0
- package/dist/mail.util.d.ts +21 -0
- package/dist/mail.util.js +53 -0
- package/dist/mail.util.js.map +1 -0
- package/dist/middleware.util.d.ts +263 -0
- package/dist/middleware.util.js +233 -0
- package/dist/middleware.util.js.map +1 -0
- package/dist/model.util.d.ts +204 -0
- package/dist/model.util.js +1495 -0
- package/dist/model.util.js.map +1 -0
- package/dist/permission.util.d.ts +38 -0
- package/dist/permission.util.js +91 -0
- package/dist/permission.util.js.map +1 -0
- package/dist/route.util.d.ts +1 -0
- package/dist/route.util.js +12 -0
- package/dist/route.util.js.map +1 -0
- package/dist/storage.util.d.ts +56 -0
- package/dist/storage.util.js +82 -0
- package/dist/storage.util.js.map +1 -0
- package/dist/validation.util.d.ts +7 -0
- package/dist/validation.util.js +237 -0
- package/dist/validation.util.js.map +1 -0
- package/package.json +34 -0
- package/src/auth.util.ts +242 -0
- package/src/context.util.ts +17 -0
- package/src/controller.util.ts +237 -0
- package/src/conversion.util.ts +65 -0
- package/src/db.util.ts +405 -0
- package/src/index.ts +13 -0
- package/src/logger.util.ts +170 -0
- package/src/mail.util.ts +86 -0
- package/src/middleware.util.ts +289 -0
- package/src/model.util.ts +2211 -0
- package/src/permission.util.ts +136 -0
- package/src/route.util.ts +12 -0
- package/src/storage.util.ts +102 -0
- package/src/validation.util.ts +338 -0
- package/tsconfig.json +23 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./auth.util";
|
|
2
|
+
export * from "./controller.util";
|
|
3
|
+
export * from "./conversion.util";
|
|
4
|
+
export * from "./context.util";
|
|
5
|
+
export * from "./db.util";
|
|
6
|
+
export * from "./middleware.util";
|
|
7
|
+
export * from "./model.util";
|
|
8
|
+
export * from "./permission.util";
|
|
9
|
+
export * from "./route.util";
|
|
10
|
+
export * from "./storage.util";
|
|
11
|
+
export * from "./validation.util";
|
|
12
|
+
export * from "./mail.util";
|
|
13
|
+
export * from "./logger.util";
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type LogType = "start" | "info" | "error" | "warning" | "cron" | "queue" | "queueError" | "cronError" | "socket" | "socketError";
|
|
7
|
+
|
|
8
|
+
export interface AccessLog {
|
|
9
|
+
method : string
|
|
10
|
+
path : string
|
|
11
|
+
status : number
|
|
12
|
+
latency : number
|
|
13
|
+
ip ?: string | null
|
|
14
|
+
agent ?: string | null
|
|
15
|
+
at ?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ErrorLog {
|
|
19
|
+
service ?: string
|
|
20
|
+
key ?: string
|
|
21
|
+
feature ?: string
|
|
22
|
+
error : string | null
|
|
23
|
+
reference ?: string | null
|
|
24
|
+
at ?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const colors: Record<LogType | "default", string> = {
|
|
31
|
+
default : "\x1b[0m", // default
|
|
32
|
+
start : "\x1b[32m", // green
|
|
33
|
+
info : "\x1b[36m", // cyan
|
|
34
|
+
error : "\x1b[31m", // red
|
|
35
|
+
warning : "\x1b[33m", // yellow
|
|
36
|
+
queue : "\x1b[34m", // blue
|
|
37
|
+
queueError : "\x1b[31m", // red
|
|
38
|
+
cron : "\x1b[35m", // magenta
|
|
39
|
+
cronError : "\x1b[31m", // red
|
|
40
|
+
socket : "\x1b[35m", // blue
|
|
41
|
+
socketError : "\x1b[31m", // red
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const prefixes: Record<LogType, string> = {
|
|
45
|
+
start : "START",
|
|
46
|
+
info : "INFO",
|
|
47
|
+
error : "ERROR",
|
|
48
|
+
warning : "WARNING",
|
|
49
|
+
cron : "CRON",
|
|
50
|
+
queue : "QUEUE",
|
|
51
|
+
socket : "SOCKET",
|
|
52
|
+
queueError : "QUEUE ERROR",
|
|
53
|
+
cronError : "CRON ERROR",
|
|
54
|
+
socketError : "SOCKET ERROR",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function log(type: LogType, msg: string) {
|
|
58
|
+
const color = colors[type];
|
|
59
|
+
const prefix = prefixes[type];
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.log(`${color}[${prefix}]${colors.default}`, msg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const logger = {
|
|
65
|
+
start : (msg: string) => log("start", msg),
|
|
66
|
+
info : (msg: string) => log("info", msg),
|
|
67
|
+
warning : (msg: string) => log("warning", msg),
|
|
68
|
+
queue : (msg: string) => log("queue", msg),
|
|
69
|
+
cron : (msg: string) => log("cron", msg),
|
|
70
|
+
socket : (msg: string) => log("socket", msg),
|
|
71
|
+
|
|
72
|
+
access : (msg: AccessLog) => logAccess(msg),
|
|
73
|
+
|
|
74
|
+
error: (msg: string, payload?: ErrorLog) => {
|
|
75
|
+
log("error", msg)
|
|
76
|
+
payload && logError({...payload, service: payload.service || 'app'})
|
|
77
|
+
},
|
|
78
|
+
queueError: (msg: string, payload?: ErrorLog) => {
|
|
79
|
+
log("queueError", msg)
|
|
80
|
+
payload && logError({...payload, service: payload.service || 'queue'})
|
|
81
|
+
},
|
|
82
|
+
cronError: (msg: string, payload?: ErrorLog) => {
|
|
83
|
+
log("cronError", msg)
|
|
84
|
+
payload && logError({...payload, service: payload.service || 'cron'})
|
|
85
|
+
},
|
|
86
|
+
socketError: (msg: string, payload?: ErrorLog) => {
|
|
87
|
+
log("socketError", msg)
|
|
88
|
+
payload && logError({...payload, service: payload.service || 'socket'})
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
type DriverName = "file" | "da"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
const ACCESS_LOG_DRIVER = process.env.ACCESS_LOG_DRIVER || "file"
|
|
100
|
+
const ACCESS_LOG_LOG_DIR = process.env.ACCESS_LOG_DIR || "storage/logs/access"
|
|
101
|
+
const ACCESS_LOG_QUEUE = process.env.ACCESS_LOG_QUEUE || "access-log"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
const ERROR_LOG_DRIVER = process.env.ERROR_LOG_DRIVER || "file"
|
|
105
|
+
const ERROR_LOG_LOG_DIR = process.env.ERROR_LOG_DIR || "storage/logs/error"
|
|
106
|
+
const ERROR_LOG_QUEUE = process.env.ERROR_LOG_QUEUE_PREFIX || "error-log"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// =====================
|
|
111
|
+
// ## Access Log Drivers
|
|
112
|
+
// =====================
|
|
113
|
+
const filePath = () => {
|
|
114
|
+
const d = new Date().toISOString().slice(0, 10)
|
|
115
|
+
return path.resolve( ACCESS_LOG_LOG_DIR, `access-${d}.log`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handlers: Record<DriverName, (log: AccessLog) => Promise<void>> = {
|
|
119
|
+
file: async (log) => {
|
|
120
|
+
const dir = path.resolve(ACCESS_LOG_LOG_DIR);
|
|
121
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
fs.appendFile(filePath(), JSON.stringify(log) + "\n", () => {})
|
|
124
|
+
},
|
|
125
|
+
da: async (log) => {
|
|
126
|
+
// try {
|
|
127
|
+
// await queue.add(ACCESS_LOG_QUEUE, log)
|
|
128
|
+
// } catch {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const activeDrivers: DriverName[] = (ACCESS_LOG_DRIVER).split(",").map(v => v.trim()).filter((v): v is DriverName => v in handlers)
|
|
133
|
+
|
|
134
|
+
function logAccess(payload: AccessLog) {
|
|
135
|
+
for (const d of activeDrivers) {
|
|
136
|
+
handlers[d](payload)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
// =====================
|
|
143
|
+
// ## Error Log Drivers
|
|
144
|
+
// =====================
|
|
145
|
+
const errorFilePath = () => {
|
|
146
|
+
const d = new Date().toISOString().slice(0, 10)
|
|
147
|
+
return path.resolve(ERROR_LOG_LOG_DIR, `error-${d}.log`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const errorHandlers: Record<DriverName, (log: ErrorLog) => Promise<void>> = {
|
|
151
|
+
file: async (log) => {
|
|
152
|
+
const dir = path.resolve(ERROR_LOG_LOG_DIR);
|
|
153
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
154
|
+
|
|
155
|
+
fs.appendFile(errorFilePath(), JSON.stringify(log) + "\n", () => {});
|
|
156
|
+
},
|
|
157
|
+
da: async (log) => {
|
|
158
|
+
// try {
|
|
159
|
+
// await queue.add(ERROR_LOG_QUEUE, log)
|
|
160
|
+
// } catch {}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const activeErrorDrivers: DriverName[] = ERROR_LOG_DRIVER.split(",").map(v => v.trim()).filter((v): v is DriverName => v in errorHandlers)
|
|
165
|
+
|
|
166
|
+
function logError(payload: ErrorLog) {
|
|
167
|
+
for (const d of activeErrorDrivers) {
|
|
168
|
+
errorHandlers[d](payload)
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/mail.util.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import nodemailer, { SentMessageInfo } from "nodemailer";
|
|
4
|
+
import { logger } from "@utils";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface SendMailOptions {
|
|
9
|
+
to : string;
|
|
10
|
+
subject : string;
|
|
11
|
+
content ?: string;
|
|
12
|
+
text ?: string;
|
|
13
|
+
attachments ?: {
|
|
14
|
+
filename : string;
|
|
15
|
+
path : string;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// =============================>
|
|
22
|
+
// ## Mail: Send mail
|
|
23
|
+
// =============================>
|
|
24
|
+
export async function sendMail(options: {
|
|
25
|
+
to : string;
|
|
26
|
+
subject : string;
|
|
27
|
+
text ?: string;
|
|
28
|
+
content ?: string;
|
|
29
|
+
attachments ?: { filename: string; path: string }[];
|
|
30
|
+
}) {
|
|
31
|
+
const transporter = nodemailer.createTransport({
|
|
32
|
+
host : process.env.MAIL_HOST,
|
|
33
|
+
port : Number(process.env.MAIL_PORT),
|
|
34
|
+
secure : Number(process.env.MAIL_PORT) === 465,
|
|
35
|
+
auth : {
|
|
36
|
+
user : process.env.MAIL_USERNAME,
|
|
37
|
+
pass : process.env.MAIL_PASSWORD,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const info = (await transporter.sendMail({
|
|
42
|
+
from : `${process.env.MAIL_FROM_NAME || process.env.APP_NAME} <${process.env.MAIL_FROM_ADDRESS || process.env.MAIL_USERNAME}>`,
|
|
43
|
+
to : options.to,
|
|
44
|
+
subject : options.subject,
|
|
45
|
+
text : options.text,
|
|
46
|
+
html : options.content,
|
|
47
|
+
attachments : options.attachments,
|
|
48
|
+
})) as SentMessageInfo;
|
|
49
|
+
|
|
50
|
+
logger.info(`Email sent successfully: ${info.messageId}`)
|
|
51
|
+
return info;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// =============================>
|
|
57
|
+
// ## Mail: Render mail template
|
|
58
|
+
// =============================>
|
|
59
|
+
export function renderMailTemplate(template: string, options: Record<string, string>) {
|
|
60
|
+
const templateDir = join(import.meta.dir, "./../outputs/mails/templates");
|
|
61
|
+
|
|
62
|
+
const contentPath = join(templateDir, `${template}.mail.stub`);
|
|
63
|
+
let content = readFileSync(contentPath, "utf-8");
|
|
64
|
+
|
|
65
|
+
for (const [key, value] of Object.entries(options)) {
|
|
66
|
+
const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
|
|
67
|
+
content = content.replace(regex, value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let layout = readFileSync(join(templateDir, "layout.mail.stub"), "utf-8");
|
|
71
|
+
|
|
72
|
+
const globalVars = {
|
|
73
|
+
...options,
|
|
74
|
+
date : "20-10-2025",
|
|
75
|
+
app_name : process.env.APP_NAME || "",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (const [key, value] of Object.entries(globalVars)) {
|
|
79
|
+
const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
|
|
80
|
+
layout = layout.replace(regex, value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
layout = layout.replace("{{content}}", content);
|
|
84
|
+
|
|
85
|
+
return layout;
|
|
86
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Elysia, status } from 'elysia'
|
|
2
|
+
import { auth, context, logger } from '@utils'
|
|
3
|
+
|
|
4
|
+
declare module "elysia" {
|
|
5
|
+
interface Elysia {
|
|
6
|
+
api(
|
|
7
|
+
basePath: string,
|
|
8
|
+
controller: {
|
|
9
|
+
index ?: any
|
|
10
|
+
store ?: any
|
|
11
|
+
show ?: any
|
|
12
|
+
update ?: any
|
|
13
|
+
destroy ?: any
|
|
14
|
+
}
|
|
15
|
+
): this
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const errors = {
|
|
21
|
+
unauthorized: {
|
|
22
|
+
status: 401,
|
|
23
|
+
message: "Unauthorized!"
|
|
24
|
+
},
|
|
25
|
+
ratelimited: {
|
|
26
|
+
status: 429,
|
|
27
|
+
message: "Too many requests!"
|
|
28
|
+
},
|
|
29
|
+
notfound: {
|
|
30
|
+
status: 404,
|
|
31
|
+
message: "Endpoint not found!"
|
|
32
|
+
},
|
|
33
|
+
request: {
|
|
34
|
+
status: 400,
|
|
35
|
+
message: "Bad Request!"
|
|
36
|
+
},
|
|
37
|
+
error: {
|
|
38
|
+
status: 500,
|
|
39
|
+
message: "Endpoint not found!"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
export const middleware = {
|
|
45
|
+
// =============================>
|
|
46
|
+
// ## Middleware: Auth hand;er
|
|
47
|
+
// =============================>
|
|
48
|
+
Auth: (app: Elysia) => app.derive(async ({ request }) => {
|
|
49
|
+
const authHeader = request.headers.get('authorization')
|
|
50
|
+
|
|
51
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) return { user: null, permissions: [], token: null }
|
|
52
|
+
|
|
53
|
+
const bearer = authHeader.substring(7).trim()
|
|
54
|
+
const result = await auth.verifyAccessToken(bearer, request)
|
|
55
|
+
|
|
56
|
+
if (!result) return { user: null, permissions: [], token: null };
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
user: result.user,
|
|
60
|
+
permissions: result.permissions,
|
|
61
|
+
token: result.token,
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// =============================>
|
|
67
|
+
// ## Middleware: Private handler
|
|
68
|
+
// =============================>
|
|
69
|
+
Private: (app: Elysia) => app.derive(async ({ user }: Record<string, any> | any) => {
|
|
70
|
+
if (!user) {
|
|
71
|
+
throw status(errors.unauthorized.status, { message: errors.unauthorized.message })
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
// =============================>
|
|
77
|
+
// ## Middleware: Cors handler
|
|
78
|
+
// =============================>
|
|
79
|
+
Cors: (app: Elysia) => app.onRequest(({ request, set }) => {
|
|
80
|
+
const origin = request.headers.get('origin') ?? ''
|
|
81
|
+
let allowedOrigin: string = '*'
|
|
82
|
+
|
|
83
|
+
const originsConf = process.env.APP_CORS_ORIGINS || '*'
|
|
84
|
+
|
|
85
|
+
if (originsConf !== '*') {
|
|
86
|
+
try {
|
|
87
|
+
const allowedOrigins = JSON.parse(originsConf)
|
|
88
|
+
if (Array.isArray(allowedOrigins) && allowedOrigins.includes(origin)) {
|
|
89
|
+
allowedOrigin = origin || ""
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
const em = 'Cors Error: Failed to parse APP_CORS_ORIGINS, fallback to "*"'
|
|
93
|
+
logger.error(em, { error: em })
|
|
94
|
+
allowedOrigin = ''
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
set.headers['Access-Control-Allow-Origin'] = allowedOrigin
|
|
99
|
+
set.headers['Access-Control-Allow-Methods'] = process.env.APP_CORS_METHODS || 'GET, POST, PUT, DELETE, OPTIONS'
|
|
100
|
+
set.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Option, x-App'
|
|
101
|
+
set.headers['Access-Control-Allow-Credentials'] = 'true'
|
|
102
|
+
|
|
103
|
+
if (request.method === 'OPTIONS') {
|
|
104
|
+
return new Response(null, { status: 204, })
|
|
105
|
+
}
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// =============================>
|
|
110
|
+
// ## Middleware: Rate limiter handler
|
|
111
|
+
// =============================>
|
|
112
|
+
RateLimiter: (app: Elysia, options?: { windowMs?: number, max?: number }) => app.onRequest(({ request, set, store }) => {
|
|
113
|
+
const max = options?.max || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATE_LIMIT) : 60 )
|
|
114
|
+
const windowMs = options?.windowMs || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATELIMIT_COUNTDOWN) : 60_000 )
|
|
115
|
+
|
|
116
|
+
const user = (store as any)?.user
|
|
117
|
+
const key = getClientKey(request, user?.id)
|
|
118
|
+
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
let record = rateLimitStore.get(key)
|
|
121
|
+
|
|
122
|
+
if (!record || record.expiresAt < now) {
|
|
123
|
+
record = { count: 1, expiresAt: now + windowMs }
|
|
124
|
+
rateLimitStore.set(key, record)
|
|
125
|
+
} else {
|
|
126
|
+
record.count++
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
set.headers['X-RateLimit-Limit'] = String(max)
|
|
130
|
+
set.headers['X-RateLimit-Remaining'] = String(Math.max(0, max - record.count))
|
|
131
|
+
set.headers['X-RateLimit-Reset'] = String(record.expiresAt)
|
|
132
|
+
|
|
133
|
+
if (record.count > max) throw status(errors.ratelimited.status, { message: errors.ratelimited.message });
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// =============================>
|
|
138
|
+
// ## Middleware: Body parse handler
|
|
139
|
+
// =============================>
|
|
140
|
+
BodyParse: (app: Elysia) => app.state<{ rawBody?: any }>({}).onRequest(async ({ request, store }) => {
|
|
141
|
+
const text = await request.clone().text();
|
|
142
|
+
|
|
143
|
+
const contentType = request.headers.get("content-type") || "";
|
|
144
|
+
let rawBody: any = {};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (contentType.includes("application/json")) {
|
|
148
|
+
rawBody = text ? JSON.parse(text) : {};
|
|
149
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
150
|
+
const params = new URLSearchParams(text);
|
|
151
|
+
for (const [key, value] of params.entries()) bodyParseNestedSet(rawBody, key, value);
|
|
152
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
153
|
+
const formData = await request.clone().formData();
|
|
154
|
+
for (const [key, value] of formData.entries()) bodyParseNestedSet(rawBody, key, value);
|
|
155
|
+
} else {
|
|
156
|
+
rawBody = {};
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
const em = e instanceof Error ? e.message : String(e)
|
|
160
|
+
logger.error(`Body parse error: ${em}`, { error: em })
|
|
161
|
+
rawBody = {};
|
|
162
|
+
throw status(errors.request.status, { message: errors.request.message })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
store.rawBody = rawBody;
|
|
166
|
+
}).derive(({ store }) => {
|
|
167
|
+
const payload = bodyParseKeyFormat(store.rawBody || {});
|
|
168
|
+
return { payload };
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
AccessLog: (app: Elysia) => app.state<{ startedAt?: number }>({}).onRequest(({ store }) => { store.startedAt = Date.now() }).onAfterResponse(({ request, set, store }) => {
|
|
173
|
+
const method = request.method
|
|
174
|
+
const url = new URL(request.url)
|
|
175
|
+
const path = url.pathname
|
|
176
|
+
const status = Number(set.status) ?? 200
|
|
177
|
+
const latency = Date.now() - (store.startedAt ?? Date.now())
|
|
178
|
+
const agent = request.headers.get("user-agent") || 'unknown'
|
|
179
|
+
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
|
|
180
|
+
|
|
181
|
+
logger.info(`${method} : ${path} - ${status} - ${latency}ms - ${ip}]`)
|
|
182
|
+
logger.access({ method, path, status, latency, ip, agent })
|
|
183
|
+
}),
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
// =============================>
|
|
187
|
+
// ## Middleware: Error handler
|
|
188
|
+
// =============================>
|
|
189
|
+
ErrorHandler: (app: Elysia) => app.onError(({ code, set, error, request }) => {
|
|
190
|
+
if (code === 'NOT_FOUND') {
|
|
191
|
+
set.status = errors.notfound.status
|
|
192
|
+
return { message: errors.notfound.message }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (code === 'INTERNAL_SERVER_ERROR') {
|
|
196
|
+
set.status = errors.error.status
|
|
197
|
+
const em = error.message
|
|
198
|
+
const url = new URL(request.url)
|
|
199
|
+
const path = url.pathname
|
|
200
|
+
|
|
201
|
+
logger.error(`error: ${em}`, { error: em, reference: path })
|
|
202
|
+
return { message: em }
|
|
203
|
+
}
|
|
204
|
+
}),
|
|
205
|
+
|
|
206
|
+
Context: (app: Elysia) => app.derive(async ({ store }) => {
|
|
207
|
+
const userId = (store as any)?.user?.id
|
|
208
|
+
|
|
209
|
+
return context.run({
|
|
210
|
+
user_id: userId,
|
|
211
|
+
},() => ({})
|
|
212
|
+
)
|
|
213
|
+
}),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
// =============================>
|
|
219
|
+
// ## Middleware: Body parse helpers
|
|
220
|
+
// =============================>
|
|
221
|
+
function bodyParseKeyFormat(input: any): any {
|
|
222
|
+
if ( typeof input !== "object" || input === null || input instanceof File ) return input;
|
|
223
|
+
|
|
224
|
+
if (Array.isArray(input)) return input.map(bodyParseKeyFormat)
|
|
225
|
+
|
|
226
|
+
const result: any = {}
|
|
227
|
+
for (const [key, value] of Object.entries(input)) {
|
|
228
|
+
if (key.includes(".") || key.includes("[")) {
|
|
229
|
+
bodyParseNestedSet(result, key, bodyParseKeyFormat(value))
|
|
230
|
+
} else {
|
|
231
|
+
result[key] = bodyParseKeyFormat(value)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
function bodyParseNestedSet(obj: any, path: string, value: any) {
|
|
239
|
+
const parts = bodyParsePathFormat(path);
|
|
240
|
+
let current = obj;
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < parts.length; i++) {
|
|
243
|
+
const key = parts[i];
|
|
244
|
+
const isLast = i === parts.length - 1;
|
|
245
|
+
|
|
246
|
+
if (isLast) {
|
|
247
|
+
current[key] = bodyParseValueFormat(value);
|
|
248
|
+
} else {
|
|
249
|
+
if (!(key in current)) {
|
|
250
|
+
const nextKey = parts[i + 1];
|
|
251
|
+
current[key] = isNaN(Number(nextKey)) ? {} : [];
|
|
252
|
+
}
|
|
253
|
+
current = current[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function bodyParsePathFormat(path: string): string[] {
|
|
259
|
+
return path.replace(/\[(\w+)\]/g, ".$1").replace(/^\./, "").split(".");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function bodyParseValueFormat(value: any) {
|
|
263
|
+
if (value == "" || value == null || value == "null") return null;
|
|
264
|
+
if (typeof value !== "string") return value;
|
|
265
|
+
if (value === "true") return true;
|
|
266
|
+
if (value === "false") return false;
|
|
267
|
+
if (!isNaN(Number(value))) return Number(value);
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
// =============================>
|
|
274
|
+
// ## Middleware: Rate Limiter Helpers
|
|
275
|
+
// =============================>
|
|
276
|
+
type RateLimitRecord = {
|
|
277
|
+
count: number
|
|
278
|
+
expiresAt: number
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const rateLimitStore = new Map<string, RateLimitRecord>()
|
|
282
|
+
|
|
283
|
+
function getClientKey(request: Request, userId?: string | number) {
|
|
284
|
+
if (userId) return `user:${userId}`
|
|
285
|
+
|
|
286
|
+
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
|
|
287
|
+
|
|
288
|
+
return `ip:${ip}`
|
|
289
|
+
}
|