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,424 @@
|
|
|
1
|
+
## ✅ The `Validator` Service
|
|
2
|
+
|
|
3
|
+
The Validator service provides a fluent, chainable API for validating user input. It's automatically available in your controllers through `Candy.Validator`.
|
|
4
|
+
|
|
5
|
+
#### Basic Usage
|
|
6
|
+
|
|
7
|
+
The validator uses a method-chaining pattern:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const validator = Candy.Validator
|
|
11
|
+
validator.post('email').check('required|email').message('Valid email required')
|
|
12
|
+
validator.post('password').check('required|minlen:8').message('Password must be at least 8 characters')
|
|
13
|
+
|
|
14
|
+
if (await validator.error()) {
|
|
15
|
+
return validator.result('Validation failed')
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
#### Available Methods
|
|
20
|
+
|
|
21
|
+
- `post(key)` - Validate a POST field
|
|
22
|
+
- `get(key)` - Validate a GET field
|
|
23
|
+
- `var(name, value)` - Validate a custom variable
|
|
24
|
+
- `file(name)` - Validate a file upload
|
|
25
|
+
- `check(rules)` - Define validation rules (pipe-separated)
|
|
26
|
+
- `message(text)` - Set custom error message
|
|
27
|
+
- `error()` - Returns true if validation failed (async)
|
|
28
|
+
- `result(message, data)` - Returns formatted result object (async)
|
|
29
|
+
- `success(callback)` - Returns success result with data
|
|
30
|
+
- `brute(maxAttempts)` - Enable brute force protection (default: 5 attempts)
|
|
31
|
+
|
|
32
|
+
#### Validation Rules
|
|
33
|
+
|
|
34
|
+
**Type Validation:**
|
|
35
|
+
- `required` - Field cannot be empty
|
|
36
|
+
- `accepted` - Must be 1, 'on', 'yes', or true
|
|
37
|
+
- `numeric` - Must be a number
|
|
38
|
+
- `alpha` - Only alphabetic characters
|
|
39
|
+
- `alphaspace` - Alphabetic characters and spaces
|
|
40
|
+
- `alphanumeric` - Alphanumeric characters only
|
|
41
|
+
- `alphanumericspace` - Alphanumeric characters and spaces
|
|
42
|
+
- `username` - Alphanumeric username (no spaces or special chars)
|
|
43
|
+
- `email` - Valid email address
|
|
44
|
+
- `ip` - Valid IP address
|
|
45
|
+
- `float` - Floating point number
|
|
46
|
+
- `mac` - Valid MAC address
|
|
47
|
+
- `domain` - Valid domain name
|
|
48
|
+
- `url` - Valid URL
|
|
49
|
+
- `array` - Must be an array
|
|
50
|
+
- `date` - Valid date format
|
|
51
|
+
|
|
52
|
+
**Length Validation:**
|
|
53
|
+
- `len:X` - Exact length must be X
|
|
54
|
+
- `minlen:X` - Minimum length of X characters
|
|
55
|
+
- `maxlen:X` - Maximum length of X characters
|
|
56
|
+
|
|
57
|
+
**Value Validation:**
|
|
58
|
+
- `min:X` - Minimum value of X
|
|
59
|
+
- `max:X` - Maximum value of X
|
|
60
|
+
- `equal:value` - Must equal specific value
|
|
61
|
+
- `not:value` - Must not equal specific value
|
|
62
|
+
- `same:field` - Must match another field
|
|
63
|
+
- `different:field` - Must differ from another field
|
|
64
|
+
|
|
65
|
+
**Date Validation:**
|
|
66
|
+
- `mindate:date` - Must be after specified date
|
|
67
|
+
- `maxdate:date` - Must be before specified date
|
|
68
|
+
|
|
69
|
+
**String Validation:**
|
|
70
|
+
- `in:substring` - Must contain substring
|
|
71
|
+
- `notin:substring` - Must not contain substring
|
|
72
|
+
- `regex:pattern` - Must match regex pattern
|
|
73
|
+
|
|
74
|
+
**Security:**
|
|
75
|
+
- `xss` - Check for HTML tags (XSS protection)
|
|
76
|
+
- `usercheck` - User must be authenticated
|
|
77
|
+
- `user:field` - Must match authenticated user's field value
|
|
78
|
+
|
|
79
|
+
**Inverse Rules:**
|
|
80
|
+
Use `!` prefix to invert any rule: `!required`, `!email`, etc.
|
|
81
|
+
|
|
82
|
+
#### Example: User Registration
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
module.exports = async function (Candy) {
|
|
86
|
+
const validator = Candy.Validator
|
|
87
|
+
|
|
88
|
+
validator.post('username').check('required|username|minlen:4|maxlen:20').message('Username must be 4-20 alphanumeric characters')
|
|
89
|
+
validator.post('email').check('required|email').message('Valid email address required')
|
|
90
|
+
validator.post('password').check('required|minlen:8').message('Password must be at least 8 characters')
|
|
91
|
+
validator.post('password_confirm').check('required|same:password').message('Passwords must match')
|
|
92
|
+
|
|
93
|
+
if (await validator.error()) {
|
|
94
|
+
return validator.result('Validation failed')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return validator.success('User registered successfully')
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Example: Login with Brute Force Protection
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
module.exports = async function (Candy) {
|
|
105
|
+
const validator = Candy.Validator
|
|
106
|
+
|
|
107
|
+
validator.post('email').check('required|email').message('Email required')
|
|
108
|
+
validator.post('password').check('required').message('Password required')
|
|
109
|
+
validator.brute(5)
|
|
110
|
+
|
|
111
|
+
if (await validator.error()) {
|
|
112
|
+
return validator.result('Login failed')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return validator.success({token: 'abc123'})
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### Example: Custom Variable Validation
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
module.exports = async function (Candy) {
|
|
123
|
+
const validator = Candy.Validator
|
|
124
|
+
const customValue = calculateSomething()
|
|
125
|
+
|
|
126
|
+
validator.var('calculated_value', customValue).check('numeric|min:100|max:1000').message('Value must be between 100 and 1000')
|
|
127
|
+
|
|
128
|
+
if (await validator.error()) {
|
|
129
|
+
return validator.result('Invalid calculation')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return validator.success('Calculation valid')
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Multiple Checks Per Field
|
|
137
|
+
|
|
138
|
+
You can chain multiple `check()` calls for the same field, each with its own specific error message. The validator will return the first error it encounters:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
module.exports = async function (Candy) {
|
|
142
|
+
const validator = Candy.Validator
|
|
143
|
+
|
|
144
|
+
validator
|
|
145
|
+
.post('password')
|
|
146
|
+
.check('required').message('Password is required')
|
|
147
|
+
.check('minlen:8').message('Password must be at least 8 characters')
|
|
148
|
+
.check('maxlen:50').message('Password cannot exceed 50 characters')
|
|
149
|
+
.check('regex:[A-Z]').message('Password must contain at least one uppercase letter')
|
|
150
|
+
.check('regex:[0-9]').message('Password must contain at least one number')
|
|
151
|
+
|
|
152
|
+
if (await validator.error()) {
|
|
153
|
+
return validator.result('Validation failed')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return validator.success('Password is strong')
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Example: Complex Form Validation
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
module.exports = async function (Candy) {
|
|
164
|
+
const validator = Candy.Validator
|
|
165
|
+
|
|
166
|
+
validator
|
|
167
|
+
.post('username')
|
|
168
|
+
.check('required').message('Username is required')
|
|
169
|
+
.check('username').message('Username can only contain letters and numbers')
|
|
170
|
+
.check('minlen:3').message('Username must be at least 3 characters')
|
|
171
|
+
.check('maxlen:20').message('Username cannot exceed 20 characters')
|
|
172
|
+
|
|
173
|
+
validator
|
|
174
|
+
.post('email')
|
|
175
|
+
.check('required').message('Email is required')
|
|
176
|
+
.check('email').message('Please enter a valid email address')
|
|
177
|
+
|
|
178
|
+
validator
|
|
179
|
+
.post('age')
|
|
180
|
+
.check('required').message('Age is required')
|
|
181
|
+
.check('numeric').message('Age must be a number')
|
|
182
|
+
.check('min:18').message('You must be at least 18 years old')
|
|
183
|
+
.check('max:120').message('Please enter a valid age')
|
|
184
|
+
|
|
185
|
+
validator
|
|
186
|
+
.post('website')
|
|
187
|
+
.check('!required').message('Website is optional')
|
|
188
|
+
.check('url').message('Please enter a valid URL')
|
|
189
|
+
|
|
190
|
+
validator
|
|
191
|
+
.post('bio')
|
|
192
|
+
.check('maxlen:500').message('Bio cannot exceed 500 characters')
|
|
193
|
+
.check('xss').message('Bio contains invalid HTML tags')
|
|
194
|
+
|
|
195
|
+
if (await validator.error()) {
|
|
196
|
+
return validator.result('Please fix the errors')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return validator.success('Profile updated successfully')
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Example: Date Range Validation
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
module.exports = async function (Candy) {
|
|
207
|
+
const validator = Candy.Validator
|
|
208
|
+
|
|
209
|
+
validator
|
|
210
|
+
.post('start_date')
|
|
211
|
+
.check('required').message('Start date is required')
|
|
212
|
+
.check('date').message('Invalid date format')
|
|
213
|
+
.check('mindate:2024-01-01').message('Start date must be after January 1, 2024')
|
|
214
|
+
|
|
215
|
+
validator
|
|
216
|
+
.post('end_date')
|
|
217
|
+
.check('required').message('End date is required')
|
|
218
|
+
.check('date').message('Invalid date format')
|
|
219
|
+
.check('maxdate:2025-12-31').message('End date must be before December 31, 2025')
|
|
220
|
+
|
|
221
|
+
if (await validator.error()) {
|
|
222
|
+
return validator.result('Invalid date range')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return validator.success('Date range is valid')
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Example: Conditional Validation with Custom Variables
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
module.exports = async function (Candy) {
|
|
233
|
+
const validator = Candy.Validator
|
|
234
|
+
const userRole = Candy.Auth.user('role')
|
|
235
|
+
const userCredits = Candy.Auth.user('credits') || 0
|
|
236
|
+
|
|
237
|
+
validator.post('title').check('required').message('Title is required')
|
|
238
|
+
validator.post('content').check('required').message('Content is required')
|
|
239
|
+
|
|
240
|
+
if (userRole !== 'premium') {
|
|
241
|
+
validator
|
|
242
|
+
.var('user_credits', userCredits)
|
|
243
|
+
.check('numeric').message('Invalid credits value')
|
|
244
|
+
.check('min:10').message('You need at least 10 credits to publish')
|
|
245
|
+
|
|
246
|
+
validator
|
|
247
|
+
.post('content')
|
|
248
|
+
.check('maxlen:1000').message('Free users are limited to 1000 characters')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
validator
|
|
252
|
+
.var('role_check', userRole)
|
|
253
|
+
.check('!equal:banned').message('Your account has been banned')
|
|
254
|
+
|
|
255
|
+
if (await validator.error()) {
|
|
256
|
+
return validator.result('Validation failed')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return validator.success('Article published')
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Example: User Authentication Validation
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
module.exports = async function (Candy) {
|
|
267
|
+
const validator = Candy.Validator
|
|
268
|
+
|
|
269
|
+
validator
|
|
270
|
+
.post('current_password')
|
|
271
|
+
.check('required').message('Current password is required')
|
|
272
|
+
.check('user:password').message('Current password is incorrect')
|
|
273
|
+
|
|
274
|
+
validator
|
|
275
|
+
.post('new_password')
|
|
276
|
+
.check('required').message('New password is required')
|
|
277
|
+
.check('minlen:8').message('New password must be at least 8 characters')
|
|
278
|
+
.check('different:current_password').message('New password must be different from current')
|
|
279
|
+
|
|
280
|
+
if (await validator.error()) {
|
|
281
|
+
return validator.result('Password change failed')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return validator.success('Password changed successfully')
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### Example: Boolean Function Results
|
|
289
|
+
|
|
290
|
+
You can use boolean values directly in `check()` for custom validation logic:
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
module.exports = async function (Candy) {
|
|
294
|
+
const validator = Candy.Validator
|
|
295
|
+
const userId = await Candy.request('user_id')
|
|
296
|
+
|
|
297
|
+
const isOwner = await checkIfUserOwnsResource(userId)
|
|
298
|
+
const hasPermission = await checkUserPermission('edit')
|
|
299
|
+
const isWithinLimit = await checkDailyLimit(userId)
|
|
300
|
+
|
|
301
|
+
validator.post('title').check('required').message('Title is required')
|
|
302
|
+
|
|
303
|
+
validator
|
|
304
|
+
.var('ownership', null)
|
|
305
|
+
.check(isOwner).message('You do not own this resource')
|
|
306
|
+
|
|
307
|
+
validator
|
|
308
|
+
.var('permission', null)
|
|
309
|
+
.check(hasPermission).message('You do not have permission to edit')
|
|
310
|
+
|
|
311
|
+
validator
|
|
312
|
+
.var('limit', null)
|
|
313
|
+
.check(isWithinLimit).message('You have reached your daily edit limit')
|
|
314
|
+
|
|
315
|
+
if (await validator.error()) {
|
|
316
|
+
return validator.result('Validation failed')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return validator.success('Resource updated')
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Example: Chained Validation (Single Statement)
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
module.exports = async function (Candy) {
|
|
327
|
+
return await Candy.Validator
|
|
328
|
+
.post('email').check('required').message('Email required').check('email').message('Invalid email')
|
|
329
|
+
.post('password').check('required').message('Password required').check('minlen:8').message('Min 8 chars')
|
|
330
|
+
.post('age').check('required').message('Age required').check('numeric').message('Must be number').check('min:18').message('Must be 18+')
|
|
331
|
+
.success('Registration successful')
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### Example: Admin-Only Action with User Check
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
module.exports = async function (Candy) {
|
|
339
|
+
const validator = Candy.Validator
|
|
340
|
+
|
|
341
|
+
validator
|
|
342
|
+
.var('auth_check', null)
|
|
343
|
+
.check('usercheck').message('You must be logged in')
|
|
344
|
+
|
|
345
|
+
validator
|
|
346
|
+
.var('admin_role', null)
|
|
347
|
+
.check('user:role').message('Admin access required')
|
|
348
|
+
.check('equal:admin').message('Only admins can perform this action')
|
|
349
|
+
|
|
350
|
+
validator.post('action').check('required').message('Action is required')
|
|
351
|
+
|
|
352
|
+
if (await validator.error()) {
|
|
353
|
+
return validator.result('Access denied')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return validator.success('Action completed')
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### Frontend Integration
|
|
361
|
+
|
|
362
|
+
When using `Candy.form()` on the frontend, validation errors are automatically displayed:
|
|
363
|
+
|
|
364
|
+
**Automatic Error Display:**
|
|
365
|
+
- Each field's error message appears below the input with attribute `[candy-form-error="fieldname"]`
|
|
366
|
+
- Invalid inputs get the `_candy_error` CSS class automatically
|
|
367
|
+
- Errors fade out when the user focuses on the input
|
|
368
|
+
|
|
369
|
+
**Success Messages:**
|
|
370
|
+
- Success messages appear in elements with `[candy-form-success]` attribute
|
|
371
|
+
- Automatically fades in when validation passes
|
|
372
|
+
|
|
373
|
+
**Auto-Redirect:**
|
|
374
|
+
- If you pass a URL as the second parameter to `Candy.form()`, successful submissions automatically redirect:
|
|
375
|
+
```javascript
|
|
376
|
+
Candy.form('myForm', '/dashboard') // Redirects to /dashboard on success
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Example HTML:**
|
|
380
|
+
```html
|
|
381
|
+
<form candy-form="register" action="/api/register" method="POST">
|
|
382
|
+
<input type="email" name="email" placeholder="Email">
|
|
383
|
+
<span candy-form-error="email"></span>
|
|
384
|
+
|
|
385
|
+
<input type="password" name="password" placeholder="Password">
|
|
386
|
+
<span candy-form-error="password"></span>
|
|
387
|
+
|
|
388
|
+
<button type="submit">Register</button>
|
|
389
|
+
|
|
390
|
+
<span candy-form-success></span>
|
|
391
|
+
</form>
|
|
392
|
+
|
|
393
|
+
<script>
|
|
394
|
+
Candy.form('register', '/dashboard') // Auto-redirect on success
|
|
395
|
+
</script>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### Response Format
|
|
399
|
+
|
|
400
|
+
The `result()` method returns a standardized response:
|
|
401
|
+
|
|
402
|
+
**Success:**
|
|
403
|
+
```json
|
|
404
|
+
{
|
|
405
|
+
"result": {
|
|
406
|
+
"success": true,
|
|
407
|
+
"message": "Operation successful"
|
|
408
|
+
},
|
|
409
|
+
"data": null
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Error:**
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"result": {
|
|
417
|
+
"success": false
|
|
418
|
+
},
|
|
419
|
+
"errors": {
|
|
420
|
+
"email": "Valid email address required",
|
|
421
|
+
"password": "Password must be at least 8 characters"
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
## 🔐 User Logins with `Auth.js`
|
|
2
|
+
|
|
3
|
+
The `Candy.Auth` service is your bouncer, managing who gets in and who stays out. It handles user login sessions for you.
|
|
4
|
+
|
|
5
|
+
#### Letting a User In
|
|
6
|
+
|
|
7
|
+
`Candy.Auth.login(userId, userData)`
|
|
8
|
+
|
|
9
|
+
* `userId`: A unique ID for the user (like their database ID).
|
|
10
|
+
* `userData`: An object with any user info you want to remember, like their username or role.
|
|
11
|
+
|
|
12
|
+
When you call this, `Auth` creates a secure session for the user.
|
|
13
|
+
|
|
14
|
+
#### Checking the Guest List
|
|
15
|
+
|
|
16
|
+
* `Candy.Auth.isLogin()`: Is the current user logged in? Returns `true` or `false`.
|
|
17
|
+
* `Candy.Auth.getId()`: Gets the ID of the logged-in user.
|
|
18
|
+
* `Candy.Auth.get('some-key')`: Grabs a specific piece of info from the `userData` you stored.
|
|
19
|
+
|
|
20
|
+
#### Showing a User Out
|
|
21
|
+
|
|
22
|
+
* `Candy.Auth.logout()`: Ends the user's session and logs them out.
|
|
23
|
+
|
|
24
|
+
#### Example: A Login Flow
|
|
25
|
+
```javascript
|
|
26
|
+
// Controller for your login form
|
|
27
|
+
module.exports = async function (Candy) {
|
|
28
|
+
const { username, password } = Candy.Request.post;
|
|
29
|
+
|
|
30
|
+
// IMPORTANT: You need to write your own code to find the user in your database!
|
|
31
|
+
const user = await yourDatabase.findUser(username, password);
|
|
32
|
+
|
|
33
|
+
if (user) {
|
|
34
|
+
// User is valid! Log them in.
|
|
35
|
+
Candy.Auth.login(user.id, { username: user.username });
|
|
36
|
+
return Candy.direct('/dashboard'); // Send them to their dashboard
|
|
37
|
+
} else {
|
|
38
|
+
// Bad credentials, send them back to the login page
|
|
39
|
+
return Candy.direct('/login?error=1');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A protected dashboard page
|
|
44
|
+
module.exports = function (Candy) {
|
|
45
|
+
// If they're not logged in, kick them back to the login page.
|
|
46
|
+
if (!Candy.Auth.isLogin()) {
|
|
47
|
+
return Candy.direct('/login');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const username = Candy.Auth.get('username');
|
|
51
|
+
return `Welcome back, ${username}!`;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## 🛡️ Foiling Villains with CSRF Protection
|
|
2
|
+
|
|
3
|
+
Cross-Site Request Forgery (CSRF) is a scary-sounding attack where a bad guy tries to trick your users into submitting forms they didn't mean to. The `Candy.Token` service is your shield against this!
|
|
4
|
+
|
|
5
|
+
#### How it Works
|
|
6
|
+
|
|
7
|
+
The idea is simple:
|
|
8
|
+
1. When you show a form, you generate a secret, one-time-use token.
|
|
9
|
+
2. You put this token in a hidden field in the form.
|
|
10
|
+
3. When the user submits the form, you check if the token they sent back matches the one you generated.
|
|
11
|
+
|
|
12
|
+
If they don't match, it's a trap!
|
|
13
|
+
|
|
14
|
+
#### Generating and Checking Tokens
|
|
15
|
+
|
|
16
|
+
* `Candy.Token.get()`: Creates a new secret token.
|
|
17
|
+
* `Candy.Token.check(theToken)`: Checks if `theToken` is valid.
|
|
18
|
+
|
|
19
|
+
#### Example: Securing a Form
|
|
20
|
+
|
|
21
|
+
**1. Add the token to your form view:**
|
|
22
|
+
```html
|
|
23
|
+
<form action="/some-action" method="post">
|
|
24
|
+
<!-- Add the secret token here! -->
|
|
25
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}">
|
|
26
|
+
|
|
27
|
+
<!-- ... your other form fields ... -->
|
|
28
|
+
<button type="submit">Submit</button>
|
|
29
|
+
</form>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**2. Your controller that shows the form:**
|
|
33
|
+
```javascript
|
|
34
|
+
module.exports = function (Candy) {
|
|
35
|
+
// Get a token and pass it to the view
|
|
36
|
+
const token = Candy.Token.get();
|
|
37
|
+
return Candy.View.render('your_form_view', { csrfToken: token });
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**3. Your controller that handles the form submission:**
|
|
42
|
+
```javascript
|
|
43
|
+
module.exports = function (Candy) {
|
|
44
|
+
const submittedToken = Candy.Request.post.csrf_token;
|
|
45
|
+
|
|
46
|
+
// Check the token!
|
|
47
|
+
if (!Candy.Token.check(submittedToken)) {
|
|
48
|
+
// If it's bad, stop right here.
|
|
49
|
+
return Candy.return('Invalid CSRF Token!').status(403);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If we get here, the token was good!
|
|
53
|
+
// ...you can now safely process the form...
|
|
54
|
+
}
|
|
55
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# User Registration
|
|
2
|
+
|
|
3
|
+
The `Candy.Auth.register()` method provides a secure and user-friendly way to create new user accounts with automatic password hashing, duplicate checking, and optional auto-login.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
module.exports = async function (Candy) {
|
|
9
|
+
const result = await Candy.Auth.register({
|
|
10
|
+
email: 'user@example.com',
|
|
11
|
+
username: 'johndoe',
|
|
12
|
+
password: 'securePassword123',
|
|
13
|
+
name: 'John Doe'
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (result.success) {
|
|
17
|
+
return {message: 'Registration successful', user: result.user}
|
|
18
|
+
} else {
|
|
19
|
+
return {error: result.error}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Advanced Options
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const result = await Candy.Auth.register(
|
|
28
|
+
{
|
|
29
|
+
email: 'user@example.com',
|
|
30
|
+
username: 'johndoe',
|
|
31
|
+
password: 'securePassword123',
|
|
32
|
+
name: 'John Doe',
|
|
33
|
+
role: 'user'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
passwordField: 'password', // Field name for password (default: 'password')
|
|
37
|
+
uniqueFields: ['email', 'username'], // Fields to check for duplicates (default: ['email'])
|
|
38
|
+
autoLogin: true // Auto-login after registration (default: true)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Response Format
|
|
44
|
+
|
|
45
|
+
### Success Response
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
{
|
|
49
|
+
success: true,
|
|
50
|
+
user: {
|
|
51
|
+
id: 123,
|
|
52
|
+
email: 'user@example.com',
|
|
53
|
+
username: 'johndoe',
|
|
54
|
+
// ... other user fields
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Error Response
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
{
|
|
63
|
+
success: false,
|
|
64
|
+
error: 'email already exists',
|
|
65
|
+
field: 'email' // Only present for duplicate field errors
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **Automatic Password Hashing**: Passwords are automatically hashed using bcrypt
|
|
72
|
+
- **Duplicate Prevention**: Checks for existing users with the same email/username
|
|
73
|
+
- **Auto-Login**: Optionally logs in the user immediately after registration
|
|
74
|
+
- **Flexible Configuration**: Customize password field name and unique fields
|
|
75
|
+
- **Detailed Error Messages**: Returns specific error information for better UX
|
|
76
|
+
|
|
77
|
+
## Example Controller
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
module.exports = async function (Candy) {
|
|
81
|
+
const validator = Candy.Validator
|
|
82
|
+
|
|
83
|
+
// Validate input
|
|
84
|
+
validator.post('email').check('required|email').message('A valid email is required')
|
|
85
|
+
validator.post('username').check('required|minlen:4').message('Username must be at least 4 characters')
|
|
86
|
+
validator.post('password').check('required|minlen:8').message('Password must be at least 8 characters')
|
|
87
|
+
|
|
88
|
+
if (await validator.error()) {
|
|
89
|
+
return validator.result('Please fix the errors below')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get validated data
|
|
93
|
+
const email = await Candy.request('email')
|
|
94
|
+
const username = await Candy.request('username')
|
|
95
|
+
const password = await Candy.request('password')
|
|
96
|
+
const name = await Candy.request('name')
|
|
97
|
+
|
|
98
|
+
// Register user
|
|
99
|
+
const result = await Candy.Auth.register(
|
|
100
|
+
{email, username, password, name},
|
|
101
|
+
{uniqueFields: ['email', 'username']}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (result.success) {
|
|
105
|
+
// User is now registered and logged in
|
|
106
|
+
return Candy.direct('/dashboard')
|
|
107
|
+
} else {
|
|
108
|
+
// Show error message
|
|
109
|
+
return {error: result.error}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
Make sure your `config.json` has the auth configuration:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"auth": {
|
|
121
|
+
"table": "users",
|
|
122
|
+
"key": "id",
|
|
123
|
+
"token": "user_tokens"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Security Notes
|
|
129
|
+
|
|
130
|
+
- Passwords are automatically hashed with bcrypt before storage
|
|
131
|
+
- The system automatically detects already-hashed passwords (bcrypt pattern) to prevent double-hashing
|
|
132
|
+
- Never store plain text passwords
|
|
133
|
+
- Use HTTPS in production to protect credentials in transit
|
|
134
|
+
- Consider adding rate limiting to prevent brute force attacks
|