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,439 @@
1
+ class Internal {
2
+ static #validateField(validator, field, validation, value) {
3
+ const rules = validation.rule.split('|')
4
+ for (const rule of rules) {
5
+ const validatorChain = validator.post(field.name).check(rule)
6
+ if (validation.message) {
7
+ const message = this.replacePlaceholders(validation.message, {
8
+ value: value,
9
+ field: field.name,
10
+ label: field.label || field.placeholder,
11
+ rule: rule
12
+ })
13
+ validatorChain.message(message)
14
+ }
15
+ }
16
+ }
17
+
18
+ static async register(Candy) {
19
+ const token = await Candy.request('_candy_register_token')
20
+ if (!token) {
21
+ return Candy.return({
22
+ result: {success: false},
23
+ errors: {_candy_form: 'Invalid request'}
24
+ })
25
+ }
26
+
27
+ const formData = Candy.Request.session(`_register_form_${token}`)
28
+
29
+ if (!formData) {
30
+ return Candy.return({
31
+ result: {success: false},
32
+ errors: {_candy_form: 'Form session expired. Please refresh the page.'}
33
+ })
34
+ }
35
+
36
+ if (formData.expires < Date.now()) {
37
+ Candy.Request.session(`_register_form_${token}`, null)
38
+ return Candy.return({
39
+ result: {success: false},
40
+ errors: {_candy_form: 'Form session expired. Please refresh the page.'}
41
+ })
42
+ }
43
+
44
+ if (formData.sessionId !== Candy.Request.session('_client')) {
45
+ return Candy.return({
46
+ result: {success: false},
47
+ errors: {_candy_form: 'Invalid session'}
48
+ })
49
+ }
50
+
51
+ if (formData.userAgent !== Candy.Request.header('user-agent')) {
52
+ return Candy.return({
53
+ result: {success: false},
54
+ errors: {_candy_form: 'Invalid request'}
55
+ })
56
+ }
57
+
58
+ if (formData.ip !== Candy.Request.ip) {
59
+ return Candy.return({
60
+ result: {success: false},
61
+ errors: {_candy_form: 'Invalid request'}
62
+ })
63
+ }
64
+
65
+ const config = formData.config
66
+ const validator = Candy.validator()
67
+ const data = {}
68
+
69
+ const uniqueFields = []
70
+
71
+ for (const field of config.fields) {
72
+ const value = await Candy.request(field.name)
73
+
74
+ for (const validation of field.validations) {
75
+ this.#validateField(validator, field, validation, value)
76
+ }
77
+
78
+ if (field.unique) {
79
+ uniqueFields.push(field.name)
80
+ }
81
+
82
+ if (!field.skip) {
83
+ data[field.name] = value
84
+ }
85
+ }
86
+
87
+ for (const set of config.sets) {
88
+ if (set.value !== null) {
89
+ if (set.ifEmpty && data[set.name]) continue
90
+ data[set.name] = set.value
91
+ } else if (set.compute) {
92
+ data[set.name] = await this.computeValue(set.compute, Candy)
93
+ } else if (set.callback) {
94
+ if (typeof Candy.fn[set.callback] === 'function') {
95
+ data[set.name] = await Candy.fn[set.callback](Candy)
96
+ }
97
+ }
98
+ }
99
+
100
+ if (await validator.error()) {
101
+ return validator.result()
102
+ }
103
+
104
+ const registerResult = await Candy.Auth.register(data, {
105
+ autoLogin: config.autologin !== false,
106
+ uniqueFields: uniqueFields.length > 0 ? uniqueFields : ['email']
107
+ })
108
+
109
+ if (!registerResult.success) {
110
+ if (registerResult.error === 'Database connection not configured') {
111
+ return Candy.return({
112
+ result: {success: false},
113
+ errors: {_candy_form: 'Service temporarily unavailable. Please try again later.'}
114
+ })
115
+ }
116
+ const errorField = registerResult.field || '_candy_form'
117
+ const errors = {[errorField]: registerResult.error}
118
+ return Candy.return({
119
+ result: {success: false},
120
+ errors: errors
121
+ })
122
+ }
123
+
124
+ Candy.Request.session(`_register_form_${token}`, null)
125
+
126
+ return Candy.return({
127
+ result: {
128
+ success: true,
129
+ message: 'Registration successful',
130
+ redirect: config.redirect
131
+ }
132
+ })
133
+ }
134
+
135
+ static replacePlaceholders(message, data) {
136
+ if (!message) return message
137
+
138
+ const ruleParts = data.rule ? data.rule.split(':') : []
139
+ const ruleValue = ruleParts[1] || null
140
+
141
+ const placeholders = {
142
+ '{value}': data.value || '',
143
+ '{field}': data.field || '',
144
+ '{label}': data.label || data.field || '',
145
+ '{min}': ruleValue,
146
+ '{max}': ruleValue,
147
+ '{len}': ruleValue,
148
+ '{other}': ruleValue
149
+ }
150
+
151
+ let result = message
152
+ for (const [placeholder, value] of Object.entries(placeholders)) {
153
+ if (value !== null) {
154
+ result = result.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value)
155
+ }
156
+ }
157
+
158
+ return result
159
+ }
160
+
161
+ static async computeValue(type, Candy) {
162
+ switch (type) {
163
+ case 'now':
164
+ return Math.floor(Date.now() / 1000)
165
+ case 'date':
166
+ return new Date().toISOString().split('T')[0]
167
+ case 'datetime':
168
+ return new Date().toISOString()
169
+ case 'timestamp':
170
+ return Date.now()
171
+ case 'ip':
172
+ return Candy.Request.ip
173
+ case 'user_agent':
174
+ return Candy.Request.header('user-agent')
175
+ case 'uuid':
176
+ return require('crypto').randomUUID()
177
+ default:
178
+ return null
179
+ }
180
+ }
181
+
182
+ static async login(Candy) {
183
+ const token = await Candy.request('_candy_login_token')
184
+ if (!token) {
185
+ return Candy.return({
186
+ result: {success: false},
187
+ errors: {_candy_form: 'Invalid request'}
188
+ })
189
+ }
190
+
191
+ const formData = Candy.Request.session(`_login_form_${token}`)
192
+
193
+ if (!formData) {
194
+ return Candy.return({
195
+ result: {success: false},
196
+ errors: {_candy_form: 'Form session expired. Please refresh the page.'}
197
+ })
198
+ }
199
+
200
+ if (formData.expires < Date.now()) {
201
+ Candy.Request.session(`_login_form_${token}`, null)
202
+ return Candy.return({
203
+ result: {success: false},
204
+ errors: {_candy_form: 'Form session expired. Please refresh the page.'}
205
+ })
206
+ }
207
+
208
+ if (formData.sessionId !== Candy.Request.session('_client')) {
209
+ return Candy.return({
210
+ result: {success: false},
211
+ errors: {_candy_form: 'Invalid session'}
212
+ })
213
+ }
214
+
215
+ if (formData.userAgent !== Candy.Request.header('user-agent')) {
216
+ return Candy.return({
217
+ result: {success: false},
218
+ errors: {_candy_form: 'Invalid request'}
219
+ })
220
+ }
221
+
222
+ if (formData.ip !== Candy.Request.ip) {
223
+ return Candy.return({
224
+ result: {success: false},
225
+ errors: {_candy_form: 'Invalid request'}
226
+ })
227
+ }
228
+
229
+ const config = formData.config
230
+ const validator = Candy.validator()
231
+ const credentials = {}
232
+
233
+ for (const field of config.fields) {
234
+ const value = await Candy.request(field.name)
235
+
236
+ for (const validation of field.validations) {
237
+ this.#validateField(validator, field, validation, value)
238
+ }
239
+
240
+ credentials[field.name] = value
241
+ }
242
+
243
+ if (await validator.error()) {
244
+ return validator.result()
245
+ }
246
+
247
+ const loginResult = await Candy.Auth.login(credentials)
248
+
249
+ if (!loginResult.success) {
250
+ if (loginResult.error === 'Database connection not configured') {
251
+ return Candy.return({
252
+ result: {success: false},
253
+ errors: {_candy_form: 'Service temporarily unavailable. Please try again later.'}
254
+ })
255
+ }
256
+ const errorField = loginResult.field || '_candy_form'
257
+ const errors = {[errorField]: loginResult.error}
258
+ return Candy.return({
259
+ result: {success: false},
260
+ errors: errors
261
+ })
262
+ }
263
+
264
+ Candy.Request.session(`_login_form_${token}`, null)
265
+
266
+ return Candy.return({
267
+ result: {
268
+ success: true,
269
+ message: 'Login successful',
270
+ redirect: config.redirect
271
+ }
272
+ })
273
+ }
274
+
275
+ static async processForm(Candy) {
276
+ const token = await Candy.request('_candy_form_token')
277
+ if (!token) return
278
+
279
+ const formData = Candy.Request.session(`_custom_form_${token}`)
280
+ if (!formData) return
281
+
282
+ if (formData.expires < Date.now()) {
283
+ Candy.Request.session(`_custom_form_${token}`, null)
284
+ return
285
+ }
286
+
287
+ if (formData.sessionId !== Candy.Request.session('_client')) return
288
+ if (formData.userAgent !== Candy.Request.header('user-agent')) return
289
+ if (formData.ip !== Candy.Request.ip) return
290
+
291
+ const config = formData.config
292
+ const validator = Candy.validator()
293
+ const data = {}
294
+
295
+ const uniqueFields = []
296
+
297
+ for (const field of config.fields) {
298
+ const value = await Candy.request(field.name)
299
+
300
+ for (const validation of field.validations) {
301
+ this.#validateField(validator, field, validation, value)
302
+
303
+ if (validation.rule.includes('unique')) {
304
+ if (!uniqueFields.some(f => f.name === field.name)) {
305
+ uniqueFields.push({name: field.name, message: validation.message})
306
+ }
307
+ }
308
+ }
309
+
310
+ if (!field.skip) {
311
+ data[field.name] = value
312
+ }
313
+ }
314
+
315
+ for (const set of config.sets || []) {
316
+ if (set.value !== undefined && set.value !== null) {
317
+ if (set.ifEmpty && data[set.name] !== undefined && data[set.name] !== null && data[set.name] !== '') continue
318
+ data[set.name] = set.value
319
+ } else if (set.compute) {
320
+ data[set.name] = await this.computeValue(set.compute, Candy)
321
+ } else if (set.callback) {
322
+ if (typeof Candy.fn[set.callback] === 'function') {
323
+ data[set.name] = await Candy.fn[set.callback](Candy)
324
+ }
325
+ }
326
+ }
327
+
328
+ Candy.formData = data
329
+ Candy.formConfig = config
330
+ Candy.formValidator = validator
331
+ Candy.formUniqueFields = uniqueFields
332
+ }
333
+
334
+ static async customForm(Candy) {
335
+ const token = await Candy.request('_candy_form_token')
336
+ if (!token) {
337
+ return Candy.return({
338
+ result: {success: false},
339
+ errors: {_candy_form: 'Invalid request'}
340
+ })
341
+ }
342
+
343
+ const formData = Candy.Request.session(`_custom_form_${token}`)
344
+
345
+ if (!formData) {
346
+ return Candy.return({
347
+ result: {success: false},
348
+ errors: {_candy_form: 'Form session expired. Please refresh the page.'}
349
+ })
350
+ }
351
+
352
+ if (formData.expires < Date.now()) {
353
+ Candy.Request.session(`_custom_form_${token}`, null)
354
+ return Candy.return({
355
+ result: {success: false},
356
+ errors: {_candy_form: 'Form session expired. Please refresh the page.'}
357
+ })
358
+ }
359
+
360
+ if (formData.sessionId !== Candy.Request.session('_client')) {
361
+ return Candy.return({
362
+ result: {success: false},
363
+ errors: {_candy_form: 'Invalid session'}
364
+ })
365
+ }
366
+
367
+ if (formData.userAgent !== Candy.Request.header('user-agent')) {
368
+ return Candy.return({
369
+ result: {success: false},
370
+ errors: {_candy_form: 'Invalid request'}
371
+ })
372
+ }
373
+
374
+ if (formData.ip !== Candy.Request.ip) {
375
+ return Candy.return({
376
+ result: {success: false},
377
+ errors: {_candy_form: 'Invalid request'}
378
+ })
379
+ }
380
+
381
+ if (await Candy.formValidator.error()) {
382
+ return Candy.formValidator.result()
383
+ }
384
+
385
+ if (Candy.formConfig.table) {
386
+ try {
387
+ const mysql = Candy.Mysql
388
+
389
+ for (const field of Candy.formUniqueFields) {
390
+ if (Candy.formData[field.name] == null) continue
391
+
392
+ const existingRecord = await mysql.query(`SELECT id FROM ?? WHERE ?? = ? LIMIT 1`, [
393
+ Candy.formConfig.table,
394
+ field.name,
395
+ Candy.formData[field.name]
396
+ ])
397
+
398
+ if (existingRecord && existingRecord.length > 0) {
399
+ const errorMessage = field.message || `This ${field.name} is already registered`
400
+ return Candy.return({
401
+ result: {success: false},
402
+ errors: {[field.name]: errorMessage}
403
+ })
404
+ }
405
+ }
406
+
407
+ await mysql.query('INSERT INTO ?? SET ?', [Candy.formConfig.table, Candy.formData])
408
+
409
+ Candy.Request.session(`_custom_form_${token}`, null)
410
+
411
+ return Candy.return({
412
+ result: {
413
+ success: true,
414
+ message: Candy.formConfig.successMessage || 'Form submitted successfully!',
415
+ redirect: Candy.formConfig.redirect
416
+ }
417
+ })
418
+ } catch (error) {
419
+ if (error.message === 'Database connection not configured') {
420
+ return Candy.return({
421
+ result: {success: false},
422
+ errors: {_candy_form: 'Database not configured. Please check your config.json'}
423
+ })
424
+ }
425
+
426
+ return Candy.return({
427
+ result: {success: false},
428
+ errors: {_candy_form: error.message || 'Database error occurred'}
429
+ })
430
+ }
431
+ }
432
+
433
+ Candy.Request.session(`_custom_form_${token}`, null)
434
+
435
+ return null
436
+ }
437
+ }
438
+
439
+ module.exports = Internal