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,23 @@
1
+ ## ✉️ Create a Mail Account
2
+ This command allows you to create a new email account.
3
+
4
+ ### Interactive Usage
5
+ ```bash
6
+ candy mail create
7
+ ```
8
+ You will be prompted to enter the new email address and a password for the account.
9
+
10
+ ### Single-Line Usage with Prefixes
11
+ ```bash
12
+ # Specify email and password directly
13
+ candy mail create -e user@example.com -p mypassword
14
+
15
+ # Or use long form prefixes
16
+ candy mail create --email user@example.com --password mypassword
17
+ ```
18
+
19
+ ### Available Prefixes
20
+ - `-e`, `--email`: Email address for the new account
21
+ - `-p`, `--password`: Password for the new account
22
+
23
+ **Note:** When using the `-p` prefix, you won't be prompted to confirm the password. In interactive mode, you'll need to enter the password twice for confirmation.
@@ -0,0 +1,20 @@
1
+ ## 🗑️ Delete a Mail Account
2
+ This command removes an existing email account.
3
+
4
+ ### Interactive Usage
5
+ ```bash
6
+ candy mail delete
7
+ ```
8
+ You will be prompted to enter the email address you wish to delete.
9
+
10
+ ### Single-Line Usage with Prefixes
11
+ ```bash
12
+ # Specify email directly
13
+ candy mail delete -e user@example.com
14
+
15
+ # Or use long form prefix
16
+ candy mail delete --email user@example.com
17
+ ```
18
+
19
+ ### Available Prefixes
20
+ - `-e`, `--email`: Email address to delete
@@ -0,0 +1,20 @@
1
+ ## 📋 List Mail Accounts
2
+ This command lists all email accounts associated with a specific domain.
3
+
4
+ ### Interactive Usage
5
+ ```bash
6
+ candy mail list
7
+ ```
8
+ You will be prompted to enter the domain name (e.g., `example.com`) to see all its email accounts.
9
+
10
+ ### Single-Line Usage with Prefixes
11
+ ```bash
12
+ # Specify domain directly
13
+ candy mail list -d example.com
14
+
15
+ # Or use long form prefix
16
+ candy mail list --domain example.com
17
+ ```
18
+
19
+ ### Available Prefixes
20
+ - `-d`, `--domain`: Domain name to list email accounts for
@@ -0,0 +1,23 @@
1
+ ## 🔑 Change Account Password
2
+ This command allows you to change the password for an existing email account.
3
+
4
+ ### Interactive Usage
5
+ ```bash
6
+ candy mail password
7
+ ```
8
+ You will be prompted to enter the email address and the new password.
9
+
10
+ ### Single-Line Usage with Prefixes
11
+ ```bash
12
+ # Specify email and new password directly
13
+ candy mail password -e user@example.com -p newpassword
14
+
15
+ # Or use long form prefixes
16
+ candy mail password --email user@example.com --password newpassword
17
+ ```
18
+
19
+ ### Available Prefixes
20
+ - `-e`, `--email`: Email address to change password for
21
+ - `-p`, `--password`: New password for the account
22
+
23
+ **Note:** When using the `-p` prefix, you won't be prompted to confirm the password. In interactive mode, you'll need to enter the password twice for confirmation.
@@ -0,0 +1,120 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import {defineConfig} from 'eslint/config'
4
+ import prettierPlugin from 'eslint-plugin-prettier'
5
+ import prettierConfig from 'eslint-config-prettier'
6
+
7
+ export default defineConfig([
8
+ {
9
+ files: ['core/**/*.js', 'watchdog/**/*.js', 'server/**/*.js', 'cli/**/*.js'],
10
+ ignores: ['server/src/Candy.js'],
11
+ languageOptions: {
12
+ globals: {
13
+ ...globals.node,
14
+ Candy: 'readonly',
15
+ __: 'readonly'
16
+ },
17
+ sourceType: 'script'
18
+ },
19
+ plugins: {
20
+ js,
21
+ prettier: prettierPlugin
22
+ },
23
+ rules: {
24
+ ...js.configs.recommended.rules,
25
+ ...prettierConfig.rules,
26
+ 'prettier/prettier': 'error'
27
+ }
28
+ },
29
+ {
30
+ files: ['server/src/Candy.js'],
31
+ languageOptions: {
32
+ globals: {
33
+ ...globals.node,
34
+ log: 'readonly',
35
+ __: 'readonly'
36
+ },
37
+ sourceType: 'script'
38
+ },
39
+ plugins: {
40
+ js,
41
+ prettier: prettierPlugin
42
+ },
43
+ rules: {
44
+ ...js.configs.recommended.rules,
45
+ ...prettierConfig.rules,
46
+ 'prettier/prettier': 'error'
47
+ }
48
+ },
49
+ {
50
+ files: ['framework/**/*.js'],
51
+ ignores: ['framework/web/**/*.js'],
52
+ languageOptions: {
53
+ globals: {
54
+ ...globals.node,
55
+ Candy: 'readonly',
56
+ __dir: 'readonly'
57
+ },
58
+ sourceType: 'script'
59
+ },
60
+ plugins: {
61
+ js,
62
+ prettier: prettierPlugin
63
+ },
64
+ rules: {
65
+ ...js.configs.recommended.rules,
66
+ ...prettierConfig.rules,
67
+ 'prettier/prettier': 'error'
68
+ }
69
+ },
70
+ {
71
+ files: ['framework/web/**/*.js'],
72
+ languageOptions: {
73
+ globals: {...globals.browser},
74
+ sourceType: 'module'
75
+ },
76
+ plugins: {js},
77
+ rules: {
78
+ ...js.configs.recommended.rules
79
+ }
80
+ },
81
+ {
82
+ files: ['web/**/*.js'],
83
+ ignores: ['web/public/**/*.js'],
84
+ languageOptions: {
85
+ globals: {
86
+ ...globals.node,
87
+ Candy: 'readonly'
88
+ },
89
+ sourceType: 'script'
90
+ },
91
+ plugins: {
92
+ js,
93
+ prettier: prettierPlugin
94
+ },
95
+ rules: {
96
+ ...js.configs.recommended.rules,
97
+ ...prettierConfig.rules,
98
+ 'prettier/prettier': 'error'
99
+ }
100
+ },
101
+ {
102
+ files: ['web/public/**/*.js'],
103
+ languageOptions: {
104
+ globals: {
105
+ ...globals.browser,
106
+ Candy: 'readonly'
107
+ },
108
+ sourceType: 'script'
109
+ },
110
+ plugins: {
111
+ js,
112
+ prettier: prettierPlugin
113
+ },
114
+ rules: {
115
+ ...js.configs.recommended.rules,
116
+ ...prettierConfig.rules,
117
+ 'prettier/prettier': 'error'
118
+ }
119
+ }
120
+ ])
@@ -0,0 +1,4 @@
1
+ 'use strict'
2
+
3
+ global.__dir = process.cwd()
4
+ require('./src/Candy.js').init()
@@ -0,0 +1,309 @@
1
+ class Auth {
2
+ #request = null
3
+ #table = null
4
+ #user = null
5
+
6
+ constructor(request) {
7
+ this.#request = request
8
+ }
9
+
10
+ #validateInput(where) {
11
+ if (!where || typeof where !== 'object') return false
12
+ for (const key in where) {
13
+ const value = where[key]
14
+ if (value instanceof Promise) continue
15
+ if (typeof value !== 'string' && typeof value !== 'number') return false
16
+ }
17
+ return true
18
+ }
19
+
20
+ async check(where) {
21
+ if (!Candy.Config.auth) Candy.Config.auth = {}
22
+ this.#table = Candy.Config.auth.table || 'users'
23
+ if (!this.#table) return false
24
+ if (where) {
25
+ if (!this.#validateInput(where)) return false
26
+ let sql = Candy.Mysql.table(this.#table)
27
+ if (!sql) {
28
+ console.error('CandyPack Auth Error: MySQL connection not configured. Please add database configuration to your config.json')
29
+ return false
30
+ }
31
+ for (let key in where) sql = sql.orWhere(key, where[key] instanceof Promise ? await where[key] : where[key])
32
+ if (!sql.rows()) return false
33
+ let get = await sql.get()
34
+ let equal = false
35
+ for (var user of get) {
36
+ equal = Object.keys(where).length > 0
37
+ for (let key of Object.keys(where)) {
38
+ if (where[key] instanceof Promise) where[key] = await where[key]
39
+ if (!user[key]) equal = false
40
+ if (user[key] === where[key]) equal = equal && true
41
+ else if (Candy.Var(user[key]).is('bcrypt')) equal = equal && Candy.Var(user[key]).hashCheck(where[key])
42
+ else if (Candy.Var(user[key]).is('md5')) equal = equal && Candy.Var(where[key]).md5() === user[key]
43
+ }
44
+ if (equal) break
45
+ }
46
+ if (!equal) return false
47
+ return user
48
+ } else if (this.#user) {
49
+ return true
50
+ } else {
51
+ let check_table = await Candy.Mysql.run('SHOW TABLES LIKE ?', [this.#table])
52
+ if (check_table.length == 0) return false
53
+ let candy_x = this.#request.cookie('candy_x')
54
+ let candy_y = this.#request.cookie('candy_y')
55
+ let browser = this.#request.header('user-agent')
56
+ if (!candy_x || !candy_y || !browser) return false
57
+ const tokenTable = Candy.Config.auth.token || 'candy_auth'
58
+ const primaryKey = Candy.Config.auth.key || 'id'
59
+ let sql_token = await Candy.Mysql.table(tokenTable).where(['token_x', candy_x], ['browser', browser]).get()
60
+ if (sql_token.length !== 1) return false
61
+ if (!Candy.Var(sql_token[0].token_y).hashCheck(candy_y)) return false
62
+
63
+ const maxAge = Candy.Config.auth?.maxAge || 30 * 24 * 60 * 60 * 1000
64
+ const updateAge = Candy.Config.auth?.updateAge || 24 * 60 * 60 * 1000
65
+ const now = Date.now()
66
+ const lastActive = new Date(sql_token[0].active).getTime()
67
+ const inactiveAge = now - lastActive
68
+
69
+ if (inactiveAge > maxAge) {
70
+ await Candy.Mysql.table(tokenTable).where('id', sql_token[0].id).delete()
71
+ return false
72
+ }
73
+
74
+ this.#user = await Candy.Mysql.table(this.#table).where(primaryKey, sql_token[0].user).first()
75
+
76
+ if (inactiveAge > updateAge) {
77
+ Candy.Mysql.table(tokenTable)
78
+ .where('id', sql_token[0].id)
79
+ .set({active: new Date()})
80
+ .catch(() => {})
81
+ }
82
+
83
+ return true
84
+ }
85
+ }
86
+
87
+ async login(where) {
88
+ this.#user = null
89
+ let user = await this.check(where)
90
+ if (!user) return false
91
+ if (!Candy.Config.auth) Candy.Config.auth = {}
92
+ let key = Candy.Config.auth.key || 'id'
93
+ let token = Candy.Config.auth.token || 'candy_auth'
94
+ const mysql = require('mysql2')
95
+ const safeTokenTable = mysql.escapeId(token)
96
+ let check_table = await Candy.Mysql.run('SHOW TABLES LIKE ?', [token])
97
+ if (check_table === false) {
98
+ console.error('CandyPack Auth Error: MySQL connection not configured. Please add database configuration to your config.json')
99
+ return false
100
+ }
101
+ if (check_table.length == 0)
102
+ await Candy.Mysql.run(
103
+ `CREATE TABLE ${safeTokenTable} (id INT NOT NULL AUTO_INCREMENT, user INT NOT NULL, token_x VARCHAR(255) NOT NULL, token_y VARCHAR(255) NOT NULL, browser VARCHAR(255) NOT NULL, ip VARCHAR(255) NOT NULL, \`date\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \`active\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id))`
104
+ )
105
+
106
+ this.#cleanupExpiredTokens(token)
107
+
108
+ let token_y = Candy.Var(Math.random().toString() + Date.now().toString() + this.#request.id + this.#request.ip).md5()
109
+ let cookie = {
110
+ user: user[key],
111
+ token_x: Candy.Var(Math.random().toString() + Date.now().toString()).md5(),
112
+ token_y: Candy.Var(token_y).hash(),
113
+ browser: this.#request.header('user-agent'),
114
+ ip: this.#request.ip
115
+ }
116
+ this.#request.cookie('candy_x', cookie.token_x, {
117
+ httpOnly: true,
118
+ secure: true,
119
+ sameSite: 'Strict'
120
+ })
121
+ this.#request.cookie('candy_y', token_y, {httpOnly: true, secure: true, sameSite: 'Strict'})
122
+ let mysqlTable = Candy.Mysql.table(token)
123
+ if (!mysqlTable) {
124
+ console.error('CandyPack Auth Error: MySQL connection not configured. Please add database configuration to your config.json')
125
+ return false
126
+ }
127
+ let sql = await mysqlTable.insert(cookie)
128
+ return sql !== false
129
+ }
130
+
131
+ async #cleanupExpiredTokens(tokenTable) {
132
+ const maxAge = Candy.Config.auth?.maxAge || 30 * 24 * 60 * 60 * 1000
133
+ const cutoffDate = new Date(Date.now() - maxAge)
134
+
135
+ Candy.Mysql.table(tokenTable)
136
+ .where('active', '<', cutoffDate)
137
+ .delete()
138
+ .catch(() => {})
139
+ }
140
+
141
+ async register(data, options = {}) {
142
+ if (!Candy.Config.auth) {
143
+ Candy.Config.auth = {}
144
+ }
145
+
146
+ this.#table = Candy.Config.auth.table || 'users'
147
+ const primaryKey = Candy.Config.auth.key || 'id'
148
+ const passwordField = options.passwordField || 'password'
149
+ const uniqueFields = options.uniqueFields || ['email']
150
+
151
+ const checkTable = await Candy.Mysql.run('SHOW TABLES LIKE ?', [this.#table])
152
+ if (checkTable === false) {
153
+ console.error('CandyPack Auth Error: MySQL connection not configured. Please add database configuration to your config.json')
154
+ return {success: false, error: 'Database connection not configured'}
155
+ }
156
+ if (checkTable.length === 0) {
157
+ await this.#createUserTable(this.#table, primaryKey, passwordField, uniqueFields, data)
158
+ }
159
+
160
+ if (!data || typeof data !== 'object') {
161
+ return {success: false, error: 'Invalid data provided'}
162
+ }
163
+
164
+ if (data[passwordField] && !Candy.Var(data[passwordField]).is('bcrypt')) {
165
+ data[passwordField] = Candy.Var(data[passwordField]).hash()
166
+ }
167
+
168
+ for (const field of uniqueFields) {
169
+ if (data[field]) {
170
+ const mysqlTable = Candy.Mysql.table(this.#table)
171
+ if (!mysqlTable) {
172
+ console.error('CandyPack Auth Error: MySQL connection not configured. Please add database configuration to your config.json')
173
+ return {success: false, error: 'Database connection not configured'}
174
+ }
175
+ const existing = await mysqlTable.where(field, data[field]).first()
176
+ if (existing) {
177
+ return {success: false, error: `${field} already exists`, field}
178
+ }
179
+ }
180
+ }
181
+
182
+ try {
183
+ const mysqlTable = Candy.Mysql.table(this.#table)
184
+ if (!mysqlTable) {
185
+ console.error('CandyPack Auth Error: MySQL connection not configured. Please add database configuration to your config.json')
186
+ return {success: false, error: 'Database connection not configured'}
187
+ }
188
+ const insertResult = await mysqlTable.insert(data)
189
+ if (insertResult === false) {
190
+ console.error('CandyPack Auth Error: Failed to insert user into database - query failed')
191
+ console.error('Data attempted to insert:', {...data, [passwordField]: '[REDACTED]'})
192
+ return {success: false, error: 'Failed to create user'}
193
+ }
194
+ if (!insertResult.affected || insertResult.affected === 0) {
195
+ console.error('CandyPack Auth Error: Insert query succeeded but no rows were affected')
196
+ console.error('Insert result:', insertResult)
197
+ console.error('Data attempted to insert:', {...data, [passwordField]: '[REDACTED]'})
198
+ return {success: false, error: 'Failed to create user'}
199
+ }
200
+
201
+ const userId = insertResult.id
202
+ const newUser = await Candy.Mysql.table(this.#table).where(primaryKey, userId).first()
203
+
204
+ if (!newUser) {
205
+ return {success: false, error: 'User created but could not be retrieved'}
206
+ }
207
+
208
+ delete newUser[passwordField]
209
+
210
+ if (options.autoLogin !== false) {
211
+ const loginData = {}
212
+ loginData[primaryKey] = userId
213
+ const loginSuccess = await this.login(loginData)
214
+
215
+ if (!loginSuccess) {
216
+ return {success: true, user: newUser, autoLogin: false, message: 'User created but auto-login failed'}
217
+ }
218
+ }
219
+
220
+ return {success: true, user: newUser}
221
+ } catch (error) {
222
+ console.error('CandyPack Auth Error: Registration failed with exception')
223
+ console.error('Error:', error.message)
224
+ console.error('Stack:', error.stack)
225
+ return {success: false, error: error.message || 'Registration failed'}
226
+ }
227
+ }
228
+
229
+ async logout() {
230
+ if (!this.#user) return false
231
+
232
+ if (!Candy.Config.auth) Candy.Config.auth = {}
233
+ const token = Candy.Config.auth.token || 'user_tokens'
234
+ const candyX = this.#request.cookie('candy_x')
235
+ const browser = this.#request.header('user-agent')
236
+
237
+ if (candyX && browser) {
238
+ const mysqlTable = Candy.Mysql.table(token)
239
+ if (mysqlTable) {
240
+ await mysqlTable.where(['token_x', candyX], ['browser', browser]).delete()
241
+ }
242
+ }
243
+
244
+ this.#request.cookie('candy_x', '', {maxAge: -1})
245
+ this.#request.cookie('candy_y', '', {maxAge: -1})
246
+
247
+ this.#user = null
248
+ return true
249
+ }
250
+
251
+ async #createUserTable(tableName, primaryKey, passwordField, uniqueFields, sampleData) {
252
+ const mysql = require('mysql2')
253
+ const columns = []
254
+
255
+ const safePrimaryKey = mysql.escapeId(primaryKey)
256
+ columns.push(`${safePrimaryKey} INT NOT NULL AUTO_INCREMENT`)
257
+
258
+ for (const field of uniqueFields) {
259
+ if (field !== primaryKey) {
260
+ const safeField = mysql.escapeId(field)
261
+ columns.push(`${safeField} VARCHAR(255) NOT NULL UNIQUE`)
262
+ }
263
+ }
264
+
265
+ if (!uniqueFields.includes(passwordField) && passwordField !== primaryKey) {
266
+ const safePasswordField = mysql.escapeId(passwordField)
267
+ columns.push(`${safePasswordField} VARCHAR(255) NOT NULL`)
268
+ }
269
+
270
+ for (const key in sampleData) {
271
+ if (key === primaryKey || uniqueFields.includes(key) || key === passwordField) continue
272
+
273
+ const value = sampleData[key]
274
+ let columnType = 'VARCHAR(255)'
275
+
276
+ if (typeof value === 'number') {
277
+ if (Number.isInteger(value)) {
278
+ columnType = value > 2147483647 ? 'BIGINT' : 'INT'
279
+ } else {
280
+ columnType = 'DECIMAL(10,2)'
281
+ }
282
+ } else if (typeof value === 'boolean') {
283
+ columnType = 'TINYINT(1)'
284
+ } else if (value && value.length > 255) {
285
+ columnType = 'TEXT'
286
+ }
287
+
288
+ const safeKey = mysql.escapeId(key)
289
+ columns.push(`${safeKey} ${columnType} NULL`)
290
+ }
291
+
292
+ columns.push(`${mysql.escapeId('created_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP`)
293
+ columns.push(`${mysql.escapeId('updated_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`)
294
+ columns.push(`PRIMARY KEY (${safePrimaryKey})`)
295
+
296
+ const safeTableName = mysql.escapeId(tableName)
297
+ const sql = `CREATE TABLE ${safeTableName} (${columns.join(', ')}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
298
+
299
+ await Candy.Mysql.run(sql)
300
+ }
301
+
302
+ user(col) {
303
+ if (!this.#user) return false
304
+ if (col === null) return this.#user
305
+ else return this.#user[col]
306
+ }
307
+ }
308
+
309
+ module.exports = Auth
@@ -0,0 +1,81 @@
1
+ module.exports = {
2
+ init: async function () {
3
+ global.Candy = this.instance()
4
+ await global.Candy.Env.init()
5
+ await global.Candy.Config.init()
6
+ await global.Candy.Mysql.init()
7
+ await global.Candy.Route.init()
8
+ await global.Candy.Server.init()
9
+ global.Candy.instance = this.instance
10
+ global.__ = value => {
11
+ return value
12
+ }
13
+ },
14
+
15
+ instance(id, req, res) {
16
+ let _candy = {}
17
+
18
+ _candy.Config = require('./Config.js')
19
+ _candy.Env = require('./Env.js')
20
+ _candy.Mail = (...args) => new (require('./Mail.js'))(...args)
21
+ _candy.Mysql = require('./Mysql.js')
22
+ _candy.Route = global.Candy?.Route ?? new (require('./Route.js'))()
23
+ _candy.Server = require('./Server.js')
24
+ _candy.Var = (...args) => new (require('./Var.js'))(...args)
25
+
26
+ if (req && res) {
27
+ _candy.Request = new (require('./Request.js'))(id, req, res, _candy)
28
+ _candy.Auth = new (require('./Auth.js'))(_candy.Request)
29
+ _candy.Token = new (require('./Token.js'))(_candy.Request)
30
+ _candy.Lang = new (require('./Lang.js'))(_candy)
31
+ _candy.View = new (require('./View.js'))(_candy)
32
+
33
+ if (global.Candy?.Route?.class) {
34
+ for (const name in global.Candy.Route.class) {
35
+ const Module = global.Candy.Route.class[name].module
36
+ _candy[name] = typeof Module === 'function' ? new Module(_candy) : Module
37
+ }
38
+ }
39
+
40
+ _candy.__ = function (...args) {
41
+ return _candy.Lang.get(...args)
42
+ }
43
+ _candy.abort = function (code) {
44
+ return _candy.Request.abort(code)
45
+ }
46
+ _candy.cookie = function (key, value, options) {
47
+ return _candy.Request.cookie(key, value, options)
48
+ }
49
+ _candy.direct = function (url) {
50
+ return _candy.Request.redirect(url)
51
+ }
52
+ _candy.env = function (key, defaultValue) {
53
+ return _candy.Env.get(key, defaultValue)
54
+ }
55
+ _candy.return = function (data) {
56
+ return _candy.Request.end(data)
57
+ }
58
+ _candy.request = function (key) {
59
+ return _candy.Request.request(key)
60
+ }
61
+ _candy.set = function (key, value) {
62
+ return _candy.Request.set(key, value)
63
+ }
64
+ _candy.token = function (hash) {
65
+ return hash ? _candy.Token.check(hash) : _candy.Token.generate()
66
+ }
67
+ _candy.validator = function () {
68
+ return new (require('./Validator.js'))(_candy.Request)
69
+ }
70
+ _candy.write = function (value) {
71
+ return _candy.Request.write(value)
72
+ }
73
+ _candy.stream = function (input) {
74
+ _candy.Request.clearTimeout()
75
+ return new (require('./Stream'))(_candy.Request.req, _candy.Request.res, input)
76
+ }
77
+ }
78
+
79
+ return _candy
80
+ }
81
+ }
@@ -0,0 +1,79 @@
1
+ const nodeCrypto = require('crypto')
2
+ const fs = require('fs')
3
+ const os = require('os')
4
+
5
+ module.exports = {
6
+ auth: {
7
+ key: 'id',
8
+ token: 'candy_auth'
9
+ },
10
+ request: {
11
+ timeout: 10000
12
+ },
13
+ encrypt: {
14
+ key: 'candy'
15
+ },
16
+ earlyHints: {
17
+ enabled: true,
18
+ auto: true,
19
+ maxResources: 5
20
+ },
21
+
22
+ init: function () {
23
+ try {
24
+ this.system = JSON.parse(fs.readFileSync(os.homedir() + '/.candypack/config.json'))
25
+ } catch {
26
+ this.system = {}
27
+ }
28
+
29
+ if (fs.existsSync(__dir + '/config.json')) {
30
+ let config = {}
31
+ try {
32
+ config = JSON.parse(fs.readFileSync(__dir + '/config.json'))
33
+ config = this._interpolate(config)
34
+ } catch (err) {
35
+ console.error('Error reading config file:', __dir + '/config.json', err.message)
36
+ }
37
+ this._deepMerge(this, config)
38
+ }
39
+ this.encrypt.key = nodeCrypto.createHash('md5').update(this.encrypt.key).digest('hex')
40
+ },
41
+
42
+ _interpolate: function (obj) {
43
+ if (typeof obj === 'string') {
44
+ return obj.replace(/\$\{(\w+)\}/g, (_, key) => {
45
+ // Special variables
46
+ if (key === 'candy') {
47
+ return __dirname.replace(/\/framework\/src$/, '')
48
+ }
49
+ // Environment variables
50
+ return process.env[key] || ''
51
+ })
52
+ }
53
+ if (Array.isArray(obj)) {
54
+ return obj.map(item => this._interpolate(item))
55
+ }
56
+ if (obj && typeof obj === 'object') {
57
+ const result = {}
58
+ for (const key of Object.keys(obj)) {
59
+ result[key] = this._interpolate(obj[key])
60
+ }
61
+ return result
62
+ }
63
+ return obj
64
+ },
65
+
66
+ _deepMerge: function (target, source) {
67
+ for (const key of Object.keys(source)) {
68
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
69
+ // Ensure target[key] is also an object before recursive merge
70
+ if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
71
+ target[key] = {}
72
+ }
73
+ this._deepMerge(target[key], source[key])
74
+ } else {
75
+ target[key] = source[key]
76
+ }
77
+ }
78
+ }
79
+ }