odac 0.9.0 → 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.
Files changed (208) hide show
  1. package/.github/workflows/auto-pr-description.yml +0 -2
  2. package/.github/workflows/codeql.yml +46 -0
  3. package/.github/workflows/release.yml +13 -6
  4. package/.github/workflows/test-coverage.yml +10 -9
  5. package/.releaserc.js +9 -6
  6. package/CHANGELOG.md +62 -150
  7. package/CODE_OF_CONDUCT.md +1 -1
  8. package/CONTRIBUTING.md +8 -8
  9. package/LICENSE +21 -661
  10. package/README.md +12 -12
  11. package/SECURITY.md +4 -4
  12. package/bin/odac.js +101 -0
  13. package/{framework/web/candy.js → client/odac.js} +310 -44
  14. package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md} +4 -2
  15. package/docs/backend/01-overview/02-super-handy-helper-functions.md +29 -1
  16. package/docs/backend/01-overview/03-development-server.md +11 -11
  17. package/docs/backend/02-structure/01-typical-project-layout.md +4 -4
  18. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  19. package/docs/backend/03-config/01-database-connection.md +1 -1
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +4 -4
  21. package/docs/backend/03-config/04-environment-variables.md +20 -20
  22. package/docs/backend/03-config/05-early-hints.md +4 -4
  23. package/docs/backend/04-routing/01-basic-page-routes.md +4 -4
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +5 -5
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +3 -3
  26. package/docs/backend/04-routing/04-authentication-aware-routes.md +5 -5
  27. package/docs/backend/04-routing/05-advanced-routing.md +3 -3
  28. package/docs/backend/04-routing/06-error-pages.md +17 -17
  29. package/docs/backend/04-routing/07-cron-jobs.md +13 -13
  30. package/docs/backend/04-routing/08-middleware.md +214 -0
  31. package/docs/backend/04-routing/09-websocket-auth-middleware.md +292 -0
  32. package/docs/backend/04-routing/09-websocket-examples.md +381 -0
  33. package/docs/backend/04-routing/09-websocket-quick-reference.md +211 -0
  34. package/docs/backend/04-routing/09-websocket.md +298 -0
  35. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +3 -3
  36. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +41 -0
  37. package/docs/backend/05-controllers/03-controller-classes.md +19 -19
  38. package/docs/backend/05-forms/01-custom-forms.md +114 -114
  39. package/docs/backend/05-forms/02-automatic-database-insert.md +82 -82
  40. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +26 -26
  41. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +10 -10
  42. package/docs/backend/07-views/01-the-view-directory.md +1 -1
  43. package/docs/backend/07-views/02-rendering-a-view.md +22 -22
  44. package/docs/backend/07-views/03-template-syntax.md +52 -52
  45. package/docs/backend/07-views/03-variables.md +84 -84
  46. package/docs/backend/07-views/04-request-data.md +57 -57
  47. package/docs/backend/07-views/05-conditionals.md +78 -78
  48. package/docs/backend/07-views/06-loops.md +114 -114
  49. package/docs/backend/07-views/07-translations.md +66 -66
  50. package/docs/backend/07-views/08-backend-javascript.md +103 -103
  51. package/docs/backend/07-views/09-comments.md +71 -71
  52. package/docs/backend/08-database/01-database-connection.md +8 -8
  53. package/docs/backend/08-database/02-using-mysql.md +49 -49
  54. package/docs/backend/09-validation/01-the-validator-service.md +38 -38
  55. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +15 -15
  56. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +10 -10
  57. package/docs/backend/10-authentication/03-register.md +12 -12
  58. package/docs/backend/10-authentication/{04-candy-register-forms.md → 04-odac-register-forms.md} +141 -141
  59. package/docs/backend/10-authentication/05-session-management.md +10 -10
  60. package/docs/backend/10-authentication/{06-candy-login-forms.md → 06-odac-login-forms.md} +125 -125
  61. package/docs/backend/11-mail/01-the-mail-service.md +5 -5
  62. package/docs/backend/12-streaming/01-streaming-overview.md +96 -54
  63. package/docs/backend/13-utilities/{01-candy-var.md → 01-odac-var.md} +109 -109
  64. package/docs/frontend/01-overview/01-introduction.md +30 -30
  65. package/docs/frontend/02-ajax-navigation/01-quick-start.md +45 -45
  66. package/docs/frontend/02-ajax-navigation/02-configuration.md +14 -14
  67. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +36 -36
  68. package/docs/frontend/03-forms/01-form-handling.md +32 -32
  69. package/docs/frontend/04-api-requests/01-get-post.md +33 -33
  70. package/docs/frontend/05-streaming/01-client-streaming.md +15 -15
  71. package/docs/frontend/06-websocket/00-overview.md +76 -0
  72. package/docs/frontend/06-websocket/01-websocket-client.md +139 -0
  73. package/docs/frontend/06-websocket/02-shared-websocket.md +149 -0
  74. package/docs/index.json +49 -11
  75. package/eslint.config.mjs +6 -6
  76. package/{framework/index.js → index.js} +1 -1
  77. package/package.json +14 -39
  78. package/{framework/src → src}/Auth.js +59 -59
  79. package/{framework/src → src}/Config.js +3 -3
  80. package/{framework/src → src}/Lang.js +7 -7
  81. package/{framework/src → src}/Mail.js +5 -5
  82. package/{framework/src → src}/Mysql.js +42 -42
  83. package/src/Odac.js +112 -0
  84. package/{framework/src → src}/Request.js +38 -36
  85. package/{framework/src → src}/Route/Internal.js +116 -116
  86. package/src/Route/Middleware.js +75 -0
  87. package/src/Route.js +621 -0
  88. package/src/Server.js +22 -0
  89. package/{framework/src → src}/Stream.js +11 -3
  90. package/{framework/src → src}/Validator.js +21 -21
  91. package/{framework/src → src}/Var.js +5 -5
  92. package/{framework/src → src}/View/EarlyHints.js +1 -1
  93. package/{framework/src → src}/View/Form.js +69 -69
  94. package/{framework/src → src}/View.js +78 -81
  95. package/src/WebSocket.js +403 -0
  96. package/template/config.json +5 -0
  97. package/{web → template}/controller/page/about.js +6 -6
  98. package/{web → template}/controller/page/index.js +9 -9
  99. package/{web → template}/package.json +4 -5
  100. package/{web → template}/public/assets/css/style.css +4 -4
  101. package/{web → template}/public/assets/js/app.js +6 -6
  102. package/{web → template}/route/www.js +6 -6
  103. package/{web → template}/skeleton/main.html +1 -1
  104. package/{web → template}/view/content/about.html +5 -5
  105. package/{web → template}/view/content/home.html +12 -12
  106. package/template/view/footer/main.html +11 -0
  107. package/{web → template}/view/head/main.html +1 -1
  108. package/{web → template}/view/header/main.html +2 -2
  109. package/test/core/Candy.test.js +58 -58
  110. package/test/core/Commands.test.js +7 -7
  111. package/test/core/Config.test.js +82 -85
  112. package/test/core/Lang.test.js +2 -2
  113. package/test/core/Process.test.js +6 -6
  114. package/test/framework/Route.test.js +56 -37
  115. package/test/framework/View/EarlyHints.test.js +2 -2
  116. package/test/framework/WebSocket.test.js +100 -0
  117. package/test/framework/middleware.test.js +85 -0
  118. package/test/server/Api.test.js +31 -31
  119. package/test/server/DNS.test.js +11 -11
  120. package/test/server/Hub.test.js +497 -0
  121. package/test/server/Mail.account.test_.js +3 -3
  122. package/test/server/Mail.init.test_.js +10 -10
  123. package/test/server/Mail.test_.js +20 -20
  124. package/test/server/SSL.test_.js +54 -54
  125. package/test/server/Server.test.js +39 -39
  126. package/test/server/Service.test_.js +7 -7
  127. package/test/server/Subdomain.test.js +7 -7
  128. package/test/server/Web/Firewall.test.js +87 -87
  129. package/test/server/Web/Proxy.test.js +397 -0
  130. package/test/server/{Web.test_.js → Web.test.js} +137 -205
  131. package/test/server/__mocks__/fs.js +2 -2
  132. package/test/server/__mocks__/{globalCandy.js → globalOdac.js} +5 -5
  133. package/test/server/__mocks__/index.js +6 -6
  134. package/test/server/__mocks__/testFactories.js +1 -1
  135. package/test/server/__mocks__/testHelpers.js +7 -7
  136. package/.husky/pre-commit +0 -2
  137. package/.kiro/steering/code-style.md +0 -56
  138. package/.kiro/steering/product.md +0 -20
  139. package/.kiro/steering/structure.md +0 -77
  140. package/.kiro/steering/tech.md +0 -87
  141. package/AGENTS.md +0 -84
  142. package/bin/candy +0 -10
  143. package/bin/candypack +0 -10
  144. package/cli/index.js +0 -3
  145. package/cli/src/Cli.js +0 -348
  146. package/cli/src/Connector.js +0 -93
  147. package/cli/src/Monitor.js +0 -416
  148. package/core/Candy.js +0 -87
  149. package/core/Commands.js +0 -239
  150. package/core/Config.js +0 -1094
  151. package/core/Lang.js +0 -52
  152. package/core/Log.js +0 -43
  153. package/core/Process.js +0 -26
  154. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +0 -20
  155. package/docs/server/01-installation/01-quick-install.md +0 -19
  156. package/docs/server/01-installation/02-manual-installation-via-npm.md +0 -9
  157. package/docs/server/02-get-started/01-core-concepts.md +0 -7
  158. package/docs/server/02-get-started/02-basic-commands.md +0 -57
  159. package/docs/server/02-get-started/03-cli-reference.md +0 -276
  160. package/docs/server/02-get-started/04-cli-quick-reference.md +0 -102
  161. package/docs/server/03-service/01-start-a-new-service.md +0 -57
  162. package/docs/server/03-service/02-delete-a-service.md +0 -48
  163. package/docs/server/04-web/01-create-a-website.md +0 -36
  164. package/docs/server/04-web/02-list-websites.md +0 -9
  165. package/docs/server/04-web/03-delete-a-website.md +0 -29
  166. package/docs/server/05-subdomain/01-create-a-subdomain.md +0 -32
  167. package/docs/server/05-subdomain/02-list-subdomains.md +0 -33
  168. package/docs/server/05-subdomain/03-delete-a-subdomain.md +0 -41
  169. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +0 -34
  170. package/docs/server/07-mail/01-create-a-mail-account.md +0 -23
  171. package/docs/server/07-mail/02-delete-a-mail-account.md +0 -20
  172. package/docs/server/07-mail/03-list-mail-accounts.md +0 -20
  173. package/docs/server/07-mail/04-change-account-password.md +0 -23
  174. package/framework/src/Candy.js +0 -81
  175. package/framework/src/Route.js +0 -455
  176. package/framework/src/Server.js +0 -15
  177. package/locale/de-DE.json +0 -80
  178. package/locale/en-US.json +0 -79
  179. package/locale/es-ES.json +0 -80
  180. package/locale/fr-FR.json +0 -80
  181. package/locale/pt-BR.json +0 -80
  182. package/locale/ru-RU.json +0 -80
  183. package/locale/tr-TR.json +0 -85
  184. package/locale/zh-CN.json +0 -80
  185. package/server/index.js +0 -5
  186. package/server/src/Api.js +0 -88
  187. package/server/src/DNS.js +0 -940
  188. package/server/src/Hub.js +0 -535
  189. package/server/src/Mail.js +0 -571
  190. package/server/src/SSL.js +0 -180
  191. package/server/src/Server.js +0 -27
  192. package/server/src/Service.js +0 -248
  193. package/server/src/Subdomain.js +0 -64
  194. package/server/src/Web/Firewall.js +0 -170
  195. package/server/src/Web/Proxy.js +0 -134
  196. package/server/src/Web.js +0 -451
  197. package/server/src/mail/imap.js +0 -1091
  198. package/server/src/mail/server.js +0 -32
  199. package/server/src/mail/smtp.js +0 -786
  200. package/test/server/Client.test.js +0 -338
  201. package/test/server/__mocks__/http-proxy.js +0 -105
  202. package/watchdog/index.js +0 -3
  203. package/watchdog/src/Watchdog.js +0 -156
  204. package/web/config.json +0 -5
  205. package/web/view/footer/main.html +0 -11
  206. /package/{framework/src → src}/Env.js +0 -0
  207. /package/{framework/src → src}/Route/Cron.js +0 -0
  208. /package/{framework/src → src}/Token.js +0 -0
