odac 0.9.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 (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,455 @@
1
+ const fs = require('fs')
2
+
3
+ const Cron = require('./Route/Cron.js')
4
+ const Internal = require('./Route/Internal.js')
5
+
6
+ var routes2 = {}
7
+ const mime = {
8
+ html: 'text/html',
9
+ css: 'text/css',
10
+ js: 'text/javascript',
11
+ json: 'application/json',
12
+ png: 'image/png',
13
+ jpg: 'image/jpg',
14
+ jpeg: 'image/jpeg',
15
+ svg: 'image/svg+xml',
16
+ ico: 'image/x-icon',
17
+ mp3: 'audio/mpeg',
18
+ mp4: 'video/mp4',
19
+ webm: 'video/webm',
20
+ woff: 'font/woff',
21
+ woff2: 'font/woff2',
22
+ ttf: 'font/ttf',
23
+ otf: 'font/otf',
24
+ eot: 'font/eot',
25
+ pdf: 'application/pdf',
26
+ zip: 'application/zip',
27
+ tar: 'application/x-tar',
28
+ gz: 'application/gzip',
29
+ rar: 'application/x-rar-compressed',
30
+ '7z': 'application/x-7z-compressed',
31
+ txt: 'text/plain',
32
+ log: 'text/plain',
33
+ csv: 'text/csv',
34
+ xml: 'text/xml',
35
+ rss: 'application/rss+xml',
36
+ atom: 'application/atom+xml',
37
+ yaml: 'application/x-yaml',
38
+ sh: 'application/x-sh',
39
+ bat: 'application/x-bat',
40
+ exe: 'application/x-exe',
41
+ bin: 'application/x-binary',
42
+ doc: 'application/msword',
43
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
44
+ xls: 'application/vnd.ms-excel',
45
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
46
+ ppt: 'application/vnd.ms-powerpoint',
47
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
48
+ avi: 'video/x-msvideo',
49
+ wmv: 'video/x-ms-wmv',
50
+ flv: 'video/x-flv',
51
+ webp: 'image/webp',
52
+ gif: 'image/gif',
53
+ bmp: 'image/bmp',
54
+ tiff: 'image/tiff',
55
+ tif: 'image/tiff',
56
+ weba: 'audio/webm',
57
+ wav: 'audio/wav',
58
+ ogg: 'audio/ogg',
59
+ flac: 'audio/flac',
60
+ aac: 'audio/aac',
61
+ midi: 'audio/midi'
62
+ }
63
+
64
+ class Route {
65
+ loading = false
66
+ routes = {}
67
+ auth = {
68
+ page: (path, authFile, file) => this.authPage(path, authFile, file),
69
+ post: (path, authFile, file) => this.authPost(path, authFile, file),
70
+ get: (path, authFile, file) => this.authGet(path, authFile, file)
71
+ }
72
+
73
+ async check(Candy) {
74
+ let url = Candy.Request.url.split('?')[0]
75
+ if (url.substr(-1) === '/') url = url.substr(0, url.length - 1)
76
+
77
+ if (url.startsWith('/_candy/')) {
78
+ Candy.Request.route = '_candy_internal'
79
+ }
80
+
81
+ if (['post', 'put', 'patch', 'delete'].includes(Candy.Request.method)) {
82
+ const formToken = await Candy.request('_candy_form_token')
83
+ if (formToken) {
84
+ await Internal.processForm(Candy)
85
+ }
86
+ }
87
+ if (
88
+ Candy.Request.url === '/' &&
89
+ Candy.Request.method === 'get' &&
90
+ Candy.Request.header('X-Candy') === 'token' &&
91
+ Candy.Request.header('Referer').startsWith((Candy.Request.ssl ? 'https://' : 'http://') + Candy.Request.host + '/') &&
92
+ Candy.Request.header('X-Candy-Client') === Candy.Request.cookie('candy_client')
93
+ ) {
94
+ Candy.Request.header('Access-Control-Allow-Origin', (Candy.Request.ssl ? 'https://' : 'http://') + Candy.Request.host)
95
+ Candy.Request.header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
96
+ return {
97
+ token: Candy.token()
98
+ }
99
+ }
100
+
101
+ // Handle AJAX page load requests
102
+ if (Candy.Request.method === 'get' && Candy.Request.header('X-Candy') === 'ajaxload') {
103
+ let loadElements = Candy.Request.header('X-Candy-Load')
104
+ if (loadElements) {
105
+ Candy.Request.ajaxLoad = loadElements.split(',')
106
+ }
107
+ Candy.Request.isAjaxLoad = true
108
+ Candy.Request.clientSkeleton = Candy.Request.header('X-Candy-Skeleton')
109
+ }
110
+ if (Candy.Config.route && Candy.Config.route[url]) {
111
+ Candy.Config.route[url] = Candy.Config.route[url].replace('${candy}', `${__dir}/node_modules/candypack`)
112
+ if (fs.existsSync(Candy.Config.route[url])) {
113
+ let stat = fs.lstatSync(Candy.Config.route[url])
114
+ if (stat.isFile()) {
115
+ let type = 'text/html'
116
+ if (Candy.Config.route[url].includes('.')) {
117
+ let arr = Candy.Config.route[url].split('.')
118
+ type = mime[arr[arr.length - 1]]
119
+ }
120
+ Candy.Request.header('Content-Type', type)
121
+ Candy.Request.header('Cache-Control', 'public, max-age=31536000')
122
+ Candy.Request.header('Content-Length', stat.size)
123
+ return fs.readFileSync(Candy.Config.route[url])
124
+ }
125
+ }
126
+ }
127
+ for (let method of ['#' + Candy.Request.method, Candy.Request.method]) {
128
+ let controller = this.#controller(Candy.Request.route, method, url)
129
+ if (controller) {
130
+ if (!method.startsWith('#') || (await Candy.Auth.check())) {
131
+ Candy.Request.header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
132
+ if (
133
+ ['post', 'get'].includes(Candy.Request.method) &&
134
+ controller.token &&
135
+ (!(await Candy.request('_token')) || !Candy.token(await Candy.Request.request('_token')))
136
+ )
137
+ return Candy.Request.abort(401)
138
+ if (typeof controller.cache === 'function') {
139
+ if (controller.params) for (let key in controller.params) Candy.Request.data.url[key] = controller.params[key]
140
+ return controller.cache(Candy)
141
+ }
142
+ }
143
+ }
144
+ }
145
+ let authPageController = this.#controller(Candy.Request.route, '#page', url)
146
+ if (authPageController && (await Candy.Auth.check())) {
147
+ if (authPageController.params) for (let key in authPageController.params) Candy.Request.data.url[key] = authPageController.params[key]
148
+ Candy.Request.page = authPageController.cache?.file || authPageController.file
149
+ if (typeof authPageController.cache === 'function') {
150
+ return authPageController.cache(Candy)
151
+ }
152
+ }
153
+ let pageController = this.#controller(Candy.Request.route, 'page', url)
154
+ if (pageController) {
155
+ if (pageController.params) for (let key in pageController.params) Candy.Request.data.url[key] = pageController.params[key]
156
+ Candy.Request.page = pageController.cache?.file || pageController.file
157
+ if (typeof pageController.cache === 'function') {
158
+ return pageController.cache(Candy)
159
+ }
160
+ }
161
+ if (url && !url.includes('/../') && fs.existsSync(`${__dir}/public${url}`)) {
162
+ let stat = fs.lstatSync(`${__dir}/public${url}`)
163
+ if (stat.isFile()) {
164
+ let type = 'text/html'
165
+ if (url.includes('.')) {
166
+ let arr = url.split('.')
167
+ type = mime[arr[arr.length - 1]]
168
+ }
169
+ Candy.Request.header('Content-Type', type)
170
+ Candy.Request.header('Cache-Control', 'public, max-age=31536000')
171
+ Candy.Request.header('Content-Length', stat.size)
172
+ return fs.readFileSync(`${__dir}/public${url}`)
173
+ }
174
+ }
175
+ return Candy.Request.abort(404)
176
+ }
177
+
178
+ #controller(route, method, url) {
179
+ if (!this.routes[route] || !this.routes[route][method]) return false
180
+ if (this.routes[route][method][url]) return this.routes[route][method][url]
181
+ let arr = url.split('/')
182
+ for (let key in this.routes[route][method]) {
183
+ if (!key.includes('{') || !key.includes('}')) continue
184
+ let route_arr = key.split('/')
185
+ if (route_arr.length !== arr.length) continue
186
+ let params = {}
187
+ let next = false
188
+ for (let i = 0; i < route_arr.length; i++) {
189
+ if (route_arr[i].includes('{') && route_arr[i].includes('}')) {
190
+ params[route_arr[i].replace('{', '').replace('}', '')] = arr[i]
191
+ arr[i] = route_arr[i]
192
+ } else if (route_arr[i] !== arr[i]) {
193
+ next = true
194
+ break
195
+ }
196
+ }
197
+ if (next) continue
198
+ if (arr.join('/') === key)
199
+ return {
200
+ params: params,
201
+ cache: this.routes[route][method][key].cache,
202
+ token: this.routes[route][method][key].token
203
+ }
204
+ }
205
+ return false
206
+ }
207
+
208
+ #init() {
209
+ if (this.loading) return
210
+ this.loading = true
211
+ for (const file of fs.readdirSync(`${__dir}/controller/`)) {
212
+ if (file.substr(-3) !== '.js') continue
213
+ let name = file.replace('.js', '')
214
+ if (!Candy.Route.class) Candy.Route.class = {}
215
+ if (Candy.Route.class[name]) {
216
+ if (Candy.Route.class[name].mtime >= fs.statSync(Candy.Route.class[name].path).mtimeMs + 1000) continue
217
+ delete require.cache[require.resolve(Candy.Route.class[name].path)]
218
+ }
219
+ Candy.Route.class[name] = {
220
+ path: `${__dir}/controller/${file}`,
221
+ mtime: fs.statSync(`${__dir}/controller/${file}`).mtimeMs,
222
+ module: require(`${__dir}/controller/${file}`)
223
+ }
224
+ }
225
+ let dir = fs.readdirSync(`${__dir}/route/`)
226
+ for (const file of dir) {
227
+ if (file.substr(-3) !== '.js') continue
228
+ let mtime = fs.statSync(`${__dir}/route/${file}`).mtimeMs
229
+ Candy.Route.buff = file.replace('.js', '')
230
+ if (!routes2[Candy.Route.buff] || routes2[Candy.Route.buff] < mtime - 1000) {
231
+ delete require.cache[require.resolve(`${__dir}/route/${file}`)]
232
+ routes2[Candy.Route.buff] = mtime
233
+ require(`${__dir}/route/${file}`)
234
+ }
235
+ for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
236
+ if (!this.routes[Candy.Route.buff]) continue
237
+ if (!this.routes[Candy.Route.buff][type]) continue
238
+ for (const route in this.routes[Candy.Route.buff][type]) {
239
+ if (routes2[Candy.Route.buff] > this.routes[Candy.Route.buff][type][route].loaded) {
240
+ delete require.cache[require.resolve(this.routes[Candy.Route.buff][type][route].path)]
241
+ delete this.routes[Candy.Route.buff][type][route]
242
+ } else if (this.routes[Candy.Route.buff][type][route]) {
243
+ if (typeof this.routes[Candy.Route.buff][type][route].type === 'function') continue
244
+ if (this.routes[Candy.Route.buff][type][route].mtime < fs.statSync(this.routes[Candy.Route.buff][type][route].path).mtimeMs) {
245
+ delete require.cache[require.resolve(this.routes[Candy.Route.buff][type][route].path)]
246
+ this.routes[Candy.Route.buff][type][route].cache = require(this.routes[Candy.Route.buff][type][route].path)
247
+ this.routes[Candy.Route.buff][type][route].mtime = fs.statSync(this.routes[Candy.Route.buff][type][route].path).mtimeMs
248
+ }
249
+ }
250
+ }
251
+ }
252
+ delete Candy.Route.buff
253
+ }
254
+ Cron.init()
255
+ this.loading = false
256
+ }
257
+
258
+ init() {
259
+ this.#init()
260
+ this.#registerInternalRoutes()
261
+ setInterval(() => {
262
+ this.#init()
263
+ }, 5000)
264
+ }
265
+
266
+ #registerInternalRoutes() {
267
+ if (!Candy.Route) Candy.Route = {}
268
+ Candy.Route.buff = '_candy_internal'
269
+
270
+ this.set(
271
+ 'POST',
272
+ '/_candy/register',
273
+ async Candy => {
274
+ const csrfToken = await Candy.request('_token')
275
+ if (!csrfToken || !Candy.token(csrfToken)) {
276
+ return Candy.Request.abort(401)
277
+ }
278
+ return await Internal.register(Candy)
279
+ },
280
+ {token: true}
281
+ )
282
+
283
+ this.set(
284
+ 'POST',
285
+ '/_candy/login',
286
+ async Candy => {
287
+ const csrfToken = await Candy.request('_token')
288
+ if (!csrfToken || !Candy.token(csrfToken)) {
289
+ return Candy.Request.abort(401)
290
+ }
291
+ return await Internal.login(Candy)
292
+ },
293
+ {token: true}
294
+ )
295
+
296
+ this.set(
297
+ ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'],
298
+ '/_candy/form',
299
+ async Candy => {
300
+ const csrfToken = await Candy.request('_token')
301
+ if (!csrfToken || !Candy.token(csrfToken)) {
302
+ return Candy.Request.abort(401)
303
+ }
304
+ const result = await Internal.customForm(Candy)
305
+ if (result !== null) return result
306
+
307
+ return Candy.return({
308
+ result: {
309
+ success: false,
310
+ message: 'No handler defined for this form'
311
+ },
312
+ errors: {_candy_form: 'Form action not configured'}
313
+ })
314
+ },
315
+ {token: true}
316
+ )
317
+
318
+ delete Candy.Route.buff
319
+ }
320
+
321
+ async request(req, res) {
322
+ let id = `${Date.now()}${Math.random().toString(36).substr(2, 9)}`
323
+ let param = Candy.instance(id, req, res)
324
+ if (!this.routes[param.Request.route]) return param.Request.end()
325
+ try {
326
+ let result = this.check(param)
327
+ if (result instanceof Promise) result = await result
328
+ const Stream = require('./Stream.js')
329
+ if (result instanceof Stream) return
330
+ if (param.Request.res.finished || param.Request.res.writableEnded) return
331
+ if (result) param.Request.end(result)
332
+ await param.View.print(param)
333
+ param.Request.print(param)
334
+ } catch (e) {
335
+ console.error(e)
336
+ param.Request.abort(500)
337
+ return param.Request.end()
338
+ }
339
+ }
340
+
341
+ set(type, url, file, options = {}) {
342
+ if (Array.isArray(type)) {
343
+ type = type.map(t => t.toLowerCase())
344
+ for (const t of type) {
345
+ this.set(t, url, file, options)
346
+ }
347
+ return
348
+ }
349
+
350
+ if (!options) options = {}
351
+ if (typeof url !== 'string') url = String(url)
352
+ if (url.length && url.substr(-1) === '/') url = url.substr(0, url.length - 1)
353
+
354
+ type = type.toLowerCase()
355
+
356
+ const isFunction = typeof file === 'function'
357
+ let path = `${__dir}/route/${Candy.Route.buff}.js`
358
+
359
+ if (!isFunction && file) {
360
+ path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
361
+ if (typeof file === 'string' && file.includes('.')) {
362
+ let arr = file.split('.')
363
+ path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
364
+ }
365
+ }
366
+
367
+ if (!this.routes[Candy.Route.buff]) this.routes[Candy.Route.buff] = {}
368
+ if (!this.routes[Candy.Route.buff][type]) this.routes[Candy.Route.buff][type] = {}
369
+
370
+ if (this.routes[Candy.Route.buff][type][url]) {
371
+ this.routes[Candy.Route.buff][type][url].loaded = routes2[Candy.Route.buff]
372
+ if (!isFunction && this.routes[Candy.Route.buff][type][url].mtime < fs.statSync(path).mtimeMs) {
373
+ delete this.routes[Candy.Route.buff][type][url]
374
+ delete require.cache[require.resolve(path)]
375
+ } else return
376
+ }
377
+
378
+ if (isFunction || fs.existsSync(path)) {
379
+ if (!this.routes[Candy.Route.buff][type][url]) this.routes[Candy.Route.buff][type][url] = {}
380
+ this.routes[Candy.Route.buff][type][url].cache = isFunction ? file : require(path)
381
+ this.routes[Candy.Route.buff][type][url].type = isFunction ? 'function' : 'controller'
382
+ this.routes[Candy.Route.buff][type][url].file = file
383
+ this.routes[Candy.Route.buff][type][url].mtime = isFunction ? Date.now() : fs.statSync(path).mtimeMs
384
+ this.routes[Candy.Route.buff][type][url].path = path
385
+ this.routes[Candy.Route.buff][type][url].loaded = routes2[Candy.Route.buff]
386
+ this.routes[Candy.Route.buff][type][url].token = options.token ?? true
387
+ }
388
+ }
389
+
390
+ page(path, file) {
391
+ if (typeof file === 'object' && !Array.isArray(file)) {
392
+ this.set('page', path, _candy => {
393
+ _candy.View.set(file)
394
+ return
395
+ })
396
+ return
397
+ }
398
+ if (file) this.set('page', path, file)
399
+ }
400
+
401
+ post(path, file, options) {
402
+ this.set('post', path, file, options)
403
+ }
404
+
405
+ get(path, file, options) {
406
+ this.set('get', path, file, options)
407
+ }
408
+
409
+ authPage(path, authFile, file) {
410
+ if (typeof authFile === 'object' && !Array.isArray(authFile)) {
411
+ this.set('#page', path, _candy => {
412
+ _candy.View.set(authFile)
413
+ return
414
+ })
415
+ if (typeof file === 'object' && !Array.isArray(file)) {
416
+ this.set('page', path, _candy => {
417
+ _candy.View.set(file)
418
+ return
419
+ })
420
+ }
421
+ return
422
+ }
423
+ if (authFile) this.set('#page', path, authFile)
424
+ if (file) {
425
+ if (typeof file === 'object' && !Array.isArray(file)) {
426
+ this.set('page', path, _candy => {
427
+ _candy.View.set(file)
428
+ return
429
+ })
430
+ } else {
431
+ this.set('page', path, file)
432
+ }
433
+ }
434
+ }
435
+
436
+ authPost(path, authFile, file) {
437
+ if (authFile) this.set('#post', path, authFile)
438
+ if (file) this.post(path, file)
439
+ }
440
+
441
+ authGet(path, authFile, file) {
442
+ if (authFile) this.set('#get', path, authFile)
443
+ if (file) this.get(path, file)
444
+ }
445
+
446
+ error(code, file) {
447
+ this.set('error', code, file)
448
+ }
449
+
450
+ cron(controller) {
451
+ return Cron.job(controller)
452
+ }
453
+ }
454
+
455
+ module.exports = Route
@@ -0,0 +1,15 @@
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(`CandyPack 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
+ http
10
+ .createServer((req, res) => {
11
+ return Candy.Route.request(req, res)
12
+ })
13
+ .listen(port)
14
+ }
15
+ }
@@ -0,0 +1,163 @@
1
+ class Stream {
2
+ #heartbeat
3
+ #req
4
+ #res
5
+ #closed = false
6
+
7
+ constructor(req, res, input) {
8
+ this.#req = req
9
+ this.#res = res
10
+ this.#init()
11
+ this.#handleInput(input)
12
+ }
13
+
14
+ #init() {
15
+ if (this.#res.headersSent || this.#res.writableEnded) {
16
+ this.#closed = true
17
+ return
18
+ }
19
+
20
+ this.#req.setTimeout(0)
21
+ this.#res.setTimeout(0)
22
+
23
+ this.#res.setHeader('Content-Type', 'text/event-stream')
24
+ this.#res.setHeader('Cache-Control', 'no-cache')
25
+ this.#res.setHeader('Connection', 'keep-alive')
26
+
27
+ this.#heartbeat = setInterval(() => {
28
+ if (!this.#closed) {
29
+ this.#res.write(': heartbeat\n\n')
30
+ }
31
+ }, 30000)
32
+
33
+ this.#req.on('close', () => {
34
+ this.close()
35
+ })
36
+ }
37
+
38
+ #handleInput(input) {
39
+ if (input === undefined) {
40
+ return
41
+ }
42
+
43
+ if (typeof input === 'function') {
44
+ const result = input(
45
+ data => this.send(data),
46
+ () => this.close(),
47
+ this
48
+ )
49
+
50
+ if (result && typeof result[Symbol.asyncIterator] === 'function') {
51
+ this.#pipeAsyncIterator(result)
52
+ } else if (result && typeof result.next === 'function') {
53
+ this.#pipeIterator(result)
54
+ } else if (typeof result === 'function') {
55
+ this.on('close', result)
56
+ }
57
+
58
+ return
59
+ }
60
+
61
+ if (input && typeof input[Symbol.asyncIterator] === 'function') {
62
+ this.#pipeAsyncIterator(input)
63
+ return
64
+ }
65
+
66
+ if (input && typeof input.next === 'function') {
67
+ this.#pipeIterator(input)
68
+ return
69
+ }
70
+
71
+ if (Array.isArray(input) || (input && typeof input[Symbol.iterator] === 'function')) {
72
+ for (const value of input) {
73
+ this.send(value)
74
+ }
75
+ this.close()
76
+ return
77
+ }
78
+
79
+ if (input && typeof input.then === 'function') {
80
+ input
81
+ .then(data => {
82
+ this.send(data)
83
+ this.close()
84
+ })
85
+ .catch(err => {
86
+ this.error(err.message)
87
+ this.close()
88
+ })
89
+ return
90
+ }
91
+
92
+ if (input && typeof input.pipe === 'function') {
93
+ input.on('data', chunk => this.send(chunk.toString()))
94
+ input.on('end', () => this.close())
95
+ input.on('error', err => {
96
+ this.error(err.message)
97
+ this.close()
98
+ })
99
+ return
100
+ }
101
+
102
+ this.send(input)
103
+ this.close()
104
+ }
105
+
106
+ send(data) {
107
+ if (this.#closed) return false
108
+ const message = typeof data === 'string' ? data : JSON.stringify(data)
109
+ this.#res.write(`data: ${message}\n\n`)
110
+ return true
111
+ }
112
+
113
+ error(message) {
114
+ return this.send({error: message})
115
+ }
116
+
117
+ on(event, callback) {
118
+ if (event === 'close') {
119
+ this.#req.on('close', callback)
120
+ }
121
+ }
122
+
123
+ close() {
124
+ if (this.#closed) return
125
+ this.#closed = true
126
+ clearInterval(this.#heartbeat)
127
+ if (!this.#res.writableEnded) {
128
+ this.#res.end()
129
+ }
130
+ }
131
+
132
+ #pipeIterator(iterator) {
133
+ const batchSize = 100
134
+ const iterate = () => {
135
+ let count = 0
136
+ while (count < batchSize) {
137
+ const {value, done} = iterator.next()
138
+ if (done) {
139
+ this.close()
140
+ return
141
+ }
142
+ this.send(value)
143
+ count++
144
+ }
145
+ setImmediate(iterate)
146
+ }
147
+ iterate()
148
+ }
149
+
150
+ async #pipeAsyncIterator(iterator) {
151
+ try {
152
+ for await (const value of iterator) {
153
+ this.send(value)
154
+ }
155
+ this.close()
156
+ } catch (err) {
157
+ this.error(err.message)
158
+ this.close()
159
+ }
160
+ }
161
+ }
162
+
163
+ module.exports = Stream
@@ -0,0 +1,37 @@
1
+ const nodeCrypto = require('crypto')
2
+
3
+ class Token {
4
+ confirmed = []
5
+
6
+ constructor(Request) {
7
+ this.Request = Request
8
+ }
9
+
10
+ // - CHECK TOKEN
11
+ check(token) {
12
+ let tokens = this.Request.session('_token') || []
13
+ if (this.confirmed.includes(token)) return true
14
+ if (tokens.includes(token)) {
15
+ tokens = tokens.filter(t => t !== token)
16
+ this.Request.session('_token', tokens)
17
+ this.confirmed.push(token)
18
+ return true
19
+ }
20
+ return false
21
+ }
22
+
23
+ // - GENERATE TOKEN
24
+ generate() {
25
+ let token = nodeCrypto
26
+ .createHash('md5')
27
+ .update(this.Request.id + Date.now().toString() + Math.random().toString())
28
+ .digest('hex')
29
+ let tokens = this.Request.session('_token') || []
30
+ tokens.push(token)
31
+ if (tokens.length > 50) tokens = tokens.slice(-50)
32
+ this.Request.session('_token', tokens)
33
+ return token
34
+ }
35
+ }
36
+
37
+ module.exports = Token