odac 1.0.1 → 1.2.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/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +33 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/auto-pr-description.yml +3 -1
- package/.github/workflows/release.yml +42 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.github/workflows/test-publish.yml +36 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +3 -3
- package/CHANGELOG.md +184 -0
- package/README.md +53 -34
- package/bin/odac.js +181 -49
- package/client/odac.js +878 -995
- package/docs/backend/01-overview/03-development-server.md +39 -46
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
- package/docs/backend/03-config/00-configuration-overview.md +15 -6
- package/docs/backend/03-config/01-database-connection.md +3 -3
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/04-routing/09-websocket.md +29 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
- package/docs/backend/05-controllers/03-controller-classes.md +61 -55
- package/docs/backend/05-forms/01-custom-forms.md +103 -95
- package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/02-rendering-a-view.md +1 -1
- package/docs/backend/07-views/03-variables.md +5 -5
- package/docs/backend/07-views/04-request-data.md +1 -1
- package/docs/backend/07-views/08-backend-javascript.md +1 -1
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +100 -0
- package/docs/backend/08-database/02-basics.md +136 -0
- package/docs/backend/08-database/03-advanced.md +84 -0
- package/docs/backend/08-database/04-migrations.md +48 -0
- package/docs/backend/09-validation/01-the-validator-service.md +1 -0
- package/docs/backend/10-authentication/03-register.md +9 -2
- package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
- package/docs/backend/10-authentication/05-session-management.md +16 -2
- package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
- package/docs/backend/10-authentication/07-magic-links.md +134 -0
- package/docs/backend/11-mail/01-the-mail-service.md +118 -28
- package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
- package/docs/backend/13-utilities/01-odac-var.md +7 -7
- package/docs/backend/13-utilities/02-ipc.md +73 -0
- package/docs/frontend/01-overview/01-introduction.md +5 -1
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
- package/docs/index.json +21 -125
- package/eslint.config.mjs +5 -47
- package/jest.config.js +1 -1
- package/package.json +16 -7
- package/src/Auth.js +414 -121
- package/src/Config.js +12 -7
- package/src/Database.js +188 -0
- package/src/Env.js +3 -1
- package/src/Ipc.js +337 -0
- package/src/Lang.js +9 -2
- package/src/Mail.js +408 -37
- package/src/Odac.js +105 -40
- package/src/Request.js +71 -49
- package/src/Route/Cron.js +62 -18
- package/src/Route/Internal.js +215 -12
- package/src/Route/Middleware.js +7 -2
- package/src/Route.js +372 -109
- package/src/Server.js +118 -12
- package/src/Storage.js +169 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +95 -3
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +210 -28
- package/src/View.js +108 -7
- package/src/WebSocket.js +18 -3
- package/template/odac.json +5 -0
- package/template/package.json +3 -1
- package/template/route/www.js +12 -10
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +86 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +1 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
- package/test/scripts/check-coverage.js +4 -4
- package/docs/backend/08-database/01-database-connection.md +0 -99
- package/docs/backend/08-database/02-using-mysql.md +0 -322
- package/src/Mysql.js +0 -575
- package/template/config.json +0 -5
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Candy.test.js +0 -234
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Process.test.js +0 -156
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- package/test/server/__mocks__/tls.js +0 -229
package/src/Route.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const fsPromises = fs.promises
|
|
2
3
|
|
|
3
4
|
const Cron = require('./Route/Cron.js')
|
|
4
5
|
const Internal = require('./Route/Internal.js')
|
|
@@ -68,13 +69,16 @@ class Route {
|
|
|
68
69
|
routes = {}
|
|
69
70
|
middlewares = {}
|
|
70
71
|
_pendingMiddlewares = []
|
|
72
|
+
_pendingRouteLoads = []
|
|
71
73
|
#wsServer = new WebSocketServer()
|
|
74
|
+
#configCache = {}
|
|
75
|
+
#publicCache = {}
|
|
72
76
|
auth = {
|
|
73
77
|
page: (path, authFile, file) => this.authPage(path, authFile, file),
|
|
74
78
|
post: (path, authFile, file) => this.authPost(path, authFile, file),
|
|
75
79
|
get: (path, authFile, file) => this.authGet(path, authFile, file),
|
|
76
80
|
ws: (path, handler, options) => this.authWs(path, handler, options),
|
|
77
|
-
use: (...middlewares) => new MiddlewareChain(this, [...middlewares.flat()])
|
|
81
|
+
use: (...middlewares) => new MiddlewareChain(this, [...middlewares.flat()], true)
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
async #runMiddlewares(Odac, middlewares) {
|
|
@@ -90,8 +94,13 @@ class Route {
|
|
|
90
94
|
|
|
91
95
|
const result = await middleware(Odac)
|
|
92
96
|
|
|
97
|
+
if (Odac.Request.res.finished) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
if (result === false) {
|
|
94
|
-
|
|
102
|
+
await Odac.Request.abort(403)
|
|
103
|
+
return false
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
if (result !== undefined && result !== true) {
|
|
@@ -101,15 +110,30 @@ class Route {
|
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
async #executeController(Odac, controller) {
|
|
104
|
-
const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
|
|
105
|
-
if (middlewareResult !== undefined) return middlewareResult
|
|
106
|
-
|
|
107
113
|
if (controller.params) {
|
|
108
114
|
for (let key in controller.params) {
|
|
109
115
|
Odac.Request.data.url[key] = controller.params[key]
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
|
|
120
|
+
if (middlewareResult !== undefined) return middlewareResult
|
|
121
|
+
|
|
122
|
+
if (controller.action) {
|
|
123
|
+
const ControllerClass = controller.cache
|
|
124
|
+
try {
|
|
125
|
+
const instance = new ControllerClass(Odac)
|
|
126
|
+
if (typeof instance[controller.action] === 'function') {
|
|
127
|
+
return instance[controller.action](Odac)
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
if (typeof ControllerClass[controller.action] === 'function') {
|
|
131
|
+
return ControllerClass[controller.action](Odac)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Odac.Request.abort(500)
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
if (typeof controller.cache === 'function') {
|
|
114
138
|
return controller.cache(Odac)
|
|
115
139
|
}
|
|
@@ -126,6 +150,7 @@ class Route {
|
|
|
126
150
|
if (['post', 'put', 'patch', 'delete'].includes(Odac.Request.method)) {
|
|
127
151
|
const formToken = await Odac.request('_odac_form_token')
|
|
128
152
|
if (formToken) {
|
|
153
|
+
Odac.Request.setSession()
|
|
129
154
|
await Internal.processForm(Odac)
|
|
130
155
|
}
|
|
131
156
|
}
|
|
@@ -152,21 +177,40 @@ class Route {
|
|
|
152
177
|
Odac.Request.isAjaxLoad = true
|
|
153
178
|
Odac.Request.clientSkeleton = Odac.Request.header('X-Odac-Skeleton')
|
|
154
179
|
}
|
|
155
|
-
if (Odac.Config
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
if (Odac.Config?.route?.[url]) {
|
|
181
|
+
// PROD CACHE HIT
|
|
182
|
+
if (!Odac.Config.debug && this.#configCache[url]) {
|
|
183
|
+
const cached = this.#configCache[url]
|
|
184
|
+
Odac.Request.header('Content-Type', cached.type)
|
|
185
|
+
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
186
|
+
Odac.Request.header('Content-Length', cached.size)
|
|
187
|
+
return cached.content
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const filePath = Odac.Config.route[url]
|
|
191
|
+
try {
|
|
192
|
+
const content = await fsPromises.readFile(filePath)
|
|
193
|
+
let type = 'text/html'
|
|
194
|
+
if (filePath.includes('.')) {
|
|
195
|
+
let arr = filePath.split('.')
|
|
196
|
+
type = mime[arr[arr.length - 1]]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// PROD CACHE SET
|
|
200
|
+
if (!Odac.Config.debug) {
|
|
201
|
+
this.#configCache[url] = {
|
|
202
|
+
content,
|
|
203
|
+
type,
|
|
204
|
+
size: content.length
|
|
164
205
|
}
|
|
165
|
-
Odac.Request.header('Content-Type', type)
|
|
166
|
-
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
167
|
-
Odac.Request.header('Content-Length', stat.size)
|
|
168
|
-
return fs.readFileSync(Odac.Config.route[url])
|
|
169
206
|
}
|
|
207
|
+
|
|
208
|
+
Odac.Request.header('Content-Type', type)
|
|
209
|
+
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
210
|
+
Odac.Request.header('Content-Length', content.length)
|
|
211
|
+
return content
|
|
212
|
+
} catch {
|
|
213
|
+
// File not found or error, continue routing
|
|
170
214
|
}
|
|
171
215
|
}
|
|
172
216
|
for (let method of ['#' + Odac.Request.method, Odac.Request.method]) {
|
|
@@ -174,6 +218,10 @@ class Route {
|
|
|
174
218
|
if (controller) {
|
|
175
219
|
if (!method.startsWith('#') || (await Odac.Auth.check())) {
|
|
176
220
|
Odac.Request.header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
|
221
|
+
Odac.Request.setSession()
|
|
222
|
+
const page = controller.cache?.file || controller.file
|
|
223
|
+
if (typeof page === 'string') Odac.Request.page = page
|
|
224
|
+
|
|
177
225
|
if (
|
|
178
226
|
['post', 'get'].includes(Odac.Request.method) &&
|
|
179
227
|
controller.token &&
|
|
@@ -187,28 +235,57 @@ class Route {
|
|
|
187
235
|
}
|
|
188
236
|
let authPageController = this.#controller(Odac.Request.route, '#page', url)
|
|
189
237
|
if (authPageController && (await Odac.Auth.check())) {
|
|
190
|
-
Odac.Request.
|
|
238
|
+
Odac.Request.setSession()
|
|
239
|
+
const page = authPageController.cache?.file || authPageController.file
|
|
240
|
+
if (typeof page === 'string') Odac.Request.page = page
|
|
191
241
|
return await this.#executeController(Odac, authPageController)
|
|
192
242
|
}
|
|
193
243
|
let pageController = this.#controller(Odac.Request.route, 'page', url)
|
|
194
244
|
if (pageController) {
|
|
195
|
-
Odac.Request.
|
|
245
|
+
Odac.Request.setSession()
|
|
246
|
+
const page = pageController.cache?.file || pageController.file
|
|
247
|
+
if (typeof page === 'string') Odac.Request.page = page
|
|
196
248
|
return await this.#executeController(Odac, pageController)
|
|
197
249
|
}
|
|
198
|
-
if (url && !url.includes('/../')
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
Odac.Request.header('Content-Type', type)
|
|
250
|
+
if (url && !url.includes('/../')) {
|
|
251
|
+
const publicPath = `${__dir}/public${url}`
|
|
252
|
+
|
|
253
|
+
// PROD CACHE HIT (Metadata)
|
|
254
|
+
if (!Odac.Config.debug && this.#publicCache[publicPath]) {
|
|
255
|
+
const cached = this.#publicCache[publicPath]
|
|
256
|
+
Odac.Request.header('Content-Type', cached.type)
|
|
207
257
|
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
208
|
-
Odac.Request.header('Content-Length',
|
|
209
|
-
return fs.
|
|
258
|
+
Odac.Request.header('Content-Length', cached.size)
|
|
259
|
+
return fs.createReadStream(publicPath)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const stat = await fsPromises.stat(publicPath)
|
|
264
|
+
if (stat.isFile()) {
|
|
265
|
+
let type = 'text/html'
|
|
266
|
+
if (url.includes('.')) {
|
|
267
|
+
let arr = url.split('.')
|
|
268
|
+
type = mime[arr[arr.length - 1]]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// PROD CACHE SET (Metadata Only)
|
|
272
|
+
if (!Odac.Config.debug) {
|
|
273
|
+
this.#publicCache[publicPath] = {
|
|
274
|
+
type,
|
|
275
|
+
size: stat.size
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
Odac.Request.header('Content-Type', type)
|
|
280
|
+
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
281
|
+
Odac.Request.header('Content-Length', stat.size)
|
|
282
|
+
return fs.createReadStream(publicPath)
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// File not found in public
|
|
210
286
|
}
|
|
211
287
|
}
|
|
288
|
+
|
|
212
289
|
return Odac.Request.abort(404)
|
|
213
290
|
}
|
|
214
291
|
|
|
@@ -237,21 +314,28 @@ class Route {
|
|
|
237
314
|
params: params,
|
|
238
315
|
cache: this.routes[route][method][key].cache,
|
|
239
316
|
token: this.routes[route][method][key].token,
|
|
240
|
-
middlewares: this.routes[route][method][key].middlewares
|
|
317
|
+
middlewares: this.routes[route][method][key].middlewares,
|
|
318
|
+
file: this.routes[route][method][key].file
|
|
241
319
|
}
|
|
242
320
|
}
|
|
243
321
|
return false
|
|
244
322
|
}
|
|
245
323
|
|
|
246
|
-
#loadMiddlewares() {
|
|
324
|
+
async #loadMiddlewares() {
|
|
247
325
|
const middlewareDir = `${__dir}/middleware/`
|
|
248
|
-
|
|
326
|
+
try {
|
|
327
|
+
await fsPromises.access(middlewareDir)
|
|
328
|
+
} catch {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
249
331
|
|
|
250
|
-
|
|
332
|
+
const files = await fsPromises.readdir(middlewareDir)
|
|
333
|
+
for (const file of files) {
|
|
251
334
|
if (!file.endsWith('.js')) continue
|
|
252
335
|
const name = file.replace('.js', '')
|
|
253
336
|
const path = `${middlewareDir}${file}`
|
|
254
|
-
const
|
|
337
|
+
const stat = await fsPromises.stat(path)
|
|
338
|
+
const mtime = stat.mtimeMs
|
|
255
339
|
|
|
256
340
|
if (this.middlewares[name] && this.middlewares[name].mtime >= mtime - 1000) continue
|
|
257
341
|
|
|
@@ -264,63 +348,123 @@ class Route {
|
|
|
264
348
|
}
|
|
265
349
|
}
|
|
266
350
|
|
|
267
|
-
#init() {
|
|
351
|
+
async #init() {
|
|
268
352
|
if (this.loading) return
|
|
269
353
|
this.loading = true
|
|
270
|
-
this.#loadMiddlewares()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
354
|
+
await this.#loadMiddlewares()
|
|
355
|
+
const classDir = `${__dir}/class/`
|
|
356
|
+
try {
|
|
357
|
+
await fsPromises.access(classDir)
|
|
358
|
+
const files = await fsPromises.readdir(classDir)
|
|
359
|
+
for (const file of files) {
|
|
360
|
+
if (!file.endsWith('.js')) continue
|
|
361
|
+
let name = file.replace('.js', '')
|
|
362
|
+
if (!Odac.Route.class) Odac.Route.class = {}
|
|
363
|
+
const filePath = `${__dir}/class/${file}`
|
|
364
|
+
|
|
365
|
+
let shouldLoad = true
|
|
366
|
+
let stat = null
|
|
367
|
+
|
|
368
|
+
if (Odac.Route.class[name]) {
|
|
369
|
+
stat = await fsPromises.stat(Odac.Route.class[name].path)
|
|
370
|
+
if (Odac.Route.class[name].mtime >= stat.mtimeMs || Date.now() < stat.mtimeMs + 1000) {
|
|
371
|
+
shouldLoad = false
|
|
372
|
+
} else {
|
|
373
|
+
delete require.cache[require.resolve(Odac.Route.class[name].path)]
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
stat = await fsPromises.stat(filePath)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (shouldLoad) {
|
|
380
|
+
Odac.Route.class[name] = {
|
|
381
|
+
path: filePath,
|
|
382
|
+
mtime: stat.mtimeMs,
|
|
383
|
+
module: require(filePath)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
283
386
|
}
|
|
387
|
+
} catch {
|
|
388
|
+
// Class dir might not exist
|
|
284
389
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const dir = await fsPromises.readdir(`${__dir}/route/`)
|
|
393
|
+
for (const file of dir) {
|
|
394
|
+
if (!file.endsWith('.js')) continue
|
|
395
|
+
const filePath = `${__dir}/route/${file}`
|
|
396
|
+
const stat = await fsPromises.stat(filePath)
|
|
397
|
+
let mtime = stat.mtimeMs
|
|
398
|
+
Odac.Route.buff = file.replace('.js', '')
|
|
399
|
+
|
|
400
|
+
if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
|
|
401
|
+
delete require.cache[require.resolve(filePath)]
|
|
402
|
+
routes2[Odac.Route.buff] = mtime
|
|
403
|
+
const routeModule = require(filePath)
|
|
404
|
+
if (typeof routeModule === 'function') {
|
|
405
|
+
// routeModule calls .set(), which pushes promises to _pendingRouteLoads
|
|
406
|
+
routeModule(Odac)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Wait for all route sets to complete for this file
|
|
411
|
+
await Promise.all(this._pendingRouteLoads)
|
|
412
|
+
this._pendingRouteLoads = []
|
|
413
|
+
|
|
414
|
+
// Clean up deleted routes logic
|
|
415
|
+
for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
|
|
416
|
+
if (!this.routes[Odac.Route.buff]) continue
|
|
417
|
+
if (!this.routes[Odac.Route.buff][type]) continue
|
|
418
|
+
for (const route in this.routes[Odac.Route.buff][type]) {
|
|
419
|
+
const routeObj = this.routes[Odac.Route.buff][type][route]
|
|
420
|
+
if (!routeObj) continue
|
|
421
|
+
|
|
422
|
+
if (routes2[Odac.Route.buff] > routeObj.loaded) {
|
|
423
|
+
if (routeObj.path) {
|
|
424
|
+
try {
|
|
425
|
+
delete require.cache[require.resolve(routeObj.path)]
|
|
426
|
+
} catch {
|
|
427
|
+
// Silently fail
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
delete this.routes[Odac.Route.buff][type][route]
|
|
431
|
+
} else if (routeObj) {
|
|
432
|
+
if (typeof routeObj.type === 'function') continue
|
|
433
|
+
// Check if controller file modified
|
|
434
|
+
try {
|
|
435
|
+
const cStat = await fsPromises.stat(routeObj.path)
|
|
436
|
+
if (routeObj.mtime < cStat.mtimeMs) {
|
|
437
|
+
delete require.cache[require.resolve(routeObj.path)]
|
|
438
|
+
routeObj.cache = require(routeObj.path)
|
|
439
|
+
routeObj.mtime = cStat.mtimeMs
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
// Controller file might have been deleted?
|
|
443
|
+
}
|
|
308
444
|
}
|
|
309
445
|
}
|
|
310
446
|
}
|
|
447
|
+
delete Odac.Route.buff
|
|
311
448
|
}
|
|
312
|
-
|
|
449
|
+
} catch (e) {
|
|
450
|
+
// route dir issue?
|
|
451
|
+
console.error(e)
|
|
313
452
|
}
|
|
453
|
+
|
|
314
454
|
Cron.init()
|
|
315
455
|
this.loading = false
|
|
316
456
|
}
|
|
317
457
|
|
|
318
|
-
init() {
|
|
319
|
-
this.#init()
|
|
458
|
+
async init() {
|
|
459
|
+
await this.#init()
|
|
320
460
|
this.#registerInternalRoutes()
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
461
|
+
|
|
462
|
+
// Hot Reload only in Debug Mode
|
|
463
|
+
if (Odac.Config.debug) {
|
|
464
|
+
setInterval(async () => {
|
|
465
|
+
await this.#init()
|
|
466
|
+
}, 5000)
|
|
467
|
+
}
|
|
324
468
|
}
|
|
325
469
|
|
|
326
470
|
#registerInternalRoutes() {
|
|
@@ -354,7 +498,7 @@ class Route {
|
|
|
354
498
|
)
|
|
355
499
|
|
|
356
500
|
this.set(
|
|
357
|
-
|
|
501
|
+
'POST',
|
|
358
502
|
'/_odac/form',
|
|
359
503
|
async Odac => {
|
|
360
504
|
const csrfToken = await Odac.request('_token')
|
|
@@ -375,6 +519,28 @@ class Route {
|
|
|
375
519
|
{token: true}
|
|
376
520
|
)
|
|
377
521
|
|
|
522
|
+
this.set(
|
|
523
|
+
'POST',
|
|
524
|
+
'/_odac/magic-login',
|
|
525
|
+
async Odac => {
|
|
526
|
+
const csrfToken = await Odac.request('_token')
|
|
527
|
+
if (!csrfToken || !Odac.token(csrfToken)) {
|
|
528
|
+
return Odac.Request.abort(401)
|
|
529
|
+
}
|
|
530
|
+
return await Internal.magicLogin(Odac)
|
|
531
|
+
},
|
|
532
|
+
{token: true}
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
this.set(
|
|
536
|
+
'GET',
|
|
537
|
+
'/_odac/magic-verify',
|
|
538
|
+
async Odac => {
|
|
539
|
+
return await Internal.magicVerify(Odac)
|
|
540
|
+
},
|
|
541
|
+
{token: false}
|
|
542
|
+
)
|
|
543
|
+
|
|
378
544
|
delete Odac.Route.buff
|
|
379
545
|
}
|
|
380
546
|
|
|
@@ -387,6 +553,11 @@ class Route {
|
|
|
387
553
|
if (result instanceof Promise) result = await result
|
|
388
554
|
const Stream = require('./Stream.js')
|
|
389
555
|
if (result instanceof Stream) return
|
|
556
|
+
if (result && typeof result.pipe === 'function') {
|
|
557
|
+
param.Request.print()
|
|
558
|
+
result.pipe(param.Request.res)
|
|
559
|
+
return
|
|
560
|
+
}
|
|
390
561
|
if (param.Request.res.finished || param.Request.res.writableEnded) {
|
|
391
562
|
param.cleanup()
|
|
392
563
|
return
|
|
@@ -408,6 +579,8 @@ class Route {
|
|
|
408
579
|
}
|
|
409
580
|
|
|
410
581
|
set(type, url, file, options = {}) {
|
|
582
|
+
const capturedMiddlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
|
|
583
|
+
|
|
411
584
|
if (Array.isArray(type)) {
|
|
412
585
|
type = type.map(t => t.toLowerCase())
|
|
413
586
|
for (const t of type) {
|
|
@@ -425,43 +598,89 @@ class Route {
|
|
|
425
598
|
const isFunction = typeof file === 'function'
|
|
426
599
|
let path = `${__dir}/route/${Odac.Route.buff}.js`
|
|
427
600
|
|
|
601
|
+
let action = null
|
|
602
|
+
|
|
428
603
|
if (!isFunction && file) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
604
|
+
if (typeof file === 'string' && file.includes('@')) {
|
|
605
|
+
let arr = file.split('@')
|
|
606
|
+
file = arr[0]
|
|
607
|
+
action = arr[1]
|
|
608
|
+
path = `${__dir}/controller/${file.replace(/\./g, '/')}.js`
|
|
609
|
+
} else {
|
|
610
|
+
path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
|
|
611
|
+
if (typeof file === 'string' && file.includes('.')) {
|
|
612
|
+
let arr = file.split('.')
|
|
613
|
+
path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
|
|
614
|
+
}
|
|
433
615
|
}
|
|
434
616
|
}
|
|
435
617
|
|
|
436
618
|
if (!this.routes[Odac.Route.buff]) this.routes[Odac.Route.buff] = {}
|
|
437
619
|
if (!this.routes[Odac.Route.buff][type]) this.routes[Odac.Route.buff][type] = {}
|
|
438
620
|
|
|
439
|
-
|
|
440
|
-
this.routes[Odac.Route.buff][type][url]
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
621
|
+
const task = async () => {
|
|
622
|
+
if (this.routes[Odac.Route.buff][type][url]) {
|
|
623
|
+
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
624
|
+
if (!isFunction) {
|
|
625
|
+
try {
|
|
626
|
+
const stat = await fsPromises.stat(path)
|
|
627
|
+
if (this.routes[Odac.Route.buff][type][url].mtime < stat.mtimeMs) {
|
|
628
|
+
delete this.routes[Odac.Route.buff][type][url]
|
|
629
|
+
delete require.cache[require.resolve(path)]
|
|
630
|
+
} else {
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
// File error, proceed to reload or re-set
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
}
|
|
446
640
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
641
|
+
if (isFunction) {
|
|
642
|
+
if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
|
|
643
|
+
this.routes[Odac.Route.buff][type][url].cache = file
|
|
644
|
+
this.routes[Odac.Route.buff][type][url].type = 'function'
|
|
645
|
+
this.routes[Odac.Route.buff][type][url].file = file
|
|
646
|
+
this.routes[Odac.Route.buff][type][url].mtime = Date.now()
|
|
647
|
+
this.routes[Odac.Route.buff][type][url].path = path
|
|
648
|
+
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
649
|
+
this.routes[Odac.Route.buff][type][url].token = options.token ?? true
|
|
650
|
+
this.routes[Odac.Route.buff][type][url].action = action
|
|
651
|
+
|
|
652
|
+
this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
|
|
653
|
+
} else {
|
|
654
|
+
try {
|
|
655
|
+
const stat = await fsPromises.stat(path)
|
|
656
|
+
if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
|
|
657
|
+
this.routes[Odac.Route.buff][type][url].cache = require(path)
|
|
658
|
+
this.routes[Odac.Route.buff][type][url].type = 'controller'
|
|
659
|
+
this.routes[Odac.Route.buff][type][url].file = file
|
|
660
|
+
this.routes[Odac.Route.buff][type][url].mtime = stat.mtimeMs
|
|
661
|
+
this.routes[Odac.Route.buff][type][url].path = path
|
|
662
|
+
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
663
|
+
this.routes[Odac.Route.buff][type][url].token = options.token ?? true
|
|
664
|
+
this.routes[Odac.Route.buff][type][url].action = action
|
|
665
|
+
|
|
666
|
+
this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
|
|
667
|
+
} catch {
|
|
668
|
+
if (file && typeof file === 'string') {
|
|
669
|
+
console.error(`\x1b[31m[Odac]\x1b[0m Controller not found: \x1b[33m${path}\x1b[0m`)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
457
673
|
}
|
|
458
674
|
|
|
675
|
+
this._pendingRouteLoads.push(task())
|
|
676
|
+
|
|
459
677
|
return this
|
|
460
678
|
}
|
|
461
679
|
|
|
462
680
|
page(path, file) {
|
|
463
681
|
if (typeof file === 'object' && !Array.isArray(file)) {
|
|
464
682
|
this.set('page', path, _odac => {
|
|
683
|
+
_odac.set(file)
|
|
465
684
|
_odac.View.set(file)
|
|
466
685
|
return
|
|
467
686
|
})
|
|
@@ -482,16 +701,20 @@ class Route {
|
|
|
482
701
|
}
|
|
483
702
|
|
|
484
703
|
authPage(path, authFile, file) {
|
|
485
|
-
if (typeof authFile === 'object' && !Array.isArray(authFile)) {
|
|
704
|
+
if (typeof authFile === 'object' && authFile !== null && !Array.isArray(authFile)) {
|
|
486
705
|
this.set('#page', path, _odac => {
|
|
706
|
+
_odac.set(authFile)
|
|
487
707
|
_odac.View.set(authFile)
|
|
488
708
|
return
|
|
489
709
|
})
|
|
490
710
|
if (typeof file === 'object' && !Array.isArray(file)) {
|
|
491
711
|
this.set('page', path, _odac => {
|
|
712
|
+
_odac.set(file)
|
|
492
713
|
_odac.View.set(file)
|
|
493
714
|
return
|
|
494
715
|
})
|
|
716
|
+
} else if (file) {
|
|
717
|
+
this.set('page', path, file)
|
|
495
718
|
}
|
|
496
719
|
return this
|
|
497
720
|
}
|
|
@@ -499,6 +722,7 @@ class Route {
|
|
|
499
722
|
if (file) {
|
|
500
723
|
if (typeof file === 'object' && !Array.isArray(file)) {
|
|
501
724
|
this.set('page', path, _odac => {
|
|
725
|
+
_odac.set(file)
|
|
502
726
|
_odac.View.set(file)
|
|
503
727
|
return
|
|
504
728
|
})
|
|
@@ -509,15 +733,31 @@ class Route {
|
|
|
509
733
|
return this
|
|
510
734
|
}
|
|
511
735
|
|
|
512
|
-
authPost(path, authFile, file) {
|
|
513
|
-
|
|
514
|
-
|
|
736
|
+
authPost(path, authFile, file, options) {
|
|
737
|
+
let opts = options
|
|
738
|
+
let publicFile = file
|
|
739
|
+
|
|
740
|
+
if (publicFile && typeof publicFile === 'object' && !opts) {
|
|
741
|
+
opts = publicFile
|
|
742
|
+
publicFile = undefined
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (authFile) this.set('#post', path, authFile, opts)
|
|
746
|
+
if (publicFile) this.post(path, publicFile, opts)
|
|
515
747
|
return this
|
|
516
748
|
}
|
|
517
749
|
|
|
518
|
-
authGet(path, authFile, file) {
|
|
519
|
-
|
|
520
|
-
|
|
750
|
+
authGet(path, authFile, file, options) {
|
|
751
|
+
let opts = options
|
|
752
|
+
let publicFile = file
|
|
753
|
+
|
|
754
|
+
if (publicFile && typeof publicFile === 'object' && !opts) {
|
|
755
|
+
opts = publicFile
|
|
756
|
+
publicFile = undefined
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (authFile) this.set('#get', path, authFile, opts)
|
|
760
|
+
if (publicFile) this.get(path, publicFile, opts)
|
|
521
761
|
return this
|
|
522
762
|
}
|
|
523
763
|
|
|
@@ -546,6 +786,26 @@ class Route {
|
|
|
546
786
|
const {token = true} = options
|
|
547
787
|
const requireAuth = type === '#ws'
|
|
548
788
|
|
|
789
|
+
if (typeof handler !== 'function') {
|
|
790
|
+
let path = `${__dir}/controller/${type.replace('#', '')}/${handler}.js`
|
|
791
|
+
if (typeof handler === 'string' && handler.includes('.')) {
|
|
792
|
+
let arr = handler.split('.')
|
|
793
|
+
path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (fs.existsSync(path)) {
|
|
797
|
+
handler = require(path)
|
|
798
|
+
} else {
|
|
799
|
+
console.error(`\x1b[31m[Odac]\x1b[0m WebSocket Controller not found: \x1b[33m${path}\x1b[0m`)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (typeof handler !== 'function') {
|
|
805
|
+
console.error(`\x1b[31m[Odac]\x1b[0m Invalid WebSocket handler (not a function).`)
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
|
|
549
809
|
const wrappedHandler = async (ws, Odac) => {
|
|
550
810
|
Odac.ws = ws
|
|
551
811
|
|
|
@@ -603,7 +863,10 @@ class Route {
|
|
|
603
863
|
}
|
|
604
864
|
}
|
|
605
865
|
}
|
|
606
|
-
|
|
866
|
+
const res = handler(Odac)
|
|
867
|
+
if (res instanceof Promise) await res
|
|
868
|
+
ws.resume()
|
|
869
|
+
return res
|
|
607
870
|
}
|
|
608
871
|
|
|
609
872
|
this.#wsServer.route(path, wrappedHandler)
|