odac 1.0.1 → 1.2.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/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +33 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/auto-pr-description.yml +3 -1
- package/.github/workflows/release.yml +42 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.github/workflows/test-publish.yml +36 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +3 -3
- package/CHANGELOG.md +184 -0
- package/README.md +53 -34
- package/bin/odac.js +181 -49
- package/client/odac.js +878 -995
- package/docs/backend/01-overview/03-development-server.md +39 -46
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
- package/docs/backend/03-config/00-configuration-overview.md +15 -6
- package/docs/backend/03-config/01-database-connection.md +3 -3
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/04-routing/09-websocket.md +29 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
- package/docs/backend/05-controllers/03-controller-classes.md +61 -55
- package/docs/backend/05-forms/01-custom-forms.md +103 -95
- package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/02-rendering-a-view.md +1 -1
- package/docs/backend/07-views/03-variables.md +5 -5
- package/docs/backend/07-views/04-request-data.md +1 -1
- package/docs/backend/07-views/08-backend-javascript.md +1 -1
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +100 -0
- package/docs/backend/08-database/02-basics.md +136 -0
- package/docs/backend/08-database/03-advanced.md +84 -0
- package/docs/backend/08-database/04-migrations.md +48 -0
- package/docs/backend/09-validation/01-the-validator-service.md +1 -0
- package/docs/backend/10-authentication/03-register.md +9 -2
- package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
- package/docs/backend/10-authentication/05-session-management.md +16 -2
- package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
- package/docs/backend/10-authentication/07-magic-links.md +134 -0
- package/docs/backend/11-mail/01-the-mail-service.md +118 -28
- package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
- package/docs/backend/13-utilities/01-odac-var.md +7 -7
- package/docs/backend/13-utilities/02-ipc.md +73 -0
- package/docs/frontend/01-overview/01-introduction.md +5 -1
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
- package/docs/index.json +21 -125
- package/eslint.config.mjs +5 -47
- package/jest.config.js +1 -1
- package/package.json +16 -7
- package/src/Auth.js +414 -121
- package/src/Config.js +12 -7
- package/src/Database.js +188 -0
- package/src/Env.js +3 -1
- package/src/Ipc.js +337 -0
- package/src/Lang.js +9 -2
- package/src/Mail.js +408 -37
- package/src/Odac.js +105 -40
- package/src/Request.js +71 -49
- package/src/Route/Cron.js +62 -18
- package/src/Route/Internal.js +215 -12
- package/src/Route/Middleware.js +7 -2
- package/src/Route.js +372 -109
- package/src/Server.js +118 -12
- package/src/Storage.js +169 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +95 -3
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +210 -28
- package/src/View.js +108 -7
- package/src/WebSocket.js +18 -3
- package/template/odac.json +5 -0
- package/template/package.json +3 -1
- package/template/route/www.js +12 -10
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +86 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +1 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
- package/test/scripts/check-coverage.js +4 -4
- package/docs/backend/08-database/01-database-connection.md +0 -99
- package/docs/backend/08-database/02-using-mysql.md +0 -322
- package/src/Mysql.js +0 -575
- package/template/config.json +0 -5
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Candy.test.js +0 -234
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Process.test.js +0 -156
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- package/test/server/__mocks__/tls.js +0 -229
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Magic Links
|
|
2
|
+
|
|
3
|
+
Magic links provide a passwordless authentication method where verification links are sent directly to the user's email address. Odac offers both a zero-configuration component and a programmatic API to implement this feature.
|
|
4
|
+
|
|
5
|
+
## The `<odac:magic-login>` Component
|
|
6
|
+
|
|
7
|
+
The simplest way to implement magic links is using the built-in view component. This works similarly to the standard `<odac:login>` component but requires no password field.
|
|
8
|
+
|
|
9
|
+
### Basic Usage
|
|
10
|
+
|
|
11
|
+
Use the `<odac:magic-login>` tag in your view. If you provide no inner content, Odac will automatically generate a default email input and submit button.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<odac:magic-login redirect="/dashboard" />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Attributes
|
|
18
|
+
|
|
19
|
+
| Attribute | Description | Default |
|
|
20
|
+
|-----------|-------------|---------|
|
|
21
|
+
| `redirect` | URL to redirect to after successful login (Required) | `null` |
|
|
22
|
+
| `email-label` | Label text for the default email field | `"Email Address"` |
|
|
23
|
+
| `submit-text` | Text for the submit button | `"Send Magic Link"` |
|
|
24
|
+
|
|
25
|
+
### Customized Usage
|
|
26
|
+
|
|
27
|
+
You can fully customize the form by adding your own inputs and buttons inside the tag.
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<div class="auth-card">
|
|
31
|
+
<h2>Sign In</h2>
|
|
32
|
+
<p>We'll send a login link to your email.</p>
|
|
33
|
+
|
|
34
|
+
<odac:magic-login redirect="/dashboard">
|
|
35
|
+
<div class="form-group">
|
|
36
|
+
<label for="email">Work Email</label>
|
|
37
|
+
<odac:input name="email" type="email" placeholder="name@company.com" class="form-control">
|
|
38
|
+
<odac:validate rule="required|email" message="Please enter a valid work email"/>
|
|
39
|
+
</odac:input>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<odac:submit class="btn btn-primary w-full">Email me a login link</odac:submit>
|
|
43
|
+
</odac:magic-login>
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Backend API
|
|
48
|
+
|
|
49
|
+
For more complex requirements, such as building a custom API or integrating with other flows, you can use the `Odac.Auth` class directly.
|
|
50
|
+
|
|
51
|
+
### Requesting a Magic Link
|
|
52
|
+
|
|
53
|
+
Use `magic` to generate a token and send the email.
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
// In your controller (e.g., AuthController.js)
|
|
57
|
+
|
|
58
|
+
async sendLink(req, res) {
|
|
59
|
+
const email = req.input('email');
|
|
60
|
+
|
|
61
|
+
// Returns { success: boolean, message: string, error?: string }
|
|
62
|
+
const result = await Odac.Auth.magic(email, {
|
|
63
|
+
redirect: '/dashboard', // Redirect after verification
|
|
64
|
+
subject: 'Log in to MyApp', // Email subject
|
|
65
|
+
template: 'auth/magic-link' // View path for the email template
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (result.success) {
|
|
69
|
+
return res.json({ message: result.message });
|
|
70
|
+
} else {
|
|
71
|
+
return res.status(400).json({ error: result.error });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Verification Logic
|
|
77
|
+
|
|
78
|
+
Odac handles the verification route automatically at `/_odac/magic-verify`. However, if you are building a custom flow, you might interact with the underlying data model directly using `Odac.DB`.
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Magic links usage is configured in your `odac.json` file under the `auth` object.
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"auth": {
|
|
87
|
+
"table": "users", // Table where users are stored
|
|
88
|
+
"magicTable": "magic_links", // Table to store tokens (auto-created)
|
|
89
|
+
"magicLinkRateLimit": 3600000, // Rate limit window in ms (Default: 1 hour)
|
|
90
|
+
"magicLinkMaxAttempts": 2, // Max emails per address per window
|
|
91
|
+
"magicLinkMaxAttemptsPerIP": 5, // Max requests per IP per window
|
|
92
|
+
"magicLinkSessionCooldown": 30000, // Session cooldown in ms (Default: 30s)
|
|
93
|
+
"passwordless": true, // Optimization: Skip generating passwords for new users (Default: false)
|
|
94
|
+
"passwordField": "password" // Column name for password (Default: password)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Auto-Registration & Passwordless Mode
|
|
100
|
+
|
|
101
|
+
When a user verifies a magic link:
|
|
102
|
+
|
|
103
|
+
1. **Existing User**: They are logged in immediately.
|
|
104
|
+
2. **New User**: An account is automatically created for them (Auto-Registration).
|
|
105
|
+
|
|
106
|
+
By default, Odac attempts to generate a secure random password for new users to satisfy typical database constraints. However, if your application is purely passwordless (e.g. your storage schema doesn't have a `password` column at all), you should set `"passwordless": true` in your config. This optimizes performance by skipping the password generation step.
|
|
107
|
+
|
|
108
|
+
Even without this setting, Odac is smart enough to retry registration without a password if the database rejects the initial attempt due to a missing password column.
|
|
109
|
+
|
|
110
|
+
## Security & Best Practices
|
|
111
|
+
|
|
112
|
+
1. **Rate Limiting**: Odac enforces a 3-layer defense system:
|
|
113
|
+
* **Session Cooldown**: immediate cookie-based block for rapid clicks (Default: 30s).
|
|
114
|
+
* **IP Limit**: prevents mass attacks from a single source.
|
|
115
|
+
* **Email Limit**: prevents spamming a specific user.
|
|
116
|
+
2. **Token Security**: Tokens are hashed in the database (`token_hash`) using secure hashing algorithms. The raw token is only sent to the user's email and never stored.
|
|
117
|
+
3. **One-Time Use**: Tokens are immediately deleted upon successful verification or expiration.
|
|
118
|
+
4. **User Enumeration**: To prevent attackers from checking if an email exists in your system, the `magic` method (and the form) will return a "success" message even if the user does not exist.
|
|
119
|
+
|
|
120
|
+
## Email Template
|
|
121
|
+
|
|
122
|
+
If using the default mailer, you can create a custom email template at `views/auth/magic-link.html`. The template receives the following variables:
|
|
123
|
+
|
|
124
|
+
- `link`: The full verification URL.
|
|
125
|
+
- `network`: The hostname.
|
|
126
|
+
- `ip`: The request IP address.
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<!-- views/auth/magic-link.html -->
|
|
130
|
+
<h1>Login Request</h1>
|
|
131
|
+
<p>Click the link below to log in to {{ network }}:</p>
|
|
132
|
+
<a href="{{ link }}">Login Now</a>
|
|
133
|
+
<p>This link was requested from IP: {{ ip }}</p>
|
|
134
|
+
```
|
|
@@ -1,42 +1,132 @@
|
|
|
1
1
|
## ✉️ The `Mail` Service
|
|
2
2
|
|
|
3
|
-
The `Odac.Mail` service
|
|
3
|
+
The `Odac.Mail` service provides a fluent, chainable interface to send emails. **It is a zero-config service** designed to work exclusively with the **ODAC Core** server. If you are running Odac, the mail service works out of the box without any additional setup.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### How to Send an Email
|
|
6
6
|
|
|
7
|
-
`Odac.Mail.
|
|
7
|
+
The `Odac.Mail` class uses a builder pattern. You start by instantiating it with a template name, set various properties, and finally call `send()`.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* `subject`: The subject line for your email.
|
|
11
|
-
* `htmlBody`: The main content of your email. You can even use HTML tags to make it look fancy!
|
|
9
|
+
#### Syntax
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
```javascript
|
|
12
|
+
await Odac.Mail('template_name')
|
|
13
|
+
.from('sender@example.com', 'Sender Name')
|
|
14
|
+
.to('recipient@example.com')
|
|
15
|
+
.subject('Your Subject Here')
|
|
16
|
+
.send({
|
|
17
|
+
variable1: 'value1',
|
|
18
|
+
variable2: 'value2'
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
* **Template**: The constructor takes the name of the HTML template file located in `view/mail/`.
|
|
23
|
+
* **Data**: The object passed to `send()` contains key-value pairs that replace `{key}` placeholders in your HTML template.
|
|
14
24
|
|
|
15
|
-
#### Example:
|
|
25
|
+
#### Example: Contact Form Controller
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
Here is a complete example of how to use the Mail service inside a controller:
|
|
18
28
|
|
|
19
29
|
```javascript
|
|
20
30
|
module.exports = async function (Odac) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const { name, email, message } = Odac.Request.post;
|
|
32
|
+
|
|
33
|
+
// 1. Validate Input
|
|
34
|
+
if (!name || !email || !message) {
|
|
35
|
+
return { error: 'Please fill in all fields.' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// 2. Prepare and Send Email
|
|
40
|
+
const result = await Odac.Mail('contact_form_notification')
|
|
41
|
+
.from('system@myapp.com', 'My App System')
|
|
42
|
+
.to('admin@myapp.com')
|
|
43
|
+
.subject('New Contact Form Submission')
|
|
44
|
+
.send({
|
|
45
|
+
user_name: name,
|
|
46
|
+
user_email: email,
|
|
47
|
+
user_message: message,
|
|
48
|
+
timestamp: new Date().toISOString()
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 3. Check Result
|
|
52
|
+
if (result) {
|
|
53
|
+
return { success: true, message: 'Thank you! We received your message.' };
|
|
54
|
+
} else {
|
|
55
|
+
return { error: 'Failed to send email.' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(e);
|
|
60
|
+
return { error: 'An unexpected error occurred.' };
|
|
61
|
+
}
|
|
39
62
|
}
|
|
40
63
|
```
|
|
41
64
|
|
|
42
|
-
|
|
65
|
+
### Template File
|
|
66
|
+
|
|
67
|
+
Create your HTML template in `view/mail/contact_form_notification.html`:
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<h1>New Contact Message</h1>
|
|
71
|
+
<p><strong>From:</strong> {user_name} ({user_email})</p>
|
|
72
|
+
<p><strong>Time:</strong> {timestamp}</p>
|
|
73
|
+
<hr>
|
|
74
|
+
<p>{user_message}</p>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Sending Plain Text or Raw HTML (No Template)
|
|
78
|
+
|
|
79
|
+
You can send emails **without creating a template file** by using the `.text()` or `.html()` methods directly. This is useful for simple notifications or dynamic content.
|
|
80
|
+
|
|
81
|
+
**Note:** If you provide both a template name AND manual content, the template will take precedence.
|
|
82
|
+
|
|
83
|
+
#### 1. Plain Text Email
|
|
84
|
+
|
|
85
|
+
Use the `.text()` method to send a simple, text-only email. The `Content-Type` will automatically be set to `text/plain`.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
await Odac.Mail() // No template name required
|
|
89
|
+
.to('recipient@example.com')
|
|
90
|
+
.subject('System Alert')
|
|
91
|
+
.text('Server load is critical. Please check immediately.')
|
|
92
|
+
.send();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Note:** Plain text does not support HTML tags like links (`<a href>`). If you write a URL (e.g., `https://odac.run`), most email clients will automatically make it clickable.
|
|
96
|
+
|
|
97
|
+
#### 2. Raw HTML Email
|
|
98
|
+
|
|
99
|
+
Use the `.html()` method to send an HTML email without a file.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
await Odac.Mail()
|
|
103
|
+
.to('user@example.com')
|
|
104
|
+
.subject('Welcome')
|
|
105
|
+
.html('<h1>Welcome!</h1><p>Please <a href="https://example.com">click here</a>.</p>')
|
|
106
|
+
.send();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If you use `.html()`, the system will automatically generate a plain-text version of your email by stripping the tags, just like it does for templates.
|
|
110
|
+
|
|
111
|
+
### Advanced Usage
|
|
112
|
+
|
|
113
|
+
#### Custom Headers
|
|
114
|
+
|
|
115
|
+
You can inject custom headers into the email using the `header()` method:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
.header({
|
|
119
|
+
'X-Custom-Header': 'CustomValue',
|
|
120
|
+
'Reply-To': 'support@example.com'
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Chainable Methods
|
|
125
|
+
|
|
126
|
+
* `from(email, name)`: Sets the sender.
|
|
127
|
+
* `to(email)`: Sets the recipient.
|
|
128
|
+
* `subject(text)`: Sets the subject line.
|
|
129
|
+
* `text(content)`: Sets the plain text body (used if no template is provided).
|
|
130
|
+
* `html(content)`: Sets the HTML body (used if no template is provided).
|
|
131
|
+
* `header(object)`: Merges custom headers.
|
|
132
|
+
* `send(data)`: Compiles the template with `data`, connects to the Odac Core, and sends the email payload. Returns a `Promise`.
|
|
@@ -91,7 +91,7 @@ module.exports = async (Odac) => {
|
|
|
91
91
|
// controller/users/get/index.js
|
|
92
92
|
module.exports = async (Odac) => {
|
|
93
93
|
Odac.stream(async function* () {
|
|
94
|
-
const users = await Odac.
|
|
94
|
+
const users = await Odac.DB.users.get()
|
|
95
95
|
|
|
96
96
|
for (const user of users) {
|
|
97
97
|
yield user
|
|
@@ -194,7 +194,7 @@ module.exports = async (Odac) => {
|
|
|
194
194
|
let hasMore = true
|
|
195
195
|
|
|
196
196
|
while (hasMore) {
|
|
197
|
-
const posts = await Odac.
|
|
197
|
+
const posts = await Odac.DB.posts
|
|
198
198
|
.limit(10)
|
|
199
199
|
.offset((page - 1) * 10)
|
|
200
200
|
.get()
|
|
@@ -313,7 +313,7 @@ module.exports = async function(Odac) {
|
|
|
313
313
|
const profileSlug = Odac.Var(username).slug()
|
|
314
314
|
|
|
315
315
|
// Save user
|
|
316
|
-
await Odac.
|
|
316
|
+
await Odac.DB.users.insert({
|
|
317
317
|
email: email,
|
|
318
318
|
username: username,
|
|
319
319
|
password: hashedPassword,
|
|
@@ -332,7 +332,7 @@ module.exports = async function(Odac) {
|
|
|
332
332
|
const password = Odac.Request.post('password')
|
|
333
333
|
|
|
334
334
|
// Find user
|
|
335
|
-
const user = await Odac.
|
|
335
|
+
const user = await Odac.DB.users
|
|
336
336
|
.where('email', email)
|
|
337
337
|
.first()
|
|
338
338
|
|
|
@@ -369,19 +369,19 @@ module.exports = async function(Odac) {
|
|
|
369
369
|
const slug = Odac.Var(title).slug()
|
|
370
370
|
|
|
371
371
|
// Check if slug exists
|
|
372
|
-
const exists = await Odac.
|
|
372
|
+
const exists = await Odac.DB.posts
|
|
373
373
|
.where('slug', slug)
|
|
374
374
|
.first()
|
|
375
375
|
|
|
376
376
|
if (exists) {
|
|
377
377
|
// Add timestamp to make unique
|
|
378
378
|
const uniqueSlug = `${slug}-${Date.now()}`
|
|
379
|
-
await Odac.
|
|
379
|
+
await Odac.DB.posts.insert({
|
|
380
380
|
title: title,
|
|
381
381
|
slug: uniqueSlug
|
|
382
382
|
})
|
|
383
383
|
} else {
|
|
384
|
-
await Odac.
|
|
384
|
+
await Odac.DB.posts.insert({
|
|
385
385
|
title: title,
|
|
386
386
|
slug: slug
|
|
387
387
|
})
|
|
@@ -464,7 +464,7 @@ module.exports = async function(Odac) {
|
|
|
464
464
|
const encryptedCard = Odac.Var(creditCard).encrypt()
|
|
465
465
|
|
|
466
466
|
// Save encrypted data
|
|
467
|
-
await Odac.
|
|
467
|
+
await Odac.DB.payments.insert({
|
|
468
468
|
user_id: Odac.Auth.id(),
|
|
469
469
|
card: encryptedCard
|
|
470
470
|
})
|
|
@@ -474,7 +474,7 @@ module.exports = async function(Odac) {
|
|
|
474
474
|
|
|
475
475
|
// Later, to retrieve and decrypt
|
|
476
476
|
module.exports = async function(Odac) {
|
|
477
|
-
const payment = await Odac.
|
|
477
|
+
const payment = await Odac.DB.payments
|
|
478
478
|
.where('user_id', Odac.Auth.id())
|
|
479
479
|
.first()
|
|
480
480
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# IPC (Inter-Process Communication)
|
|
2
|
+
|
|
3
|
+
Odac provides a built-in IPC system that allows your application workers to communicate with each other, share data, and sync states. It abstracts the underlying driver, allowing you to switch between in-memory (cluster) and Redis-based communication with a simple configuration change.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
To configure the IPC system, update your project configuration. The default driver is `memory`.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// config.js
|
|
11
|
+
module.exports = {
|
|
12
|
+
// ...
|
|
13
|
+
database: {
|
|
14
|
+
// ... other dbs
|
|
15
|
+
redis: {
|
|
16
|
+
default: {
|
|
17
|
+
// node-redis connection options
|
|
18
|
+
// e.g., url: 'redis://user:pass@host:port'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
ipc: {
|
|
23
|
+
driver: 'memory', // Options: 'memory' or 'redis'
|
|
24
|
+
redis: 'default' // The name of the redis connection to use (if driver is 'redis')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Drivers
|
|
30
|
+
|
|
31
|
+
- **memory**: Uses Node.js `cluster` IPC. Ideal for single-server deployments. Data is stored in the Main/Primary process RAM and shared across workers via `process.send`.
|
|
32
|
+
- **redis**: Uses a Redis server. Ideal for multi-server/horizontal scaling deployments. Requires a configured Redis connection.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
You can access the IPC module via `Odac.Ipc`.
|
|
37
|
+
|
|
38
|
+
### Data Storage (Key-Value)
|
|
39
|
+
|
|
40
|
+
You can set, get, and delete values. These values are shared across all workers.
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// Set a value (with optional TTL in seconds)
|
|
44
|
+
await Odac.Ipc.set('maintenance_mode', true);
|
|
45
|
+
await Odac.Ipc.set('temp_cache', { foo: 'bar' }, 60);
|
|
46
|
+
|
|
47
|
+
// Get a value
|
|
48
|
+
const isMaintenance = await Odac.Ipc.get('maintenance_mode');
|
|
49
|
+
|
|
50
|
+
// Delete a value
|
|
51
|
+
await Odac.Ipc.del('maintenance_mode');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> [!NOTE]
|
|
55
|
+
> `get`, `set`, and `del` are asynchronous and return Promises.
|
|
56
|
+
|
|
57
|
+
### Pub/Sub (Messaging)
|
|
58
|
+
|
|
59
|
+
You can publish messages to channels and subscribe to them in other workers.
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// Subscribe to a channel
|
|
63
|
+
// Best practice: Subscribe in your app initialization or a Service Provider
|
|
64
|
+
await Odac.Ipc.subscribe('chat:global', (message) => {
|
|
65
|
+
console.log('New chat message:', message);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Publish a message
|
|
69
|
+
await Odac.Ipc.publish('chat:global', { user: 'Emre', text: 'Hello World' });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> [!TIP]
|
|
73
|
+
> When using `memory` driver, the subscription listener is registered in the current worker. When a message is published, it goes to the Main process and is then broadcasted to all subscribed workers.
|
|
@@ -131,7 +131,11 @@ odac.get('/api/users', function(data) {
|
|
|
131
131
|
## Other Utility Functions
|
|
132
132
|
|
|
133
133
|
- **`Odac.client()`**: Returns a unique client identifier from a cookie.
|
|
134
|
-
- **`Odac.data()`**: Returns data from the
|
|
134
|
+
- **`Odac.data(key)`**: Returns shared data passed from the backend via `Odac.share`. You can get the full data object or a specific key:
|
|
135
|
+
```javascript
|
|
136
|
+
let allData = odac.data();
|
|
137
|
+
let user = odac.data('user'); // Returns null if not exists
|
|
138
|
+
```
|
|
135
139
|
- **`Odac.page()`**: Returns the identifier of the current page. This is the controller name (e.g., `'user'`) or view name (e.g., `'dashboard'`) set by the backend. Use this to conditionally run code for specific pages.
|
|
136
140
|
- **`Odac.storage()`**: A wrapper for `localStorage`.
|
|
137
141
|
```javascript
|
|
@@ -347,7 +347,7 @@ Odac.action({
|
|
|
347
347
|
**Page Identifier Rules:**
|
|
348
348
|
- **With controller**: Uses controller filename (e.g., `user.js` → `'user'`)
|
|
349
349
|
- **With view object**: Uses `content` or `all` value (e.g., `{content: 'dashboard'}` → `'dashboard'`)
|
|
350
|
-
- Accessible via `Odac.page()`
|
|
350
|
+
- Accessible via `Odac.page()`
|
|
351
351
|
|
|
352
352
|
## Server Variables
|
|
353
353
|
|
package/docs/index.json
CHANGED
|
@@ -1,124 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"server": [
|
|
3
|
-
{
|
|
4
|
-
"file": "01-installation",
|
|
5
|
-
"title": "Installation",
|
|
6
|
-
"children": [
|
|
7
|
-
{
|
|
8
|
-
"file": "01-quick-install.md",
|
|
9
|
-
"title": "Quick Install"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"file": "02-manual-installation-via-npm.md",
|
|
13
|
-
"title": "Manual Installation (via NPM)"
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"file": "02-get-started",
|
|
19
|
-
"title": "Getting Started",
|
|
20
|
-
"children": [
|
|
21
|
-
{
|
|
22
|
-
"file": "01-core-concepts.md",
|
|
23
|
-
"title": "Core Concepts"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"file": "02-basic-commands.md",
|
|
27
|
-
"title": "Basic Commands"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"file": "03-cli-reference.md",
|
|
31
|
-
"title": "CLI Reference"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"file": "04-cli-quick-reference.md",
|
|
35
|
-
"title": "CLI Quick Reference"
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"file": "03-service",
|
|
41
|
-
"title": "Running a Service",
|
|
42
|
-
"children": [
|
|
43
|
-
{
|
|
44
|
-
"file": "01-start-a-new-service.md",
|
|
45
|
-
"title": "Start a New Service"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"file": "02-delete-a-service.md",
|
|
49
|
-
"title": "Delete a Service"
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"file": "04-web",
|
|
55
|
-
"title": "Managing Websites",
|
|
56
|
-
"children": [
|
|
57
|
-
{
|
|
58
|
-
"file": "01-create-a-website.md",
|
|
59
|
-
"title": "Create a Website"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"file": "02-list-websites.md",
|
|
63
|
-
"title": "List Websites"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"file": "03-delete-a-website.md",
|
|
67
|
-
"title": "Delete a Website"
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"file": "05-subdomain",
|
|
73
|
-
"title": "Managing Subdomains",
|
|
74
|
-
"children": [
|
|
75
|
-
{
|
|
76
|
-
"file": "01-create-a-subdomain.md",
|
|
77
|
-
"title": "Create a Subdomain"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"file": "02-list-subdomains.md",
|
|
81
|
-
"title": "List Subdomains"
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
"file": "03-delete-a-subdomain.md",
|
|
85
|
-
"title": "Delete a Subdomain"
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"file": "06-ssl",
|
|
91
|
-
"title": "Managing SSL",
|
|
92
|
-
"children": [
|
|
93
|
-
{
|
|
94
|
-
"file": "01-renew-an-ssl-certificate.md",
|
|
95
|
-
"title": "Renew an SSL Certificate"
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"file": "07-mail",
|
|
101
|
-
"title": "Managing Mail",
|
|
102
|
-
"children": [
|
|
103
|
-
{
|
|
104
|
-
"file": "01-create-a-mail-account.md",
|
|
105
|
-
"title": "Create a Mail Account"
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
"file": "02-delete-a-mail-account.md",
|
|
109
|
-
"title": "Delete a Mail Account"
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
"file": "03-list-mail-accounts.md",
|
|
113
|
-
"title": "List Mail Accounts"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"file": "04-change-account-password.md",
|
|
117
|
-
"title": "Change Account Password"
|
|
118
|
-
}
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
],
|
|
122
2
|
"backend": [
|
|
123
3
|
{
|
|
124
4
|
"file": "01-overview",
|
|
@@ -134,7 +14,7 @@
|
|
|
134
14
|
},
|
|
135
15
|
{
|
|
136
16
|
"file": "03-development-server.md",
|
|
137
|
-
"title": "
|
|
17
|
+
"title": "CLI Commands & Deployment"
|
|
138
18
|
}
|
|
139
19
|
]
|
|
140
20
|
},
|
|
@@ -321,6 +201,10 @@
|
|
|
321
201
|
{
|
|
322
202
|
"file": "09-comments.md",
|
|
323
203
|
"title": "Comments"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"file": "10-styling-and-tailwind.md",
|
|
207
|
+
"title": "Styling & Tailwind CSS"
|
|
324
208
|
}
|
|
325
209
|
]
|
|
326
210
|
},
|
|
@@ -329,12 +213,20 @@
|
|
|
329
213
|
"title": "Database",
|
|
330
214
|
"children": [
|
|
331
215
|
{
|
|
332
|
-
"file": "01-
|
|
333
|
-
"title": "
|
|
216
|
+
"file": "01-getting-started.md",
|
|
217
|
+
"title": "Getting Started"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"file": "02-basics.md",
|
|
221
|
+
"title": "Query Basics"
|
|
334
222
|
},
|
|
335
223
|
{
|
|
336
|
-
"file": "
|
|
337
|
-
"title": "
|
|
224
|
+
"file": "03-advanced.md",
|
|
225
|
+
"title": "Advanced Queries"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"file": "04-migrations.md",
|
|
229
|
+
"title": "Code-First Migrations"
|
|
338
230
|
}
|
|
339
231
|
]
|
|
340
232
|
},
|
|
@@ -375,6 +267,10 @@
|
|
|
375
267
|
{
|
|
376
268
|
"file": "06-odac-login-forms.md",
|
|
377
269
|
"title": "Odac Login Forms (Zero-Config)"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"file": "07-magic-links.md",
|
|
273
|
+
"title": "Magic Links"
|
|
378
274
|
}
|
|
379
275
|
]
|
|
380
276
|
},
|