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.
- package/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- 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,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
|
+
}
|