package/src/Route.js ADDED
@@ -0,0 +1,621 @@
1
+ const fs = require('fs')
2
+
3
+ const Cron = require('./Route/Cron.js')
4
+ const Internal = require('./Route/Internal.js')
5
+ const MiddlewareChain = require('./Route/Middleware.js')
6
+ const {WebSocketServer} = require('./WebSocket.js')
7
+
8
+ var routes2 = {}
9
+ const mime = {
10
+ html: 'text/html',
11
+ css: 'text/css',
12
+ js: 'text/javascript',
13
+ json: 'application/json',
14
+ png: 'image/png',
15
+ jpg: 'image/jpg',
16
+ jpeg: 'image/jpeg',
17
+ svg: 'image/svg+xml',
18
+ ico: 'image/x-icon',
19
+ mp3: 'audio/mpeg',
20
+ mp4: 'video/mp4',
21
+ webm: 'video/webm',
22
+ woff: 'font/woff',
23
+ woff2: 'font/woff2',
24
+ ttf: 'font/ttf',
25
+ otf: 'font/otf',
26
+ eot: 'font/eot',
27
+ pdf: 'application/pdf',
28
+ zip: 'application/zip',
29
+ tar: 'application/x-tar',
30
+ gz: 'application/gzip',
31
+ rar: 'application/x-rar-compressed',
32
+ '7z': 'application/x-7z-compressed',
33
+ txt: 'text/plain',
34
+ log: 'text/plain',
35
+ csv: 'text/csv',
36
+ xml: 'text/xml',
37
+ rss: 'application/rss+xml',
38
+ atom: 'application/atom+xml',
39
+ yaml: 'application/x-yaml',
40
+ sh: 'application/x-sh',
41
+ bat: 'application/x-bat',
42
+ exe: 'application/x-exe',
43
+ bin: 'application/x-binary',
44
+ doc: 'application/msword',
45
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
46
+ xls: 'application/vnd.ms-excel',
47
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
48
+ ppt: 'application/vnd.ms-powerpoint',
49
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
50
+ avi: 'video/x-msvideo',
51
+ wmv: 'video/x-ms-wmv',
52
+ flv: 'video/x-flv',
53
+ webp: 'image/webp',
54
+ gif: 'image/gif',
55
+ bmp: 'image/bmp',
56
+ tiff: 'image/tiff',
57
+ tif: 'image/tiff',
58
+ weba: 'audio/webm',
59
+ wav: 'audio/wav',
60
+ ogg: 'audio/ogg',
61
+ flac: 'audio/flac',
62
+ aac: 'audio/aac',
63
+ midi: 'audio/midi'
64
+ }
65
+
66
+ class Route {
67
+ loading = false
68
+ routes = {}
69
+ middlewares = {}
70
+ _pendingMiddlewares = []
71
+ #wsServer = new WebSocketServer()
72
+ auth = {
73
+ page: (path, authFile, file) => this.authPage(path, authFile, file),
74
+ post: (path, authFile, file) => this.authPost(path, authFile, file),
75
+ get: (path, authFile, file) => this.authGet(path, authFile, file),
76
+ ws: (path, handler, options) => this.authWs(path, handler, options),
77
+ use: (...middlewares) => new MiddlewareChain(this, [...middlewares.flat()])
78
+ }
79
+
80
+ async #runMiddlewares(Odac, middlewares) {
81
+ if (!middlewares || middlewares.length === 0) return
82
+
83
+ for (const mw of middlewares) {
84
+ const middleware = typeof mw === 'function' ? mw : this.middlewares[mw]?.handler
85
+
86
+ if (!middleware) {
87
+ console.error(`Middleware not found: ${mw}`)
88
+ return Odac.Request.abort(500)
89
+ }
90
+
91
+ const result = await middleware(Odac)
92
+
93
+ if (result === false) {
94
+ return Odac.Request.abort(403)
95
+ }
96
+
97
+ if (result !== undefined && result !== true) {
98
+ return result
99
+ }
100
+ }
101
+ }
102
+
103
+ async #executeController(Odac, controller) {
104
+ const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
105
+ if (middlewareResult !== undefined) return middlewareResult
106
+
107
+ if (controller.params) {
108
+ for (let key in controller.params) {
109
+ Odac.Request.data.url[key] = controller.params[key]
110
+ }
111
+ }
112
+
113
+ if (typeof controller.cache === 'function') {
114
+ return controller.cache(Odac)
115
+ }
116
+ }
117
+
118
+ async check(Odac) {
119
+ let url = Odac.Request.url.split('?')[0]
120
+ if (url.endsWith('/')) url = url.slice(0, -1)
121
+
122
+ if (url.startsWith('/_odac/')) {
123
+ Odac.Request.route = '_odac_internal'
124
+ }
125
+
126
+ if (['post', 'put', 'patch', 'delete'].includes(Odac.Request.method)) {
127
+ const formToken = await Odac.request('_odac_form_token')
128
+ if (formToken) {
129
+ await Internal.processForm(Odac)
130
+ }
131
+ }
132
+ if (
133
+ Odac.Request.url === '/' &&
134
+ Odac.Request.method === 'get' &&
135
+ Odac.Request.header('X-Odac') === 'token' &&
136
+ Odac.Request.header('Referer').startsWith((Odac.Request.ssl ? 'https://' : 'http://') + Odac.Request.host + '/') &&
137
+ Odac.Request.header('X-Odac-Client') === Odac.Request.cookie('odac_client')
138
+ ) {
139
+ Odac.Request.header('Access-Control-Allow-Origin', (Odac.Request.ssl ? 'https://' : 'http://') + Odac.Request.host)
140
+ Odac.Request.header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
141
+ return {
142
+ token: Odac.token()
143
+ }
144
+ }
145
+
146
+ // Handle AJAX page load requests
147
+ if (Odac.Request.method === 'get' && Odac.Request.header('X-Odac') === 'ajaxload') {
148
+ let loadElements = Odac.Request.header('X-Odac-Load')
149
+ if (loadElements) {
150
+ Odac.Request.ajaxLoad = loadElements.split(',')
151
+ }
152
+ Odac.Request.isAjaxLoad = true
153
+ Odac.Request.clientSkeleton = Odac.Request.header('X-Odac-Skeleton')
154
+ }
155
+ if (Odac.Config && Odac.Config.route && Odac.Config.route[url]) {
156
+ Odac.Config.route[url] = Odac.Config.route[url].replace('${odac}', `${__dir}/node_modules/odac`)
157
+ if (fs.existsSync(Odac.Config.route[url])) {
158
+ let stat = fs.lstatSync(Odac.Config.route[url])
159
+ if (stat.isFile()) {
160
+ let type = 'text/html'
161
+ if (Odac.Config.route[url].includes('.')) {
162
+ let arr = Odac.Config.route[url].split('.')
163
+ type = mime[arr[arr.length - 1]]
164
+ }
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
+ }
170
+ }
171
+ }
172
+ for (let method of ['#' + Odac.Request.method, Odac.Request.method]) {
173
+ let controller = this.#controller(Odac.Request.route, method, url)
174
+ if (controller) {
175
+ if (!method.startsWith('#') || (await Odac.Auth.check())) {
176
+ Odac.Request.header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
177
+ if (
178
+ ['post', 'get'].includes(Odac.Request.method) &&
179
+ controller.token &&
180
+ (!(await Odac.request('_token')) || !Odac.token(await Odac.Request.request('_token')))
181
+ )
182
+ return Odac.Request.abort(401)
183
+
184
+ return await this.#executeController(Odac, controller)
185
+ }
186
+ }
187
+ }
188
+ let authPageController = this.#controller(Odac.Request.route, '#page', url)
189
+ if (authPageController && (await Odac.Auth.check())) {
190
+ Odac.Request.page = authPageController.cache?.file || authPageController.file
191
+ return await this.#executeController(Odac, authPageController)
192
+ }
193
+ let pageController = this.#controller(Odac.Request.route, 'page', url)
194
+ if (pageController) {
195
+ Odac.Request.page = pageController.cache?.file || pageController.file
196
+ return await this.#executeController(Odac, pageController)
197
+ }
198
+ if (url && !url.includes('/../') && fs.existsSync(`${__dir}/public${url}`)) {
199
+ let stat = fs.lstatSync(`${__dir}/public${url}`)
200
+ if (stat.isFile()) {
201
+ let type = 'text/html'
202
+ if (url.includes('.')) {
203
+ let arr = url.split('.')
204
+ type = mime[arr[arr.length - 1]]
205
+ }
206
+ Odac.Request.header('Content-Type', type)
207
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
208
+ Odac.Request.header('Content-Length', stat.size)
209
+ return fs.readFileSync(`${__dir}/public${url}`)
210
+ }
211
+ }
212
+ return Odac.Request.abort(404)
213
+ }
214
+
215
+ #controller(route, method, url) {
216
+ if (!this.routes[route] || !this.routes[route][method]) return false
217
+ if (this.routes[route][method][url]) return this.routes[route][method][url]
218
+ let arr = url.split('/')
219
+ for (let key in this.routes[route][method]) {
220
+ if (!key.includes('{') || !key.includes('}')) continue
221
+ let route_arr = key.split('/')
222
+ if (route_arr.length !== arr.length) continue
223
+ let params = {}
224
+ let next = false
225
+ for (let i = 0; i < route_arr.length; i++) {
226
+ if (route_arr[i].includes('{') && route_arr[i].includes('}')) {
227
+ params[route_arr[i].replace('{', '').replace('}', '')] = arr[i]
228
+ arr[i] = route_arr[i]
229
+ } else if (route_arr[i] !== arr[i]) {
230
+ next = true
231
+ break
232
+ }
233
+ }
234
+ if (next) continue
235
+ if (arr.join('/') === key)
236
+ return {
237
+ params: params,
238
+ cache: this.routes[route][method][key].cache,
239
+ token: this.routes[route][method][key].token,
240
+ middlewares: this.routes[route][method][key].middlewares
241
+ }
242
+ }
243
+ return false
244
+ }
245
+
246
+ #loadMiddlewares() {
247
+ const middlewareDir = `${__dir}/middleware/`
248
+ if (!fs.existsSync(middlewareDir)) return
249
+
250
+ for (const file of fs.readdirSync(middlewareDir)) {
251
+ if (!file.endsWith('.js')) continue
252
+ const name = file.replace('.js', '')
253
+ const path = `${middlewareDir}${file}`
254
+ const mtime = fs.statSync(path).mtimeMs
255
+
256
+ if (this.middlewares[name] && this.middlewares[name].mtime >= mtime - 1000) continue
257
+
258
+ delete require.cache[require.resolve(path)]
259
+ this.middlewares[name] = {
260
+ path,
261
+ mtime,
262
+ handler: require(path)
263
+ }
264
+ }
265
+ }
266
+
267
+ #init() {
268
+ if (this.loading) return
269
+ this.loading = true
270
+ this.#loadMiddlewares()
271
+ for (const file of fs.readdirSync(`${__dir}/controller/`)) {
272
+ if (!file.endsWith('.js')) continue
273
+ let name = file.replace('.js', '')
274
+ if (!Odac.Route.class) Odac.Route.class = {}
275
+ if (Odac.Route.class[name]) {
276
+ if (Odac.Route.class[name].mtime >= fs.statSync(Odac.Route.class[name].path).mtimeMs + 1000) continue
277
+ delete require.cache[require.resolve(Odac.Route.class[name].path)]
278
+ }
279
+ Odac.Route.class[name] = {
280
+ path: `${__dir}/controller/${file}`,
281
+ mtime: fs.statSync(`${__dir}/controller/${file}`).mtimeMs,
282
+ module: require(`${__dir}/controller/${file}`)
283
+ }
284
+ }
285
+ let dir = fs.readdirSync(`${__dir}/route/`)
286
+ for (const file of dir) {
287
+ if (!file.endsWith('.js')) continue
288
+ let mtime = fs.statSync(`${__dir}/route/${file}`).mtimeMs
289
+ Odac.Route.buff = file.replace('.js', '')
290
+ if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
291
+ delete require.cache[require.resolve(`${__dir}/route/${file}`)]
292
+ routes2[Odac.Route.buff] = mtime
293
+ require(`${__dir}/route/${file}`)
294
+ }
295
+ for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
296
+ if (!this.routes[Odac.Route.buff]) continue
297
+ if (!this.routes[Odac.Route.buff][type]) continue
298
+ for (const route in this.routes[Odac.Route.buff][type]) {
299
+ if (routes2[Odac.Route.buff] > this.routes[Odac.Route.buff][type][route].loaded) {
300
+ delete require.cache[require.resolve(this.routes[Odac.Route.buff][type][route].path)]
301
+ delete this.routes[Odac.Route.buff][type][route]
302
+ } else if (this.routes[Odac.Route.buff][type][route]) {
303
+ if (typeof this.routes[Odac.Route.buff][type][route].type === 'function') continue
304
+ if (this.routes[Odac.Route.buff][type][route].mtime < fs.statSync(this.routes[Odac.Route.buff][type][route].path).mtimeMs) {
305
+ delete require.cache[require.resolve(this.routes[Odac.Route.buff][type][route].path)]
306
+ this.routes[Odac.Route.buff][type][route].cache = require(this.routes[Odac.Route.buff][type][route].path)
307
+ this.routes[Odac.Route.buff][type][route].mtime = fs.statSync(this.routes[Odac.Route.buff][type][route].path).mtimeMs
308
+ }
309
+ }
310
+ }
311
+ }
312
+ delete Odac.Route.buff
313
+ }
314
+ Cron.init()
315
+ this.loading = false
316
+ }
317
+
318
+ init() {
319
+ this.#init()
320
+ this.#registerInternalRoutes()
321
+ setInterval(() => {
322
+ this.#init()
323
+ }, 5000)
324
+ }
325
+
326
+ #registerInternalRoutes() {
327
+ if (!Odac.Route) Odac.Route = {}
328
+ Odac.Route.buff = '_odac_internal'
329
+
330
+ this.set(
331
+ 'POST',
332
+ '/_odac/register',
333
+ async Odac => {
334
+ const csrfToken = await Odac.request('_token')
335
+ if (!csrfToken || !Odac.token(csrfToken)) {
336
+ return Odac.Request.abort(401)
337
+ }
338
+ return await Internal.register(Odac)
339
+ },
340
+ {token: true}
341
+ )
342
+
343
+ this.set(
344
+ 'POST',
345
+ '/_odac/login',
346
+ async Odac => {
347
+ const csrfToken = await Odac.request('_token')
348
+ if (!csrfToken || !Odac.token(csrfToken)) {
349
+ return Odac.Request.abort(401)
350
+ }
351
+ return await Internal.login(Odac)
352
+ },
353
+ {token: true}
354
+ )
355
+
356
+ this.set(
357
+ ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'],
358
+ '/_odac/form',
359
+ async Odac => {
360
+ const csrfToken = await Odac.request('_token')
361
+ if (!csrfToken || !Odac.token(csrfToken)) {
362
+ return Odac.Request.abort(401)
363
+ }
364
+ const result = await Internal.customForm(Odac)
365
+ if (result !== null) return result
366
+
367
+ return Odac.return({
368
+ result: {
369
+ success: false,
370
+ message: 'No handler defined for this form'
371
+ },
372
+ errors: {_odac_form: 'Form action not configured'}
373
+ })
374
+ },
375
+ {token: true}
376
+ )
377
+
378
+ delete Odac.Route.buff
379
+ }
380
+
381
+ async request(req, res) {
382
+ let id = `${Date.now()}${Math.random().toString(36).substr(2, 9)}`
383
+ let param = Odac.instance(id, req, res)
384
+ if (!this.routes[param.Request.route]) return param.Request.end()
385
+ try {
386
+ let result = this.check(param)
387
+ if (result instanceof Promise) result = await result
388
+ const Stream = require('./Stream.js')
389
+ if (result instanceof Stream) return
390
+ if (param.Request.res.finished || param.Request.res.writableEnded) {
391
+ param.cleanup()
392
+ return
393
+ }
394
+ if (result) param.Request.end(result)
395
+ await param.View.print(param)
396
+ param.Request.print(param)
397
+ param.cleanup()
398
+ } catch (e) {
399
+ console.error(e)
400
+ param.Request.abort(500)
401
+ param.cleanup()
402
+ return param.Request.end()
403
+ }
404
+ }
405
+
406
+ use(...middlewares) {
407
+ return new MiddlewareChain(this, [...middlewares.flat()])
408
+ }
409
+
410
+ set(type, url, file, options = {}) {
411
+ if (Array.isArray(type)) {
412
+ type = type.map(t => t.toLowerCase())
413
+ for (const t of type) {
414
+ this.set(t, url, file, options)
415
+ }
416
+ return this
417
+ }
418
+
419
+ if (!options) options = {}
420
+ if (typeof url !== 'string') url = String(url)
421
+ if (url.length && url.endsWith('/')) url = url.slice(0, -1)
422
+
423
+ type = type.toLowerCase()
424
+
425
+ const isFunction = typeof file === 'function'
426
+ let path = `${__dir}/route/${Odac.Route.buff}.js`
427
+
428
+ if (!isFunction && file) {
429
+ path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
430
+ if (typeof file === 'string' && file.includes('.')) {
431
+ let arr = file.split('.')
432
+ path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
433
+ }
434
+ }
435
+
436
+ if (!this.routes[Odac.Route.buff]) this.routes[Odac.Route.buff] = {}
437
+ if (!this.routes[Odac.Route.buff][type]) this.routes[Odac.Route.buff][type] = {}
438
+
439
+ if (this.routes[Odac.Route.buff][type][url]) {
440
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
441
+ if (!isFunction && this.routes[Odac.Route.buff][type][url].mtime < fs.statSync(path).mtimeMs) {
442
+ delete this.routes[Odac.Route.buff][type][url]
443
+ delete require.cache[require.resolve(path)]
444
+ } else return this
445
+ }
446
+
447
+ if (isFunction || fs.existsSync(path)) {
448
+ if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
449
+ this.routes[Odac.Route.buff][type][url].cache = isFunction ? file : require(path)
450
+ this.routes[Odac.Route.buff][type][url].type = isFunction ? 'function' : 'controller'
451
+ this.routes[Odac.Route.buff][type][url].file = file
452
+ this.routes[Odac.Route.buff][type][url].mtime = isFunction ? Date.now() : fs.statSync(path).mtimeMs
453
+ this.routes[Odac.Route.buff][type][url].path = path
454
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
455
+ this.routes[Odac.Route.buff][type][url].token = options.token ?? true
456
+ this.routes[Odac.Route.buff][type][url].middlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
457
+ }
458
+
459
+ return this
460
+ }
461
+
462
+ page(path, file) {
463
+ if (typeof file === 'object' && !Array.isArray(file)) {
464
+ this.set('page', path, _odac => {
465
+ _odac.View.set(file)
466
+ return
467
+ })
468
+ return this
469
+ }
470
+ if (file) this.set('page', path, file)
471
+ return this
472
+ }
473
+
474
+ post(path, file, options) {
475
+ this.set('post', path, file, options)
476
+ return this
477
+ }
478
+
479
+ get(path, file, options) {
480
+ this.set('get', path, file, options)
481
+ return this
482
+ }
483
+
484
+ authPage(path, authFile, file) {
485
+ if (typeof authFile === 'object' && !Array.isArray(authFile)) {
486
+ this.set('#page', path, _odac => {
487
+ _odac.View.set(authFile)
488
+ return
489
+ })
490
+ if (typeof file === 'object' && !Array.isArray(file)) {
491
+ this.set('page', path, _odac => {
492
+ _odac.View.set(file)
493
+ return
494
+ })
495
+ }
496
+ return this
497
+ }
498
+ if (authFile) this.set('#page', path, authFile)
499
+ if (file) {
500
+ if (typeof file === 'object' && !Array.isArray(file)) {
501
+ this.set('page', path, _odac => {
502
+ _odac.View.set(file)
503
+ return
504
+ })
505
+ } else {
506
+ this.set('page', path, file)
507
+ }
508
+ }
509
+ return this
510
+ }
511
+
512
+ authPost(path, authFile, file) {
513
+ if (authFile) this.set('#post', path, authFile)
514
+ if (file) this.post(path, file)
515
+ return this
516
+ }
517
+
518
+ authGet(path, authFile, file) {
519
+ if (authFile) this.set('#get', path, authFile)
520
+ if (file) this.get(path, file)
521
+ return this
522
+ }
523
+
524
+ error(code, file) {
525
+ this.set('error', code, file)
526
+ }
527
+
528
+ cron(controller) {
529
+ return Cron.job(controller)
530
+ }
531
+
532
+ ws(path, handler, options = {}) {
533
+ this.setWs('ws', path, handler, options)
534
+ return this
535
+ }
536
+
537
+ authWs(path, handler, options = {}) {
538
+ this.setWs('#ws', path, handler, options)
539
+ return this
540
+ }
541
+
542
+ setWs(type, path, handler, options = {}) {
543
+ const middlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
544
+ this._pendingMiddlewares = []
545
+
546
+ const {token = true} = options
547
+ const requireAuth = type === '#ws'
548
+
549
+ const wrappedHandler = async (ws, Odac) => {
550
+ Odac.ws = ws
551
+
552
+ ws.on('close', () => {
553
+ if (Odac.cleanup && typeof Odac.cleanup === 'function') {
554
+ Odac.cleanup()
555
+ }
556
+ })
557
+
558
+ if (requireAuth) {
559
+ const isAuthenticated = await Odac.Auth.check()
560
+ if (!isAuthenticated) {
561
+ ws.close(4001, 'Unauthorized')
562
+ return
563
+ }
564
+ }
565
+
566
+ if (token) {
567
+ const wsToken = Odac.Request._wsHeaders ? Odac.Request._wsHeaders['sec-websocket-protocol'] : null
568
+ const tokens = wsToken ? wsToken.split(', ') : []
569
+ const odacToken = tokens.find(t => t.startsWith('odac-token-'))
570
+
571
+ if (!odacToken) {
572
+ ws.close(4002, 'Missing token')
573
+ return
574
+ }
575
+
576
+ const tokenValue = odacToken.replace('odac-token-', '')
577
+ if (!Odac.token(tokenValue)) {
578
+ ws.close(4002, 'Invalid token')
579
+ return
580
+ }
581
+ }
582
+
583
+ if (middlewares) {
584
+ for (const mw of middlewares) {
585
+ const middleware = typeof mw === 'function' ? mw : this.middlewares[mw]?.handler
586
+
587
+ if (!middleware) {
588
+ console.error(`Middleware not found: ${mw}`)
589
+ ws.close(4000, 'Internal error')
590
+ return
591
+ }
592
+
593
+ const result = await middleware(Odac)
594
+
595
+ if (result === false) {
596
+ ws.close(4003, 'Forbidden')
597
+ return
598
+ }
599
+
600
+ if (result !== undefined && result !== true) {
601
+ ws.close(4000, 'Middleware rejected')
602
+ return
603
+ }
604
+ }
605
+ }
606
+ return handler(Odac)
607
+ }
608
+
609
+ this.#wsServer.route(path, wrappedHandler)
610
+ }
611
+
612
+ handleWebSocketUpgrade(req, socket, head, Odac) {
613
+ this.#wsServer.handleUpgrade(req, socket, head, Odac)
614
+ }
615
+
616
+ get wsServer() {
617
+ return this.#wsServer
618
+ }
619
+ }
620
+
621
+ module.exports = Route
package/src/Server.js ADDED
@@ -0,0 +1,22 @@
1
+ const http = require(`http`)
2
+
3
+ module.exports = {
4
+ init: function () {
5
+ let args = process.argv.slice(2)
6
+ if (args[0] == 'framework' && args[1] == 'run') args = args.slice(2)
7
+ let port = parseInt(args[0] ?? '1071')
8
+ console.log(`Odac Server running on \x1b]8;;http://127.0.0.1:${port}\x1b\\\x1b[4mhttp://127.0.0.1:${port}\x1b[0m\x1b]8;;\x1b\\.`)
9
+
10
+ const server = http.createServer((req, res) => {
11
+ return Odac.Route.request(req, res)
12
+ })
13
+
14
+ server.on('upgrade', (req, socket, head) => {
15
+ const id = `${Date.now()}${Math.random().toString(36).substr(2, 9)}`
16
+ const param = Odac.instance(id, req, null)
17
+ Odac.Route.handleWebSocketUpgrade(req, socket, head, param)
18
+ })
19
+
20
+ server.listen(port)
21
+ }
22
+ }
@@ -3,10 +3,12 @@ class Stream {
3
3
  #req
4
4
  #res
5
5
  #closed = false
6
+ #odac
6
7
 
7
- constructor(req, res, input) {
8
+ constructor(req, res, input, odac) {
8
9
  this.#req = req
9
10
  this.#res = res
11
+ this.#odac = odac
10
12
  this.#init()
11
13
  this.#handleInput(input)
12
14
  }
@@ -30,9 +32,12 @@ class Stream {
30
32
  }
31
33
  }, 30000)
32
34
 
33
- this.#req.on('close', () => {
35
+ const handleClose = () => {
34
36
  this.close()
35
- })
37
+ }
38
+
39
+ this.#req.on('close', handleClose)
40
+ this.#res.on('close', handleClose)
36
41
  }
37
42
 
38
43
  #handleInput(input) {
@@ -127,6 +132,9 @@ class Stream {
127
132
  if (!this.#res.writableEnded) {
128
133
  this.#res.end()
129
134
  }
135
+ if (this.#odac && typeof this.#odac.cleanup === 'function') {
136
+ this.#odac.cleanup()
137
+ }
130
138
  }
131
139
 
132
140
  #pipeIterator(iterator) {