@kava/kava-api-core 1.0.0 → 1.0.2
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/.github/workflows/publish.yml +40 -0
- package/dist/{auth.util.d.ts → auth/auth.d.ts} +1 -5
- package/dist/{auth.util.js → auth/auth.js} +38 -30
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/{context.util.js → context/context.js} +1 -1
- package/dist/context/context.js.map +1 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{controller.util.js → controller/controller.js} +1 -1
- package/dist/controller/controller.js.map +1 -0
- package/dist/controller/index.d.ts +1 -0
- package/dist/controller/index.js +2 -0
- package/dist/controller/index.js.map +1 -0
- package/dist/{conversion.util.js → conversion/conversion.js} +1 -1
- package/dist/conversion/conversion.js.map +1 -0
- package/dist/conversion/index.d.ts +1 -0
- package/dist/conversion/index.js +2 -0
- package/dist/conversion/index.js.map +1 -0
- package/dist/{db.util.d.ts → db/db.d.ts} +9 -5
- package/dist/{db.util.js → db/db.js} +40 -29
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +14 -13
- package/dist/index.js.map +1 -1
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.js +2 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/{logger.util.js → logger/logger.js} +16 -7
- package/dist/logger/logger.js.map +1 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/{mail.util.js → mail/mail.js} +1 -1
- package/dist/mail/mail.js.map +1 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/{middleware.util.js → middleware/middleware.js} +1 -1
- package/dist/middleware/middleware.js.map +1 -0
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +2 -0
- package/dist/model/index.js.map +1 -0
- package/dist/{model.util.js → model/model.js} +1 -1
- package/dist/model/model.js.map +1 -0
- package/dist/permission/index.d.ts +1 -0
- package/dist/permission/index.js +2 -0
- package/dist/permission/index.js.map +1 -0
- package/dist/{permission.util.js → permission/permission.js} +1 -1
- package/dist/permission/permission.js.map +1 -0
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/registry.d.ts +28 -0
- package/dist/registry/registry.js +19 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/route/index.d.ts +1 -0
- package/dist/route/index.js +2 -0
- package/dist/route/index.js.map +1 -0
- package/dist/{route.util.js → route/route.js} +1 -1
- package/dist/route/route.js.map +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/{storage.util.js → storage/storage.js} +2 -2
- package/dist/storage/storage.js.map +1 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/{validation.util.js → validation/validation.js} +1 -1
- package/dist/validation/validation.js.map +1 -0
- package/package.json +2 -2
- package/src/{auth.util.ts → auth/auth.ts} +255 -241
- package/src/auth/index.ts +1 -0
- package/src/{context.util.ts → context/context.ts} +17 -17
- package/src/context/index.ts +1 -0
- package/src/{controller.util.ts → controller/controller.ts} +236 -236
- package/src/controller/index.ts +1 -0
- package/src/{conversion.util.ts → conversion/conversion.ts} +64 -64
- package/src/conversion/index.ts +1 -0
- package/src/{db.util.ts → db/db.ts} +420 -405
- package/src/db/index.ts +1 -0
- package/src/index.ts +14 -13
- package/src/logger/index.ts +1 -0
- package/src/{logger.util.ts → logger/logger.ts} +176 -169
- package/src/mail/index.ts +1 -0
- package/src/{mail.util.ts → mail/mail.ts} +85 -85
- package/src/middleware/index.ts +1 -0
- package/src/{middleware.util.ts → middleware/middleware.ts} +288 -288
- package/src/model/index.ts +1 -0
- package/src/{model.util.ts → model/model.ts} +2210 -2210
- package/src/permission/index.ts +1 -0
- package/src/{permission.util.ts → permission/permission.ts} +136 -136
- package/src/registry/index.ts +1 -0
- package/src/registry/registry.ts +37 -0
- package/src/route/index.ts +1 -0
- package/src/{route.util.ts → route/route.ts} +11 -11
- package/src/storage/index.ts +1 -0
- package/src/{storage.util.ts → storage/storage.ts} +101 -101
- package/src/validation/index.ts +1 -0
- package/src/{validation.util.ts → validation/validation.ts} +338 -338
- package/tsconfig.json +1 -1
- package/bun.lock +0 -160
- package/dist/auth.util.js.map +0 -1
- package/dist/context.util.js.map +0 -1
- package/dist/controller.util.js.map +0 -1
- package/dist/conversion.util.js.map +0 -1
- package/dist/db.util.js.map +0 -1
- package/dist/logger.util.js.map +0 -1
- package/dist/mail.util.js.map +0 -1
- package/dist/middleware.util.js.map +0 -1
- package/dist/model.util.js.map +0 -1
- package/dist/permission.util.js.map +0 -1
- package/dist/route.util.js.map +0 -1
- package/dist/storage.util.js.map +0 -1
- package/dist/validation.util.js.map +0 -1
- /package/dist/{context.util.d.ts → context/context.d.ts} +0 -0
- /package/dist/{controller.util.d.ts → controller/controller.d.ts} +0 -0
- /package/dist/{conversion.util.d.ts → conversion/conversion.d.ts} +0 -0
- /package/dist/{logger.util.d.ts → logger/logger.d.ts} +0 -0
- /package/dist/{mail.util.d.ts → mail/mail.d.ts} +0 -0
- /package/dist/{middleware.util.d.ts → middleware/middleware.d.ts} +0 -0
- /package/dist/{model.util.d.ts → model/model.d.ts} +0 -0
- /package/dist/{permission.util.d.ts → permission/permission.d.ts} +0 -0
- /package/dist/{route.util.d.ts → route/route.d.ts} +0 -0
- /package/dist/{storage.util.d.ts → storage/storage.d.ts} +0 -0
- /package/dist/{validation.util.d.ts → validation/validation.d.ts} +0 -0
|
@@ -1,289 +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}`
|
|
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
289
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./model";
|