odac 1.4.8 → 1.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/docs/ai/README.md +2 -1
  3. package/docs/ai/skills/SKILL.md +2 -1
  4. package/docs/ai/skills/backend/authentication.md +12 -6
  5. package/docs/ai/skills/backend/database.md +85 -5
  6. package/docs/ai/skills/backend/migrations.md +23 -0
  7. package/docs/ai/skills/backend/odac-var.md +155 -0
  8. package/docs/ai/skills/backend/utilities.md +1 -1
  9. package/docs/ai/skills/frontend/forms.md +23 -1
  10. package/docs/backend/04-routing/09-websocket-quick-reference.md +21 -1
  11. package/docs/backend/04-routing/09-websocket.md +22 -1
  12. package/docs/backend/08-database/06-read-through-cache.md +206 -0
  13. package/docs/backend/10-authentication/01-authentication-basics.md +53 -0
  14. package/docs/backend/10-authentication/05-session-management.md +12 -3
  15. package/docs/backend/13-utilities/01-odac-var.md +13 -19
  16. package/docs/frontend/03-forms/01-form-handling.md +15 -2
  17. package/docs/index.json +1 -1
  18. package/package.json +1 -1
  19. package/src/Auth.js +17 -0
  20. package/src/Database/Migration.js +321 -10
  21. package/src/Database/ReadCache.js +174 -0
  22. package/src/Database/WriteBuffer.js +15 -1
  23. package/src/Database.js +78 -1
  24. package/src/Validator.js +1 -1
  25. package/src/Var.js +1 -0
  26. package/src/WebSocket.js +80 -23
  27. package/test/Database/Migration/migrate_column.test.js +311 -0
  28. package/test/Database/ReadCache/crossTable.test.js +179 -0
  29. package/test/Database/ReadCache/get.test.js +128 -0
  30. package/test/Database/ReadCache/invalidate.test.js +103 -0
  31. package/test/Database/ReadCache/proxy.test.js +184 -0
  32. package/test/Database/WriteBuffer/insert.test.js +118 -0
  33. package/test/Database/insert.test.js +98 -0
  34. package/test/WebSocket/Client/fragmentation.test.js +130 -0
  35. package/test/WebSocket/Client/limits.test.js +10 -4
  36. package/test/WebSocket/Client/readyState.test.js +154 -0
  37. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +0 -55
