odac 1.3.0 → 1.4.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/memory.md +7 -1
- package/.github/workflows/release.yml +0 -4
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/bin/odac.js +169 -6
- package/client/odac.js +15 -11
- package/docs/ai/README.md +49 -0
- package/docs/ai/skills/SKILL.md +39 -0
- package/docs/ai/skills/backend/authentication.md +67 -0
- package/docs/ai/skills/backend/config.md +32 -0
- package/docs/ai/skills/backend/controllers.md +62 -0
- package/docs/ai/skills/backend/cron.md +50 -0
- package/docs/ai/skills/backend/database.md +21 -0
- package/docs/ai/skills/backend/forms.md +19 -0
- package/docs/ai/skills/backend/ipc.md +55 -0
- package/docs/ai/skills/backend/mail.md +34 -0
- package/docs/ai/skills/backend/request_response.md +35 -0
- package/docs/ai/skills/backend/routing.md +51 -0
- package/docs/ai/skills/backend/storage.md +43 -0
- package/docs/ai/skills/backend/streaming.md +34 -0
- package/docs/ai/skills/backend/structure.md +57 -0
- package/docs/ai/skills/backend/translations.md +42 -0
- package/docs/ai/skills/backend/utilities.md +24 -0
- package/docs/ai/skills/backend/validation.md +53 -0
- package/docs/ai/skills/backend/views.md +61 -0
- package/docs/ai/skills/frontend/core.md +66 -0
- package/docs/ai/skills/frontend/forms.md +21 -0
- package/docs/ai/skills/frontend/navigation.md +20 -0
- package/docs/ai/skills/frontend/realtime.md +47 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +2 -0
- package/docs/backend/10-authentication/05-session-management.md +25 -3
- package/package.json +1 -1
- package/src/Auth.js +100 -15
- package/src/Route/Internal.js +21 -18
- package/src/Route/MimeTypes.js +56 -0
- package/src/Route.js +40 -63
- package/src/View/Form.js +91 -51
- package/src/View.js +8 -3
- package/test/Auth.test.js +249 -0
- package/test/Client.test.js +29 -0
- package/test/View/Form.test.js +37 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Frontend Navigation & SPA Skill
|
|
2
|
+
|
|
3
|
+
Smooth transitions and single-page application behavior using `odac.js`.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
1. **Selection**: Enable via `Odac.action({ navigate: 'main' })`.
|
|
7
|
+
2. **Exclusion**: Use `data-navigate="false"` or `.no-navigate` class for full reloads.
|
|
8
|
+
3. **Lifecycle**: Use `load` and `page` events to run code after navigation.
|
|
9
|
+
|
|
10
|
+
## Patterns
|
|
11
|
+
```javascript
|
|
12
|
+
Odac.action({
|
|
13
|
+
navigate: {
|
|
14
|
+
update: 'main',
|
|
15
|
+
on: function(page, vars) {
|
|
16
|
+
console.log('Navigated to:', page);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Frontend Realtime & WebSocket Skill
|
|
2
|
+
|
|
3
|
+
Real-time bidirectional communication and server-sent events with high efficiency.
|
|
4
|
+
|
|
5
|
+
## Architectural Approach
|
|
6
|
+
ODAC prioritizes connection efficiency. `Odac.ws()` provides shared WebSocket connections across multiple browser tabs using `SharedWorker`, significantly reducing server load.
|
|
7
|
+
|
|
8
|
+
## Core Rules
|
|
9
|
+
1. **Shared Connections**: Always prefer `Odac.ws(url, { shared: true })` for scalable real-time apps.
|
|
10
|
+
2. **Auto-Reconnect**: Enabled by default; the client handles network drops automatically.
|
|
11
|
+
3. **SSE (Streaming)**: Use `new EventSource(url)` for one-way streams (e.g., live logs, notifications).
|
|
12
|
+
4. **JSON Native**: Messages sent and received via `Odac.ws` are automatically parsed/stringified.
|
|
13
|
+
|
|
14
|
+
## Reference Patterns
|
|
15
|
+
|
|
16
|
+
### 1. Shared WebSocket (Cross-Tab)
|
|
17
|
+
```javascript
|
|
18
|
+
// One connection shared across all open tabs
|
|
19
|
+
const ws = Odac.ws('/chat', { shared: true });
|
|
20
|
+
|
|
21
|
+
ws.on('message', (data) => {
|
|
22
|
+
console.log('Update received in all tabs:', data);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Sends message from current tab; others will receive the response
|
|
26
|
+
ws.send({ type: 'chat', text: 'Hello World' });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Standard WebSocket (Per-Tab)
|
|
30
|
+
```javascript
|
|
31
|
+
const ws = Odac.ws('/game', { shared: false });
|
|
32
|
+
ws.on('open', () => console.log('Game connected'));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Server-Sent Events (SSE)
|
|
36
|
+
```javascript
|
|
37
|
+
const source = new EventSource('/api/events');
|
|
38
|
+
source.onmessage = (event) => {
|
|
39
|
+
const data = JSON.parse(event.data);
|
|
40
|
+
updateUI(data);
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Best Practices
|
|
45
|
+
- **Resource Management**: Shared WebSocket automatically closes when the last tab using it is closed.
|
|
46
|
+
- **Fallback**: If `SharedWorker` is not supported (e.g., Safari), `Odac.ws` automatically falls back to a standard WebSocket.
|
|
47
|
+
- **Server-Side Hubs**: Ensure the backend uses `Odac.Hub` to send messages to the correct rooms or users.
|
|
@@ -11,6 +11,8 @@ The `Odac.Auth` service is your bouncer, managing who gets in and who stays out.
|
|
|
11
11
|
|
|
12
12
|
When you call this, `Auth` creates a secure session for the user.
|
|
13
13
|
|
|
14
|
+
> **💡 Enterprise Security:** ODAC automatically handles **Token Rotation** every 15 minutes (configurable) and includes built-in **CSRF protection** for all forms. Sessions are persistent across browser restarts by default.
|
|
15
|
+
|
|
14
16
|
#### Checking the Guest List
|
|
15
17
|
|
|
16
18
|
* `Odac.Auth.isLogin()`: Is the current user logged in? Returns `true` or `false`.
|
|
@@ -29,6 +29,20 @@ Sessions use a **sliding window** approach (similar to NextAuth.js):
|
|
|
29
29
|
3. User inactive for 30 days, session expires
|
|
30
30
|
4. Active users stay logged in indefinitely (up to 30 days of inactivity)
|
|
31
31
|
|
|
32
|
+
### Token Rotation (Enterprise Grade)
|
|
33
|
+
|
|
34
|
+
Odac implements a non-blocking **Refresh Token Rotation** mechanism to enhance security:
|
|
35
|
+
|
|
36
|
+
- **rotationAge**: How often to rotate tokens (default: 15 minutes)
|
|
37
|
+
- **Grace Period**: When a token is rotated, the old token remains valid for **60 seconds** to prevent race conditions in Single Page Applications (SPAs) making concurrent requests.
|
|
38
|
+
|
|
39
|
+
**How it works:**
|
|
40
|
+
1. A request is made with an active token older than `rotationAge`.
|
|
41
|
+
2. Odac issues a brand new token set and sends them as cookies.
|
|
42
|
+
3. The old token is marked as "rotated" and assigned a 60-second lütuf (grace) period.
|
|
43
|
+
4. Subsequent concurrent requests using the old token still pass within those 60 seconds.
|
|
44
|
+
5. After 60 seconds, the old token is naturally expired.
|
|
45
|
+
|
|
32
46
|
### Configuration
|
|
33
47
|
|
|
34
48
|
Configure session behavior in `odac.json`:
|
|
@@ -39,21 +53,29 @@ Configure session behavior in `odac.json`:
|
|
|
39
53
|
"table": "users",
|
|
40
54
|
"token": "user_tokens",
|
|
41
55
|
"maxAge": 2592000000,
|
|
42
|
-
"updateAge": 86400000
|
|
56
|
+
"updateAge": 86400000,
|
|
57
|
+
"rotationAge": 900000,
|
|
58
|
+
"rotation": true
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
```
|
|
46
62
|
|
|
47
63
|
**Options:**
|
|
48
64
|
|
|
49
|
-
- `maxAge` (milliseconds): Maximum inactivity period before session expires
|
|
65
|
+
- `maxAge` (milliseconds): Maximum inactivity period before session expires. **Note:** Cookies also use this value for persistence.
|
|
50
66
|
- Default: `2592000000` (30 days)
|
|
51
67
|
- Example: `604800000` (7 days)
|
|
52
68
|
|
|
53
|
-
- `updateAge` (milliseconds): How often to update the session timestamp
|
|
69
|
+
- `updateAge` (milliseconds): How often to update the session timestamp (heartbeat)
|
|
54
70
|
- Default: `86400000` (1 day)
|
|
55
71
|
- Example: `3600000` (1 hour)
|
|
56
72
|
|
|
73
|
+
- `rotationAge` (milliseconds): How often to rotate the session tokens
|
|
74
|
+
- Default: `900000` (15 minutes)
|
|
75
|
+
|
|
76
|
+
- `rotation` (boolean): Enable or disable token rotation
|
|
77
|
+
- Default: `true`
|
|
78
|
+
|
|
57
79
|
### Common Configurations
|
|
58
80
|
|
|
59
81
|
**Short sessions (banking apps):**
|
package/package.json
CHANGED
package/src/Auth.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const nodeCrypto = require('crypto')
|
|
2
|
+
const ROTATED_TOKEN_EPOCH_THRESHOLD_MS = 31536000000
|
|
3
|
+
const TOKEN_ROTATION_GRACE_PERIOD_MS = 60 * 1000
|
|
2
4
|
class Auth {
|
|
3
5
|
#request = null
|
|
4
6
|
#table = null
|
|
5
7
|
#user = null
|
|
8
|
+
static #migrationCache = new Set()
|
|
6
9
|
|
|
7
10
|
constructor(request) {
|
|
8
11
|
this.#request = request
|
|
@@ -98,7 +101,10 @@ class Auth {
|
|
|
98
101
|
|
|
99
102
|
// Code First Migration: Ensure token table exists and clean up old tokens
|
|
100
103
|
try {
|
|
101
|
-
|
|
104
|
+
if (!Auth.#migrationCache.has(tokenTable)) {
|
|
105
|
+
await this.#ensureTokenTableV2(tokenTable)
|
|
106
|
+
Auth.#migrationCache.add(tokenTable)
|
|
107
|
+
}
|
|
102
108
|
} catch (e) {
|
|
103
109
|
console.error('Odac Auth Error: Failed to ensure token table exists:', e.message)
|
|
104
110
|
}
|
|
@@ -112,13 +118,21 @@ class Auth {
|
|
|
112
118
|
|
|
113
119
|
const maxAge = Odac.Config.auth?.maxAge || 30 * 24 * 60 * 60 * 1000
|
|
114
120
|
const updateAge = Odac.Config.auth?.updateAge || 24 * 60 * 60 * 1000
|
|
121
|
+
const rotationAge = Odac.Config.auth?.rotationAge || 15 * 60 * 1000 // Default 15 mins for rotation
|
|
122
|
+
const shouldRotate = Odac.Config.auth?.rotation !== false // Allow disabling rotation
|
|
115
123
|
const now = Date.now()
|
|
116
124
|
|
|
117
125
|
// Active comes as Date object usually from drivers
|
|
118
126
|
const lastActive = new Date(sql_token[0].active).getTime()
|
|
127
|
+
const tokenDate = new Date(sql_token[0].date).getTime()
|
|
119
128
|
const inactiveAge = now - lastActive
|
|
129
|
+
const tokenAge = now - tokenDate
|
|
130
|
+
|
|
131
|
+
// If date is before 1971, it's a marker for a rotated (grace period) token
|
|
132
|
+
const isRotated = tokenDate < ROTATED_TOKEN_EPOCH_THRESHOLD_MS
|
|
120
133
|
|
|
121
134
|
if (inactiveAge > maxAge) {
|
|
135
|
+
// Naturally cleans up expired tokens and rotated tokens after grace period
|
|
122
136
|
await Odac.DB[tokenTable].where('id', sql_token[0].id).delete()
|
|
123
137
|
return false
|
|
124
138
|
}
|
|
@@ -126,12 +140,63 @@ class Auth {
|
|
|
126
140
|
this.#user = await Odac.DB[this.#table].where(primaryKey, sql_token[0].user).first()
|
|
127
141
|
if (!this.#user) return false
|
|
128
142
|
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
if (!isRotated) {
|
|
144
|
+
if (shouldRotate && tokenAge > rotationAge) {
|
|
145
|
+
// --- Token Rotation ---
|
|
146
|
+
const newTokenX = nodeCrypto.randomBytes(32).toString('hex')
|
|
147
|
+
const newTokenY = nodeCrypto.randomBytes(32).toString('hex')
|
|
148
|
+
const newToken = {
|
|
149
|
+
id: Odac.DB.nanoid(),
|
|
150
|
+
user: sql_token[0].user,
|
|
151
|
+
token_x: newTokenX,
|
|
152
|
+
token_y: Odac.Var(newTokenY).hash(),
|
|
153
|
+
browser: sql_token[0].browser,
|
|
154
|
+
ip: this.#request.ip,
|
|
155
|
+
date: new Date(),
|
|
156
|
+
active: new Date()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 1. Persist new token (await to ensure it exists before client uses new cookies)
|
|
160
|
+
const insertOk = await Odac.DB[tokenTable].insert(newToken).catch(e => {
|
|
161
|
+
console.error('Odac Auth Error: Token rotation failed', e.message)
|
|
162
|
+
return false
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (insertOk !== false) {
|
|
166
|
+
// 2. Mark old token as rotated and set exactly 60 seconds grace period
|
|
167
|
+
// Non-blocking I/O (Fire & Forget) -> High Throughput
|
|
168
|
+
const rotatedActiveDate = new Date(now - maxAge + TOKEN_ROTATION_GRACE_PERIOD_MS)
|
|
169
|
+
const epochDate = new Date(0)
|
|
170
|
+
|
|
171
|
+
Odac.DB[tokenTable]
|
|
172
|
+
.where('id', sql_token[0].id)
|
|
173
|
+
.update({
|
|
174
|
+
active: rotatedActiveDate,
|
|
175
|
+
date: epochDate
|
|
176
|
+
})
|
|
177
|
+
.catch(() => {})
|
|
178
|
+
|
|
179
|
+
// 3. Issue new cookies immediately
|
|
180
|
+
this.#request.cookie('odac_x', newTokenX, {
|
|
181
|
+
httpOnly: true,
|
|
182
|
+
secure: true,
|
|
183
|
+
sameSite: 'Lax',
|
|
184
|
+
maxAge: maxAge
|
|
185
|
+
})
|
|
186
|
+
this.#request.cookie('odac_y', newTokenY, {
|
|
187
|
+
httpOnly: true,
|
|
188
|
+
secure: true,
|
|
189
|
+
sameSite: 'Lax',
|
|
190
|
+
maxAge: maxAge
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
} else if (inactiveAge > updateAge) {
|
|
194
|
+
// Fallback simple active update if rotation is not triggered
|
|
195
|
+
Odac.DB[tokenTable]
|
|
196
|
+
.where('id', sql_token[0].id)
|
|
197
|
+
.update({active: new Date()})
|
|
198
|
+
.catch(() => {})
|
|
199
|
+
}
|
|
135
200
|
}
|
|
136
201
|
|
|
137
202
|
return true
|
|
@@ -143,11 +208,13 @@ class Auth {
|
|
|
143
208
|
let user = await this.check(where)
|
|
144
209
|
if (!user) return false
|
|
145
210
|
|
|
146
|
-
if (!Odac.Config.auth) Odac.Config.auth = {}
|
|
147
211
|
let key = Odac.Config.auth.key || 'id'
|
|
148
212
|
let token = Odac.Config.auth.token || 'odac_auth'
|
|
149
213
|
|
|
150
|
-
|
|
214
|
+
if (!Auth.#migrationCache.has(token)) {
|
|
215
|
+
await this.#ensureTokenTableV2(token)
|
|
216
|
+
Auth.#migrationCache.add(token)
|
|
217
|
+
}
|
|
151
218
|
|
|
152
219
|
this.#cleanupExpiredTokens(token)
|
|
153
220
|
|
|
@@ -165,12 +232,20 @@ class Auth {
|
|
|
165
232
|
ip: this.#request.ip
|
|
166
233
|
}
|
|
167
234
|
|
|
235
|
+
const maxAge = Odac.Config.auth?.maxAge || 30 * 24 * 60 * 60 * 1000
|
|
236
|
+
|
|
168
237
|
this.#request.cookie('odac_x', cookie.token_x, {
|
|
169
238
|
httpOnly: true,
|
|
170
239
|
secure: true,
|
|
171
|
-
sameSite: 'Lax'
|
|
240
|
+
sameSite: 'Lax',
|
|
241
|
+
maxAge: maxAge
|
|
242
|
+
})
|
|
243
|
+
this.#request.cookie('odac_y', token_y, {
|
|
244
|
+
httpOnly: true,
|
|
245
|
+
secure: true,
|
|
246
|
+
sameSite: 'Lax',
|
|
247
|
+
maxAge: maxAge
|
|
172
248
|
})
|
|
173
|
-
this.#request.cookie('odac_y', token_y, {httpOnly: true, secure: true, sameSite: 'Lax'})
|
|
174
249
|
|
|
175
250
|
// Knex insert returns ids on some dbs, promise resolves to result
|
|
176
251
|
const result = await Odac.DB[token].insert(cookie)
|
|
@@ -199,7 +274,10 @@ class Auth {
|
|
|
199
274
|
const uniqueFields = options.uniqueFields || ['email']
|
|
200
275
|
|
|
201
276
|
try {
|
|
202
|
-
|
|
277
|
+
if (!Auth.#migrationCache.has(this.#table)) {
|
|
278
|
+
await this.#ensureUserTableV2(this.#table, primaryKey, passwordField, uniqueFields, data)
|
|
279
|
+
Auth.#migrationCache.add(this.#table)
|
|
280
|
+
}
|
|
203
281
|
} catch (e) {
|
|
204
282
|
// If DB not configured or connection failed
|
|
205
283
|
console.error('Odac Auth Error:', e.message)
|
|
@@ -302,12 +380,16 @@ class Auth {
|
|
|
302
380
|
if (!this.#user) return false
|
|
303
381
|
|
|
304
382
|
if (!Odac.Config.auth) Odac.Config.auth = {}
|
|
305
|
-
const
|
|
383
|
+
const tokenTable = Odac.Config.auth.token || 'user_tokens'
|
|
384
|
+
const primaryKey = Odac.Config.auth.key || 'id'
|
|
306
385
|
const odacX = this.#request.cookie('odac_x')
|
|
307
386
|
const browser = this.#request.header('user-agent')
|
|
308
387
|
|
|
309
388
|
if (odacX && browser) {
|
|
310
|
-
|
|
389
|
+
// Delete current token AND any rotated grace-period tokens for this user+browser
|
|
390
|
+
// Why: After rotation, the old token stays alive for ~60s. Explicit logout must kill it too.
|
|
391
|
+
const userId = this.#user[primaryKey]
|
|
392
|
+
await Odac.DB[tokenTable].where('user', userId).where('browser', browser).delete()
|
|
311
393
|
}
|
|
312
394
|
|
|
313
395
|
this.#request.cookie('odac_x', '', {maxAge: -1})
|
|
@@ -326,7 +408,10 @@ class Auth {
|
|
|
326
408
|
|
|
327
409
|
// Ensure magic table exists
|
|
328
410
|
try {
|
|
329
|
-
|
|
411
|
+
if (!Auth.#migrationCache.has(magicTable)) {
|
|
412
|
+
await this.#ensureMagicLinkTable(magicTable)
|
|
413
|
+
Auth.#migrationCache.add(magicTable)
|
|
414
|
+
}
|
|
330
415
|
} catch (e) {
|
|
331
416
|
console.error('Failed to ensure magic link table exists:', e)
|
|
332
417
|
// Consider returning an error here to prevent further execution.
|
package/src/Route/Internal.js
CHANGED
|
@@ -541,23 +541,13 @@ class Internal {
|
|
|
541
541
|
|
|
542
542
|
if (Odac.formConfig.action) {
|
|
543
543
|
const actionParts = Odac.formConfig.action.split('.')
|
|
544
|
-
if (actionParts.length
|
|
544
|
+
if (actionParts.length >= 2) {
|
|
545
545
|
const controllerName = actionParts[0]
|
|
546
|
-
const methodName = actionParts[1]
|
|
547
|
-
|
|
548
|
-
// Dynamically load controller
|
|
549
|
-
// We need to access Odac.Route.class to find the controller path/module
|
|
550
|
-
// Or use require directly if we know the path structure.
|
|
551
|
-
// Since we are in framework/src/Route/Internal.js, controllers are in framework/controller/ OR app/controller/
|
|
552
|
-
// Ideally Odac.Route.class has the loaded controllers.
|
|
553
546
|
|
|
554
547
|
let controllerModule = null
|
|
555
548
|
|
|
556
549
|
if (Odac.Route && Odac.Route.class && Odac.Route.class[controllerName]) {
|
|
557
550
|
controllerModule = Odac.Route.class[controllerName].module
|
|
558
|
-
} else {
|
|
559
|
-
// Try to require it if not loaded (though Route.js should have loaded it)
|
|
560
|
-
// This fallback might be tricky with absolute paths, relying on Route.class is safer.
|
|
561
551
|
}
|
|
562
552
|
|
|
563
553
|
if (controllerModule) {
|
|
@@ -605,16 +595,29 @@ class Internal {
|
|
|
605
595
|
}
|
|
606
596
|
}
|
|
607
597
|
|
|
608
|
-
|
|
598
|
+
let method = controllerModule
|
|
599
|
+
let context = null
|
|
600
|
+
let isClass = false
|
|
601
|
+
|
|
609
602
|
if (typeof controllerModule === 'function' && controllerModule.prototype) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
603
|
+
isClass = true
|
|
604
|
+
context = new controllerModule(Odac)
|
|
605
|
+
method = context
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
for (let i = 1; i < actionParts.length; i++) {
|
|
609
|
+
if (method) {
|
|
610
|
+
context = method
|
|
611
|
+
method = method[actionParts[i]]
|
|
613
612
|
}
|
|
614
613
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
614
|
+
|
|
615
|
+
if (typeof method === 'function') {
|
|
616
|
+
if (isClass) {
|
|
617
|
+
return await method.call(context, formHelper)
|
|
618
|
+
} else {
|
|
619
|
+
return await method.call(context, Odac, formHelper)
|
|
620
|
+
}
|
|
618
621
|
}
|
|
619
622
|
} catch (e) {
|
|
620
623
|
console.error(e)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
html: 'text/html',
|
|
3
|
+
css: 'text/css',
|
|
4
|
+
js: 'text/javascript',
|
|
5
|
+
json: 'application/json',
|
|
6
|
+
png: 'image/png',
|
|
7
|
+
jpg: 'image/jpg',
|
|
8
|
+
jpeg: 'image/jpeg',
|
|
9
|
+
svg: 'image/svg+xml',
|
|
10
|
+
ico: 'image/x-icon',
|
|
11
|
+
mp3: 'audio/mpeg',
|
|
12
|
+
mp4: 'video/mp4',
|
|
13
|
+
webm: 'video/webm',
|
|
14
|
+
woff: 'font/woff',
|
|
15
|
+
woff2: 'font/woff2',
|
|
16
|
+
ttf: 'font/ttf',
|
|
17
|
+
otf: 'font/otf',
|
|
18
|
+
eot: 'font/eot',
|
|
19
|
+
pdf: 'application/pdf',
|
|
20
|
+
zip: 'application/zip',
|
|
21
|
+
tar: 'application/x-tar',
|
|
22
|
+
gz: 'application/gzip',
|
|
23
|
+
rar: 'application/x-rar-compressed',
|
|
24
|
+
'7z': 'application/x-7z-compressed',
|
|
25
|
+
txt: 'text/plain',
|
|
26
|
+
log: 'text/plain',
|
|
27
|
+
csv: 'text/csv',
|
|
28
|
+
xml: 'text/xml',
|
|
29
|
+
rss: 'application/rss+xml',
|
|
30
|
+
atom: 'application/atom+xml',
|
|
31
|
+
yaml: 'application/x-yaml',
|
|
32
|
+
sh: 'application/x-sh',
|
|
33
|
+
bat: 'application/x-bat',
|
|
34
|
+
exe: 'application/x-exe',
|
|
35
|
+
bin: 'application/x-binary',
|
|
36
|
+
doc: 'application/msword',
|
|
37
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
38
|
+
xls: 'application/vnd.ms-excel',
|
|
39
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
40
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
41
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
42
|
+
avi: 'video/x-msvideo',
|
|
43
|
+
wmv: 'video/x-ms-wmv',
|
|
44
|
+
flv: 'video/x-flv',
|
|
45
|
+
webp: 'image/webp',
|
|
46
|
+
gif: 'image/gif',
|
|
47
|
+
bmp: 'image/bmp',
|
|
48
|
+
tiff: 'image/tiff',
|
|
49
|
+
tif: 'image/tiff',
|
|
50
|
+
weba: 'audio/webm',
|
|
51
|
+
wav: 'audio/wav',
|
|
52
|
+
ogg: 'audio/ogg',
|
|
53
|
+
flac: 'audio/flac',
|
|
54
|
+
aac: 'audio/aac',
|
|
55
|
+
midi: 'audio/midi'
|
|
56
|
+
}
|
package/src/Route.js
CHANGED
|
@@ -7,62 +7,7 @@ const MiddlewareChain = require('./Route/Middleware.js')
|
|
|
7
7
|
const {WebSocketServer} = require('./WebSocket.js')
|
|
8
8
|
|
|
9
9
|
var routes2 = {}
|
|
10
|
-
const mime =
|
|
11
|
-
html: 'text/html',
|
|
12
|
-
css: 'text/css',
|
|
13
|
-
js: 'text/javascript',
|
|
14
|
-
json: 'application/json',
|
|
15
|
-
png: 'image/png',
|
|
16
|
-
jpg: 'image/jpg',
|
|
17
|
-
jpeg: 'image/jpeg',
|
|
18
|
-
svg: 'image/svg+xml',
|
|
19
|
-
ico: 'image/x-icon',
|
|
20
|
-
mp3: 'audio/mpeg',
|
|
21
|
-
mp4: 'video/mp4',
|
|
22
|
-
webm: 'video/webm',
|
|
23
|
-
woff: 'font/woff',
|
|
24
|
-
woff2: 'font/woff2',
|
|
25
|
-
ttf: 'font/ttf',
|
|
26
|
-
otf: 'font/otf',
|
|
27
|
-
eot: 'font/eot',
|
|
28
|
-
pdf: 'application/pdf',
|
|
29
|
-
zip: 'application/zip',
|
|
30
|
-
tar: 'application/x-tar',
|
|
31
|
-
gz: 'application/gzip',
|
|
32
|
-
rar: 'application/x-rar-compressed',
|
|
33
|
-
'7z': 'application/x-7z-compressed',
|
|
34
|
-
txt: 'text/plain',
|
|
35
|
-
log: 'text/plain',
|
|
36
|
-
csv: 'text/csv',
|
|
37
|
-
xml: 'text/xml',
|
|
38
|
-
rss: 'application/rss+xml',
|
|
39
|
-
atom: 'application/atom+xml',
|
|
40
|
-
yaml: 'application/x-yaml',
|
|
41
|
-
sh: 'application/x-sh',
|
|
42
|
-
bat: 'application/x-bat',
|
|
43
|
-
exe: 'application/x-exe',
|
|
44
|
-
bin: 'application/x-binary',
|
|
45
|
-
doc: 'application/msword',
|
|
46
|
-
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
47
|
-
xls: 'application/vnd.ms-excel',
|
|
48
|
-
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
49
|
-
ppt: 'application/vnd.ms-powerpoint',
|
|
50
|
-
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
51
|
-
avi: 'video/x-msvideo',
|
|
52
|
-
wmv: 'video/x-ms-wmv',
|
|
53
|
-
flv: 'video/x-flv',
|
|
54
|
-
webp: 'image/webp',
|
|
55
|
-
gif: 'image/gif',
|
|
56
|
-
bmp: 'image/bmp',
|
|
57
|
-
tiff: 'image/tiff',
|
|
58
|
-
tif: 'image/tiff',
|
|
59
|
-
weba: 'audio/webm',
|
|
60
|
-
wav: 'audio/wav',
|
|
61
|
-
ogg: 'audio/ogg',
|
|
62
|
-
flac: 'audio/flac',
|
|
63
|
-
aac: 'audio/aac',
|
|
64
|
-
midi: 'audio/midi'
|
|
65
|
-
}
|
|
10
|
+
const mime = require('./Route/MimeTypes.js')
|
|
66
11
|
|
|
67
12
|
class Route {
|
|
68
13
|
loading = false
|
|
@@ -122,15 +67,37 @@ class Route {
|
|
|
122
67
|
if (middlewareResult !== undefined) return middlewareResult
|
|
123
68
|
|
|
124
69
|
if (controller.action) {
|
|
125
|
-
const
|
|
70
|
+
const ControllerModule = controller.cache
|
|
71
|
+
const actionParts = controller.action.split('.')
|
|
72
|
+
|
|
126
73
|
try {
|
|
127
|
-
const instance = new
|
|
128
|
-
|
|
129
|
-
|
|
74
|
+
const instance = new ControllerModule(Odac)
|
|
75
|
+
let method = instance
|
|
76
|
+
let context = instance
|
|
77
|
+
|
|
78
|
+
for (const segment of actionParts) {
|
|
79
|
+
if (method) {
|
|
80
|
+
context = method
|
|
81
|
+
method = method[segment]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof method === 'function') {
|
|
86
|
+
return method.call(context, Odac)
|
|
130
87
|
}
|
|
131
88
|
} catch {
|
|
132
|
-
|
|
133
|
-
|
|
89
|
+
let method = ControllerModule
|
|
90
|
+
let context = ControllerModule
|
|
91
|
+
|
|
92
|
+
for (const segment of actionParts) {
|
|
93
|
+
if (method) {
|
|
94
|
+
context = method
|
|
95
|
+
method = method[segment]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof method === 'function') {
|
|
100
|
+
return method.call(context, Odac)
|
|
134
101
|
}
|
|
135
102
|
}
|
|
136
103
|
return Odac.Request.abort(500)
|
|
@@ -144,6 +111,9 @@ class Route {
|
|
|
144
111
|
async check(Odac) {
|
|
145
112
|
let url = Odac.Request.url.split('?')[0]
|
|
146
113
|
if (url.endsWith('/')) url = url.slice(0, -1)
|
|
114
|
+
// Global Auth Check: Load user if valid tokens exist to simplify DX
|
|
115
|
+
// This allows calling Odac.Auth.user() anywhere without manual await Odac.Auth.check()
|
|
116
|
+
if (Odac.Auth) await Odac.Auth.check()
|
|
147
117
|
|
|
148
118
|
if (url.startsWith('/_odac/')) {
|
|
149
119
|
Odac.Request.route = '_odac_internal'
|
|
@@ -882,8 +852,15 @@ class Route {
|
|
|
882
852
|
}
|
|
883
853
|
})
|
|
884
854
|
|
|
855
|
+
// Global Auth Check: Load user if valid tokens exist to simplify DX
|
|
856
|
+
// This allows calling Odac.Auth.user() anywhere without manual await Odac.Auth.check()
|
|
857
|
+
if (Odac.Auth) await Odac.Auth.check()
|
|
858
|
+
|
|
885
859
|
if (requireAuth) {
|
|
886
|
-
|
|
860
|
+
let isAuthenticated = false
|
|
861
|
+
if (Odac.Auth) {
|
|
862
|
+
isAuthenticated = await Odac.Auth.check()
|
|
863
|
+
}
|
|
887
864
|
if (!isAuthenticated) {
|
|
888
865
|
ws.close(4001, 'Unauthorized')
|
|
889
866
|
return
|