@@ -0,0 +1,118 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+
5
+ /**
6
+ * Tests WriteBuffer.insert() nanoid auto-generation.
7
+ * Why: WriteBuffer bypasses the Database.js proxy QB nanoid injection.
8
+ * Rows must be populated with nanoid values before being queued to IPC,
9
+ * otherwise flush writes to DB with a null primary key and violates NOT NULL.
10
+ */
11
+
12
+ let knexLib, db
13
+
14
+ beforeEach(async () => {
15
+ jest.resetModules()
16
+
17
+ knexLib = require('knex')
18
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
19
+
20
+ await db.schema.createTable('activity', table => {
21
+ table.string('id', 21).primary().notNullable()
22
+ table.string('user', 255).notNullable()
23
+ table.string('action', 50).notNullable()
24
+ })
25
+
26
+ await db.schema.createTable('events', table => {
27
+ table.string('eid', 12).primary().notNullable()
28
+ table.string('name', 100)
29
+ })
30
+
31
+ Object.defineProperty(cluster, 'isPrimary', {value: true, configurable: true})
32
+
33
+ const Ipc = require('../../../src/Ipc')
34
+ global.Odac = {
35
+ Config: {buffer: {flushInterval: 999999, checkpointInterval: 999999}},
36
+ Storage: {
37
+ isReady: () => false,
38
+ put: jest.fn(),
39
+ remove: jest.fn(),
40
+ getRange: () => []
41
+ },
42
+ Ipc
43
+ }
44
+ await Ipc.init()
45
+
46
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
47
+ await writeBuffer.init(
48
+ {default: db},
49
+ {
50
+ default: {
51
+ activity: [{column: 'id', size: 21}],
52
+ events: [{column: 'eid', size: 12}]
53
+ }
54
+ }
55
+ )
56
+
57
+ const DB = require('../../../src/Database')
58
+ DB.connections = {default: db}
59
+ })
60
+
61
+ afterEach(async () => {
62
+ const writeBuffer = require('../../../src/Database/WriteBuffer')
63
+ await writeBuffer.close()
64
+ await Odac.Ipc.close()
65
+ await db.destroy()
66
+ delete global.Odac
67
+ })
68
+
69
+ describe('WriteBuffer.insert() - NanoID auto-generation', () => {
70
+ it('should auto-generate nanoid for a column when not provided', async () => {
71
+ const DB = require('../../../src/Database')
72
+
73
+ await DB.activity.buffer.insert({user: 'alice', action: 'login'})
74
+ await DB.activity.buffer.flush()
75
+
76
+ const rows = await db('activity').select()
77
+ expect(rows).toHaveLength(1)
78
+ expect(rows[0].id).toBeTruthy()
79
+ expect(rows[0].id).toHaveLength(21)
80
+ })
81
+
82
+ it('should not overwrite an explicitly provided id', async () => {
83
+ const DB = require('../../../src/Database')
84
+
85
+ await DB.activity.buffer.insert({id: 'my-custom-id-00000', user: 'bob', action: 'logout'})
86
+ await DB.activity.buffer.flush()
87
+
88
+ const row = await db('activity').first()
89
+ expect(row.id).toBe('my-custom-id-00000')
90
+ })
91
+
92
+ it('should respect custom nanoid length from schema metadata', async () => {
93
+ const DB = require('../../../src/Database')
94
+
95
+ await DB.events.buffer.insert({name: 'page_view'})
96
+ await DB.events.buffer.flush()
97
+
98
+ const row = await db('events').first()
99
+ expect(row.eid).toBeTruthy()
100
+ expect(row.eid).toHaveLength(12)
101
+ })
102
+
103
+ it('should generate unique ids for multiple buffered inserts', async () => {
104
+ const DB = require('../../../src/Database')
105
+
106
+ await DB.activity.buffer.insert({user: 'alice', action: 'login'})
107
+ await DB.activity.buffer.insert({user: 'bob', action: 'view'})
108
+ await DB.activity.buffer.insert({user: 'carol', action: 'logout'})
109
+ await DB.activity.buffer.flush()
110
+
111
+ const rows = await db('activity').select()
112
+ expect(rows).toHaveLength(3)
113
+
114
+ const ids = rows.map(r => r.id)
115
+ expect(new Set(ids).size).toBe(3) // all unique
116
+ ids.forEach(id => expect(id).toHaveLength(21))
117
+ })
118
+ })
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const knexLib = require('knex')
4
+
5
+ /**
6
+ * Tests the wrapWithInvalidation thenable returned by write operations (insert/update/delete/truncate).
7
+ * Why: Ensures the thenable is fully Promise-compatible — supporting both await and .catch() chaining.
8
+ * This prevents TypeError when consumers call .insert(...).catch() or .update(...).catch().
9
+ */
10
+
11
+ let db
12
+
13
+ beforeEach(async () => {
14
+ db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
15
+ await db.schema.createTable('tokens', table => {
16
+ table.string('id', 21).primary()
17
+ table.string('user', 100)
18
+ table.string('token_x', 64)
19
+ })
20
+ })
21
+
22
+ afterEach(async () => {
23
+ await db.destroy()
24
+ jest.resetModules()
25
+ })
26
+
27
+ describe('Database.js - wrapWithInvalidation thenable', () => {
28
+ it('insert().catch() should be a function', () => {
29
+ const DB = require('../../src/Database')
30
+ DB.connections = {default: db}
31
+ db._odacConnectionKey = 'default'
32
+ DB._nanoidColumns = {}
33
+
34
+ const result = DB.tokens.insert({id: 'test1', user: 'u1', token_x: 'tx1'})
35
+ expect(typeof result.catch).toBe('function')
36
+ })
37
+
38
+ it('insert().catch() should resolve on success', async () => {
39
+ const DB = require('../../src/Database')
40
+ DB.connections = {default: db}
41
+ db._odacConnectionKey = 'default'
42
+ DB._nanoidColumns = {}
43
+
44
+ const result = await DB.tokens.insert({id: 'test2', user: 'u2', token_x: 'tx2'}).catch(() => false)
45
+ expect(result).not.toBe(false)
46
+
47
+ const rows = await db('tokens').where('id', 'test2')
48
+ expect(rows).toHaveLength(1)
49
+ expect(rows[0].user).toBe('u2')
50
+ })
51
+
52
+ it('insert().catch() should catch errors gracefully', async () => {
53
+ const DB = require('../../src/Database')
54
+ DB.connections = {default: db}
55
+ db._odacConnectionKey = 'default'
56
+ DB._nanoidColumns = {}
57
+
58
+ // Insert first row
59
+ await DB.tokens.insert({id: 'dup1', user: 'u1', token_x: 'tx1'})
60
+
61
+ // Duplicate primary key — should trigger catch
62
+ const result = await DB.tokens.insert({id: 'dup1', user: 'u2', token_x: 'tx2'}).catch(() => 'caught')
63
+ expect(result).toBe('caught')
64
+ })
65
+
66
+ it('update().catch() should be a function', () => {
67
+ const DB = require('../../src/Database')
68
+ DB.connections = {default: db}
69
+ db._odacConnectionKey = 'default'
70
+ DB._nanoidColumns = {}
71
+
72
+ const result = DB.tokens.where('id', 'x').update({user: 'new'})
73
+ expect(typeof result.catch).toBe('function')
74
+ })
75
+
76
+ it('delete().catch() should be a function', () => {
77
+ const DB = require('../../src/Database')
78
+ DB.connections = {default: db}
79
+ db._odacConnectionKey = 'default'
80
+ DB._nanoidColumns = {}
81
+
82
+ const result = DB.tokens.where('id', 'x').delete()
83
+ expect(typeof result.catch).toBe('function')
84
+ })
85
+
86
+ it('insert() should work with await (no .catch)', async () => {
87
+ const DB = require('../../src/Database')
88
+ DB.connections = {default: db}
89
+ db._odacConnectionKey = 'default'
90
+ DB._nanoidColumns = {}
91
+
92
+ await DB.tokens.insert({id: 'await1', user: 'u_await', token_x: 'tx_await'})
93
+
94
+ const rows = await db('tokens').where('id', 'await1')
95
+ expect(rows).toHaveLength(1)
96
+ expect(rows[0].user).toBe('u_await')
97
+ })
98
+ })
@@ -0,0 +1,130 @@
1
+ const {WebSocketServer, WebSocketClient} = require('../../../src/WebSocket.js')
2
+
3
+ /**
4
+ * Builds a masked WebSocket frame from raw payload bytes.
5
+ * Uses a zero mask key for deterministic test output.
6
+ */
7
+ function buildFrame(opcode, payload, fin = true) {
8
+ const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
9
+ const maskKey = Buffer.alloc(4) // zero mask — XOR is identity
10
+ const masked = Buffer.from(buf)
11
+
12
+ const finBit = fin ? 0x80 : 0x00
13
+ const header = Buffer.alloc(2 + 4 + buf.length)
14
+ header[0] = finBit | opcode
15
+ header[1] = 0x80 | buf.length // masked bit + length
16
+ maskKey.copy(header, 2)
17
+ masked.copy(header, 6)
18
+
19
+ return header
20
+ }
21
+
22
+ function createMockSocket() {
23
+ return {
24
+ pause: jest.fn(),
25
+ resume: jest.fn(),
26
+ on: jest.fn(),
27
+ write: jest.fn(),
28
+ end: jest.fn(),
29
+ removeAllListeners: jest.fn(),
30
+ writable: true
31
+ }
32
+ }
33
+
34
+ describe('WebSocketClient Fragmentation', () => {
35
+ let server
36
+
37
+ beforeEach(() => {
38
+ server = new WebSocketServer()
39
+ })
40
+
41
+ it('should reassemble fragmented text messages', () => {
42
+ const socket = createMockSocket()
43
+ const client = new WebSocketClient(socket, server, 'frag-1')
44
+ client.resume()
45
+
46
+ const messages = []
47
+ client.on('message', msg => messages.push(msg))
48
+
49
+ const dataHandler = socket.on.mock.calls.find(c => c[0] === 'data')[1]
50
+
51
+ // Fragment 1: TEXT opcode, fin=false
52
+ dataHandler(buildFrame(0x1, 'hel', false))
53
+ // Fragment 2: CONTINUATION opcode, fin=false
54
+ dataHandler(buildFrame(0x0, 'lo ', false))
55
+ // Fragment 3: CONTINUATION opcode, fin=true
56
+ dataHandler(buildFrame(0x0, 'world', true))
57
+
58
+ expect(messages).toEqual(['hello world'])
59
+ })
60
+
61
+ it('should reassemble fragmented binary messages', () => {
62
+ const socket = createMockSocket()
63
+ const client = new WebSocketClient(socket, server, 'frag-2')
64
+ client.resume()
65
+
66
+ const messages = []
67
+ client.on('message', msg => messages.push(msg))
68
+
69
+ const dataHandler = socket.on.mock.calls.find(c => c[0] === 'data')[1]
70
+
71
+ const part1 = Buffer.from([0x01, 0x02])
72
+ const part2 = Buffer.from([0x03, 0x04])
73
+
74
+ // Fragment 1: BINARY opcode, fin=false
75
+ dataHandler(buildFrame(0x2, part1, false))
76
+ // Fragment 2: CONTINUATION opcode, fin=true
77
+ dataHandler(buildFrame(0x0, part2, true))
78
+
79
+ expect(messages.length).toBe(1)
80
+ expect(Buffer.isBuffer(messages[0])).toBe(true)
81
+ expect(messages[0]).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04]))
82
+ })
83
+
84
+ it('should close with 1002 on unexpected continuation frame', () => {
85
+ const socket = createMockSocket()
86
+ const client = new WebSocketClient(socket, server, 'frag-3')
87
+ client.resume()
88
+
89
+ const dataHandler = socket.on.mock.calls.find(c => c[0] === 'data')[1]
90
+
91
+ // Send CONTINUATION without a preceding TEXT/BINARY
92
+ dataHandler(buildFrame(0x0, 'orphan', true))
93
+
94
+ expect(socket.end).toHaveBeenCalled()
95
+ })
96
+
97
+ it('should handle single unfragmented message normally', () => {
98
+ const socket = createMockSocket()
99
+ const client = new WebSocketClient(socket, server, 'frag-4')
100
+ client.resume()
101
+
102
+ const messages = []
103
+ client.on('message', msg => messages.push(msg))
104
+
105
+ const dataHandler = socket.on.mock.calls.find(c => c[0] === 'data')[1]
106
+
107
+ // Single complete frame: TEXT, fin=true
108
+ dataHandler(buildFrame(0x1, 'complete', true))
109
+
110
+ expect(messages).toEqual(['complete'])
111
+ })
112
+
113
+ it('should discard fragment buffer on close', () => {
114
+ const socket = createMockSocket()
115
+ const client = new WebSocketClient(socket, server, 'frag-5')
116
+ client.resume()
117
+
118
+ const messages = []
119
+ client.on('message', msg => messages.push(msg))
120
+
121
+ const dataHandler = socket.on.mock.calls.find(c => c[0] === 'data')[1]
122
+
123
+ // Start a fragmented message but close before completion
124
+ dataHandler(buildFrame(0x1, 'partial', false))
125
+ client.close()
126
+
127
+ // No message should have been emitted
128
+ expect(messages).toEqual([])
129
+ })
130
+ })
@@ -11,12 +11,15 @@ describe('WebSocketClient Limits', () => {
11
11
  it('should close connection if payload exceeds limit', () => {
12
12
  const socket = {
13
13
  pause: jest.fn(),
14
+ resume: jest.fn(),
14
15
  on: jest.fn(),
15
16
  write: jest.fn(),
16
17
  end: jest.fn(),
17
- removeAllListeners: jest.fn()
18
+ removeAllListeners: jest.fn(),
19
+ writable: true
18
20
  }
19
- new WebSocketClient(socket, server, 'test-id', {maxPayload: 10})
21
+ const client = new WebSocketClient(socket, server, 'test-id', {maxPayload: 10})
22
+ client.resume()
20
23
 
21
24
  const buffer = Buffer.alloc(100)
22
25
  buffer[0] = 0x81
@@ -32,12 +35,15 @@ describe('WebSocketClient Limits', () => {
32
35
  it('should close connection if rate limit exceeded', () => {
33
36
  const socket = {
34
37
  pause: jest.fn(),
38
+ resume: jest.fn(),
35
39
  on: jest.fn(),
36
40
  write: jest.fn(),
37
41
  end: jest.fn(),
38
- removeAllListeners: jest.fn()
42
+ removeAllListeners: jest.fn(),
43
+ writable: true
39
44
  }
40
- new WebSocketClient(socket, server, 'test-id', {rateLimit: {max: 2, window: 1000}})
45
+ const client = new WebSocketClient(socket, server, 'test-id', {rateLimit: {max: 2, window: 1000}})
46
+ client.resume()
41
47
 
42
48
  const buffer = Buffer.alloc(7)
43
49
  buffer[0] = 0x81
@@ -0,0 +1,154 @@
1
+ const {WebSocketServer, WebSocketClient, READY_STATE} = require('../../../src/WebSocket.js')
2
+
3
+ /**
4
+ * Helper: creates a mock TCP socket with the minimum interface
5
+ * required by WebSocketClient.
6
+ */
7
+ function createMockSocket() {
8
+ return {
9
+ pause: jest.fn(),
10
+ resume: jest.fn(),
11
+ on: jest.fn(),
12
+ write: jest.fn(),
13
+ end: jest.fn(),
14
+ removeAllListeners: jest.fn(),
15
+ writable: true
16
+ }
17
+ }
18
+
19
+ describe('WebSocketClient readyState', () => {
20
+ let server
21
+
22
+ beforeEach(() => {
23
+ server = new WebSocketServer()
24
+ })
25
+
26
+ it('should expose static state constants matching READY_STATE enum', () => {
27
+ expect(WebSocketClient.CONNECTING).toBe(0)
28
+ expect(WebSocketClient.OPEN).toBe(1)
29
+ expect(WebSocketClient.CLOSING).toBe(2)
30
+ expect(WebSocketClient.CLOSED).toBe(3)
31
+ })
32
+
33
+ it('should export READY_STATE enum', () => {
34
+ expect(READY_STATE).toEqual({
35
+ CONNECTING: 0,
36
+ OPEN: 1,
37
+ CLOSING: 2,
38
+ CLOSED: 3
39
+ })
40
+ })
41
+
42
+ it('should start in CONNECTING state', () => {
43
+ const socket = createMockSocket()
44
+ const client = new WebSocketClient(socket, server, 'rs-1')
45
+ expect(client.readyState).toBe(READY_STATE.CONNECTING)
46
+ })
47
+
48
+ it('should transition to OPEN on resume()', () => {
49
+ const socket = createMockSocket()
50
+ const client = new WebSocketClient(socket, server, 'rs-2')
51
+
52
+ client.resume()
53
+
54
+ expect(client.readyState).toBe(READY_STATE.OPEN)
55
+ expect(socket.resume).toHaveBeenCalled()
56
+ })
57
+
58
+ it('should transition to CLOSED after close()', () => {
59
+ const socket = createMockSocket()
60
+ const client = new WebSocketClient(socket, server, 'rs-3')
61
+ client.resume()
62
+
63
+ client.close()
64
+
65
+ expect(client.readyState).toBe(READY_STATE.CLOSED)
66
+ expect(socket.end).toHaveBeenCalled()
67
+ })
68
+
69
+ it('should be idempotent — second close() is a no-op', () => {
70
+ const socket = createMockSocket()
71
+ const client = new WebSocketClient(socket, server, 'rs-4')
72
+ client.resume()
73
+
74
+ client.close()
75
+ client.close()
76
+
77
+ expect(socket.end).toHaveBeenCalledTimes(1)
78
+ })
79
+
80
+ it('should not send data when in CONNECTING state', () => {
81
+ const socket = createMockSocket()
82
+ const client = new WebSocketClient(socket, server, 'rs-5')
83
+
84
+ client.send('hello')
85
+
86
+ expect(socket.write).not.toHaveBeenCalled()
87
+ })
88
+
89
+ it('should not send data when in CLOSED state', () => {
90
+ const socket = createMockSocket()
91
+ const client = new WebSocketClient(socket, server, 'rs-6')
92
+ client.resume()
93
+ client.close()
94
+
95
+ client.send('hello')
96
+
97
+ // Only the close frame write should exist, no data frame
98
+ const writes = socket.write.mock.calls
99
+ const lastWrite = writes[writes.length - 1]
100
+ // Close frame starts with 0x88
101
+ expect(lastWrite[0][0]).toBe(0x88)
102
+ })
103
+
104
+ it('should not send ping when not OPEN', () => {
105
+ const socket = createMockSocket()
106
+ const client = new WebSocketClient(socket, server, 'rs-7')
107
+
108
+ client.ping()
109
+
110
+ expect(socket.write).not.toHaveBeenCalled()
111
+ })
112
+
113
+ it('should not write when socket is not writable', () => {
114
+ const socket = createMockSocket()
115
+ socket.writable = false
116
+ const client = new WebSocketClient(socket, server, 'rs-8')
117
+ client.resume()
118
+
119
+ client.send('hello')
120
+
121
+ expect(socket.write).not.toHaveBeenCalled()
122
+ })
123
+
124
+ it('should transition to CLOSED when socket fires close event', () => {
125
+ const socket = createMockSocket()
126
+ const client = new WebSocketClient(socket, server, 'rs-9')
127
+ server.clients.set('rs-9', client)
128
+ client.resume()
129
+
130
+ // Simulate socket 'close' event
131
+ const closeHandler = socket.on.mock.calls.find(c => c[0] === 'close')[1]
132
+ closeHandler()
133
+
134
+ expect(client.readyState).toBe(READY_STATE.CLOSED)
135
+ })
136
+
137
+ it('should emit close event only once on double cleanup', () => {
138
+ const socket = createMockSocket()
139
+ const client = new WebSocketClient(socket, server, 'rs-10')
140
+ server.clients.set('rs-10', client)
141
+ client.resume()
142
+
143
+ const closeSpy = jest.fn()
144
+ client.on('close', closeSpy)
145
+
146
+ client.close()
147
+
148
+ // Simulate socket 'close' event firing after close() already cleaned up
149
+ const closeHandler = socket.on.mock.calls.find(c => c[0] === 'close')
150
+ if (closeHandler) closeHandler[1]()
151
+
152
+ expect(closeSpy).toHaveBeenCalledTimes(1)
153
+ })
154
+ })
@@ -1,55 +0,0 @@
1
- ## 🔐 User Logins with `Auth.js`
2
-
3
- The `Odac.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
- `Odac.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
- > **💡 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
-
16
- #### Checking the Guest List
17
-
18
- * `Odac.Auth.isLogin()`: Is the current user logged in? Returns `true` or `false`.
19
- * `Odac.Auth.getId()`: Gets the ID of the logged-in user.
20
- * `Odac.Auth.get('some-key')`: Grabs a specific piece of info from the `userData` you stored.
21
-
22
- #### Showing a User Out
23
-
24
- * `Odac.Auth.logout()`: Ends the user's session and logs them out.
25
-
26
- #### Example: A Login Flow
27
- ```javascript
28
- // Controller for your login form
29
- module.exports = async function (Odac) {
30
- const { username, password } = Odac.Request.post;
31
-
32
- // IMPORTANT: You need to write your own code to find the user in your database!
33
- const user = await yourDatabase.findUser(username, password);
34
-
35
- if (user) {
36
- // User is valid! Log them in.
37
- Odac.Auth.login(user.id, { username: user.username });
38
- return Odac.direct('/dashboard'); // Send them to their dashboard
39
- } else {
40
- // Bad credentials, send them back to the login page
41
- return Odac.direct('/login?error=1');
42
- }
43
- }
44
-
45
- // A protected dashboard page
46
- module.exports = function (Odac) {
47
- // If they're not logged in, kick them back to the login page.
48
- if (!Odac.Auth.isLogin()) {
49
- return Odac.direct('/login');
50
- }
51
-
52
- const username = Odac.Auth.get('username');
53
- return `Welcome back, ${username}!`;
54
- }
55
- ```