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.
Files changed (143) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/auto-pr-description.yml +3 -1
  6. package/.github/workflows/release.yml +42 -1
  7. package/.github/workflows/test-coverage.yml +6 -5
  8. package/.github/workflows/test-publish.yml +36 -0
  9. package/.husky/pre-commit +10 -0
  10. package/.husky/pre-push +13 -0
  11. package/.releaserc.js +3 -3
  12. package/CHANGELOG.md +184 -0
  13. package/README.md +53 -34
  14. package/bin/odac.js +181 -49
  15. package/client/odac.js +878 -995
  16. package/docs/backend/01-overview/03-development-server.md +39 -46
  17. package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
  18. package/docs/backend/03-config/00-configuration-overview.md +15 -6
  19. package/docs/backend/03-config/01-database-connection.md +3 -3
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  21. package/docs/backend/03-config/03-request-timeout.md +1 -1
  22. package/docs/backend/03-config/04-environment-variables.md +4 -4
  23. package/docs/backend/03-config/05-early-hints.md +2 -2
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  26. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  27. package/docs/backend/04-routing/09-websocket.md +29 -0
  28. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  29. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
  30. package/docs/backend/05-controllers/03-controller-classes.md +61 -55
  31. package/docs/backend/05-forms/01-custom-forms.md +103 -95
  32. package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
  33. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  34. package/docs/backend/07-views/02-rendering-a-view.md +1 -1
  35. package/docs/backend/07-views/03-variables.md +5 -5
  36. package/docs/backend/07-views/04-request-data.md +1 -1
  37. package/docs/backend/07-views/08-backend-javascript.md +1 -1
  38. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  39. package/docs/backend/08-database/01-getting-started.md +100 -0
  40. package/docs/backend/08-database/02-basics.md +136 -0
  41. package/docs/backend/08-database/03-advanced.md +84 -0
  42. package/docs/backend/08-database/04-migrations.md +48 -0
  43. package/docs/backend/09-validation/01-the-validator-service.md +1 -0
  44. package/docs/backend/10-authentication/03-register.md +9 -2
  45. package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
  46. package/docs/backend/10-authentication/05-session-management.md +16 -2
  47. package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
  48. package/docs/backend/10-authentication/07-magic-links.md +134 -0
  49. package/docs/backend/11-mail/01-the-mail-service.md +118 -28
  50. package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
  51. package/docs/backend/13-utilities/01-odac-var.md +7 -7
  52. package/docs/backend/13-utilities/02-ipc.md +73 -0
  53. package/docs/frontend/01-overview/01-introduction.md +5 -1
  54. package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
  55. package/docs/index.json +21 -125
  56. package/eslint.config.mjs +5 -47
  57. package/jest.config.js +1 -1
  58. package/package.json +16 -7
  59. package/src/Auth.js +414 -121
  60. package/src/Config.js +12 -7
  61. package/src/Database.js +188 -0
  62. package/src/Env.js +3 -1
  63. package/src/Ipc.js +337 -0
  64. package/src/Lang.js +9 -2
  65. package/src/Mail.js +408 -37
  66. package/src/Odac.js +105 -40
  67. package/src/Request.js +71 -49
  68. package/src/Route/Cron.js +62 -18
  69. package/src/Route/Internal.js +215 -12
  70. package/src/Route/Middleware.js +7 -2
  71. package/src/Route.js +372 -109
  72. package/src/Server.js +118 -12
  73. package/src/Storage.js +169 -0
  74. package/src/Token.js +6 -4
  75. package/src/Validator.js +95 -3
  76. package/src/Var.js +22 -6
  77. package/src/View/EarlyHints.js +43 -33
  78. package/src/View/Form.js +210 -28
  79. package/src/View.js +108 -7
  80. package/src/WebSocket.js +18 -3
  81. package/template/odac.json +5 -0
  82. package/template/package.json +3 -1
  83. package/template/route/www.js +12 -10
  84. package/template/view/content/home.html +3 -3
  85. package/template/view/head/main.html +2 -2
  86. package/test/Client.test.js +168 -0
  87. package/test/Config.test.js +112 -0
  88. package/test/Lang.test.js +92 -0
  89. package/test/Odac.test.js +86 -0
  90. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  91. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  92. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  93. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  94. package/test/scripts/check-coverage.js +4 -4
  95. package/docs/backend/08-database/01-database-connection.md +0 -99
  96. package/docs/backend/08-database/02-using-mysql.md +0 -322
  97. package/src/Mysql.js +0 -575
  98. package/template/config.json +0 -5
  99. package/test/cli/Cli.test.js +0 -36
  100. package/test/core/Candy.test.js +0 -234
  101. package/test/core/Commands.test.js +0 -538
  102. package/test/core/Config.test.js +0 -1432
  103. package/test/core/Lang.test.js +0 -250
  104. package/test/core/Process.test.js +0 -156
  105. package/test/server/Api.test.js +0 -647
  106. package/test/server/DNS.test.js +0 -2050
  107. package/test/server/DNS.test.js.bak +0 -2084
  108. package/test/server/Hub.test.js +0 -497
  109. package/test/server/Log.test.js +0 -73
  110. package/test/server/Mail.account.test_.js +0 -460
  111. package/test/server/Mail.init.test_.js +0 -411
  112. package/test/server/Mail.test_.js +0 -1340
  113. package/test/server/SSL.test_.js +0 -1491
  114. package/test/server/Server.test.js +0 -765
  115. package/test/server/Service.test_.js +0 -1127
  116. package/test/server/Subdomain.test.js +0 -440
  117. package/test/server/Web/Firewall.test.js +0 -175
  118. package/test/server/Web/Proxy.test.js +0 -397
  119. package/test/server/Web.test.js +0 -1494
  120. package/test/server/__mocks__/acme-client.js +0 -17
  121. package/test/server/__mocks__/bcrypt.js +0 -50
  122. package/test/server/__mocks__/child_process.js +0 -389
  123. package/test/server/__mocks__/crypto.js +0 -432
  124. package/test/server/__mocks__/fs.js +0 -450
  125. package/test/server/__mocks__/globalOdac.js +0 -227
  126. package/test/server/__mocks__/http.js +0 -575
  127. package/test/server/__mocks__/https.js +0 -272
  128. package/test/server/__mocks__/index.js +0 -249
  129. package/test/server/__mocks__/mail/server.js +0 -100
  130. package/test/server/__mocks__/mail/smtp.js +0 -31
  131. package/test/server/__mocks__/mailparser.js +0 -81
  132. package/test/server/__mocks__/net.js +0 -369
  133. package/test/server/__mocks__/node-forge.js +0 -328
  134. package/test/server/__mocks__/os.js +0 -320
  135. package/test/server/__mocks__/path.js +0 -291
  136. package/test/server/__mocks__/selfsigned.js +0 -8
  137. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  138. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  139. package/test/server/__mocks__/smtp-server.js +0 -106
  140. package/test/server/__mocks__/sqlite3.js +0 -394
  141. package/test/server/__mocks__/testFactories.js +0 -299
  142. package/test/server/__mocks__/testHelpers.js +0 -363
  143. package/test/server/__mocks__/tls.js +0 -229
package/src/Mail.js CHANGED
@@ -1,5 +1,9 @@
1
- const axios = require('axios')
1
+ const net = require('net')
2
+ const nodeCrypto = require('crypto')
2
3
  const fs = require('fs')
4
+ // const Form = require('./View/Form')
5
+
6
+ const CACHE_DIR = './storage/.cache'
3
7
 
4
8
  class Mail {
5
9
  #header = {}
@@ -7,11 +11,273 @@ class Mail {
7
11
  #subject = ''
8
12
  #template
9
13
  #to
14
+ #htmlContent
15
+ #textContent
10
16
 
11
17
  constructor(template) {
12
18
  this.#template = template
13
19
  }
14
20
 
21
+ #functions = {
22
+ '{!!': {
23
+ function: '${await ',
24
+ close: '!!}',
25
+ end: '}'
26
+ },
27
+ '{{--': {
28
+ function: '`; /*',
29
+ close: '--}}',
30
+ end: '*/ html += `'
31
+ },
32
+ '{{': {
33
+ function: '${Odac.Var(await ',
34
+ close: '}}',
35
+ end: ').html().replace(/\\n/g, "<br>")}'
36
+ },
37
+ break: {
38
+ function: 'break;',
39
+ arguments: {}
40
+ },
41
+ component: {},
42
+ continue: {
43
+ function: 'continue;',
44
+ arguments: {}
45
+ },
46
+ mysql: {},
47
+ elseif: {
48
+ function: '} else if(await ($condition)){',
49
+ arguments: {
50
+ condition: true
51
+ }
52
+ },
53
+ else: {
54
+ function: '} else {'
55
+ },
56
+ fetch: {},
57
+ for: {
58
+ function: '{ let _arr = $constructor; for(let $key in _arr){ let $value = _arr[$key];',
59
+ end: '}}',
60
+ arguments: {
61
+ var: null,
62
+ get: null,
63
+ key: 'key',
64
+ value: 'value'
65
+ }
66
+ },
67
+ if: {
68
+ function: 'if(await ($condition)){',
69
+ arguments: {
70
+ condition: true
71
+ }
72
+ },
73
+ '<odac:js>': {
74
+ end: ' html += `',
75
+ function: '`; ',
76
+ close: '</odac:js>'
77
+ },
78
+ lazy: {},
79
+ list: {
80
+ arguments: {
81
+ var: null,
82
+ get: null,
83
+ key: 'key',
84
+ value: 'value'
85
+ },
86
+ end: '}}',
87
+ function: '{ let _arr = $constructor; for(let $key in _arr){ let $value = _arr[$key];',
88
+ replace: 'ul'
89
+ },
90
+ while: {
91
+ function: 'while(await ($condition)){',
92
+ arguments: {
93
+ condition: true
94
+ }
95
+ }
96
+ }
97
+
98
+ #parseOdacTag(content) {
99
+ // Parse backend comments
100
+ content = content.replace(/<!--odac([\s\S]*?)(?:odac-->|-->)/g, () => '')
101
+
102
+ // Parse <script:odac> tags
103
+ content = content.replace(/<script:odac([^>]*)>([\s\S]*?)<\/script:odac>/g, (fullMatch, attributes, jsContent) => {
104
+ return `<odac:js>${jsContent}</odac:js>`
105
+ })
106
+
107
+ content = content.replace(/<odac:else\s*\/>/g, '<odac:else>')
108
+ content = content.replace(/<odac:elseif\s+([^>]*?)\/>/g, '<odac:elseif $1>')
109
+
110
+ content = content.replace(/<odac([^>]*?)\/>/g, (fullMatch, attributes) => {
111
+ attributes = attributes.trim()
112
+ const attrs = {}
113
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
114
+ let match
115
+ while ((match = attrRegex.exec(attributes))) {
116
+ const key = match[1]
117
+ const value = match[3] !== undefined ? match[3] : match[4] !== undefined ? match[4] : true
118
+ attrs[key] = value
119
+ }
120
+
121
+ if (attrs.get) return `{{ get('${attrs.get}') || '' }}`
122
+ else if (attrs.var) return attrs.raw ? `{!! ${attrs.var} !!}` : `{{ ${attrs.var} }}`
123
+
124
+ return fullMatch
125
+ })
126
+
127
+ let depth = 0
128
+ let maxDepth = 10
129
+ while (depth < maxDepth && content.includes('<odac')) {
130
+ const before = content
131
+ content = content.replace(/<odac([^>]*)>((?:(?!<odac)[\s\S])*?)<\/odac>/g, (fullMatch, attributes, innerContent) => {
132
+ attributes = attributes.trim()
133
+ innerContent = innerContent.trim()
134
+
135
+ const attrs = {}
136
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
137
+ let match
138
+ while ((match = attrRegex.exec(attributes))) {
139
+ const key = match[1]
140
+ const value = match[3] !== undefined ? match[3] : match[4] !== undefined ? match[4] : true
141
+ attrs[key] = value
142
+ }
143
+
144
+ if (attrs.get) return `{{ get('${attrs.get}') || '' }}`
145
+ else if (attrs.var) return attrs.raw ? `{!! ${attrs.var} !!}` : `{{ ${attrs.var} }}`
146
+ else if (attrs.t || attrs.translate)
147
+ return `{{ '${innerContent}' }}` // Simple fallback for mail
148
+ else return `{{ '${innerContent}' }}`
149
+ })
150
+ if (before === content) break
151
+ depth++
152
+ }
153
+
154
+ return content
155
+ }
156
+
157
+ async #render(file, data) {
158
+ const fd = fs.openSync(file, 'r')
159
+ let mtime, content
160
+ try {
161
+ mtime = fs.fstatSync(fd).mtimeMs
162
+ content = fs.readFileSync(fd, 'utf8')
163
+ } finally {
164
+ fs.closeSync(fd)
165
+ }
166
+
167
+ // Since mail doesn't have a persistent Odac instance access like View cache, we manage a simple cache or just re-compile.
168
+ // For performance in emails (usually background), re-compiling is okay, but caching is better.
169
+ // Let's use global Odac.View.cache if available or local.
170
+ if (!Odac.View) Odac.View = {}
171
+ if (!Odac.View.cache) Odac.View.cache = {}
172
+
173
+ if (Odac.View.cache[file]?.mtime !== mtime) {
174
+ // No Form options needed normally for simplified email templates, but keeping Form.parse for consistency if needed
175
+ // content = Form.parse(content, {Request: {}, ...Odac}) // Partially mock if Form needs it, but Form usually needs full Request.
176
+ // Skipping Form.parse for mail for now unless requested, as it relies on Session/Request heavily.
177
+ // User asked for "View file rendering", usually meaning logic tags.
178
+
179
+ const jsBlocks = []
180
+ content = content.replace(/<script:odac([^>]*)>([\s\S]*?)<\/script:odac>/g, (match, attrs, jsContent) => {
181
+ const placeholder = `___ODAC_JS_BLOCK_${jsBlocks.length}___`
182
+ jsBlocks.push(jsContent)
183
+ return `<script:odac${attrs}>${placeholder}</script:odac>`
184
+ })
185
+
186
+ content = this.#parseOdacTag(content)
187
+ content = content.replace(/`/g, '\\\\`').replace(/\$\{/g, '\\\\${')
188
+
189
+ jsBlocks.forEach((jsContent, index) => {
190
+ content = content.replace(`___ODAC_JS_BLOCK_${index}___`, jsContent)
191
+ })
192
+
193
+ let result = 'html += `\n' + content + '\n`'
194
+ content = content.split('\n')
195
+
196
+ for (let key in this.#functions) {
197
+ let att = ''
198
+ let func = this.#functions[key]
199
+ let matches = func.close
200
+ ? result.match(new RegExp(`${key}[\\s\\S]*?${func.close}`, 'g'))
201
+ : result.match(new RegExp(`<odac:${key}(?:\\s+[^>]*?(?:"[^"]*"|'[^']*'|[^"'>])*)?>`, 'g'))
202
+ if (!matches) continue
203
+ for (let match of matches) {
204
+ let matchForParsing = match
205
+ if (!func.close) matchForParsing = matchForParsing.replace(/^<odac:/, '').replace(/>$/, '')
206
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
207
+ let attrMatch
208
+ const args = []
209
+ while ((attrMatch = attrRegex.exec(matchForParsing))) {
210
+ args.push(attrMatch[0])
211
+ }
212
+ let vars = {}
213
+ if (func.arguments)
214
+ for (let arg of args) {
215
+ const argRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/
216
+ const argMatch = argRegex.exec(arg)
217
+ if (!argMatch) continue
218
+ const argKey = argMatch[1]
219
+ const value = argMatch[3] !== undefined ? argMatch[3] : argMatch[4] !== undefined ? argMatch[4] : true
220
+ if (func.arguments[argKey] === undefined) {
221
+ att += `${argKey}="${value}"`
222
+ continue
223
+ }
224
+ vars[argKey] = value
225
+ }
226
+ if (!func.function) continue
227
+ let fun = func.function
228
+
229
+ // Simplified logic for loop
230
+ if (key === 'for' || key === 'list') {
231
+ let constructor
232
+ if (vars.var) {
233
+ constructor = `await ${vars.var}`
234
+ delete vars.var
235
+ }
236
+ fun = fun.replace(/\$constructor/g, constructor)
237
+ }
238
+
239
+ for (let argKey in func.arguments) {
240
+ if (argKey === 'var' || argKey === 'get') continue
241
+ if (vars[argKey] === undefined) vars[argKey] = func.arguments[argKey]
242
+ fun = fun.replace(new RegExp(`\\$${argKey}`, 'g'), vars[argKey])
243
+ }
244
+ if (func.close) {
245
+ result = result.replace(match, fun + match.substring(key.length, match.length - func.close.length) + func.end)
246
+ } else {
247
+ result = result.replace(match, (func.replace ? `<${[func.replace, att].join(' ')}>` : '') + '`; ' + fun + ' html += `')
248
+ result = result.replace(`</odac:${key}>`, '`; ' + (func.end ?? '}') + ' html += `' + (func.replace ? `</${func.replace}>` : ''))
249
+ }
250
+ }
251
+ }
252
+
253
+ let cache = `${nodeCrypto.createHash('md5').update(file).digest('hex')}`
254
+ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, {recursive: true})
255
+ fs.writeFileSync(
256
+ `${CACHE_DIR}/${cache}`,
257
+ `module.exports = async (Odac, data, get, __) => {\n
258
+ // Destructure data keys into local scope variables
259
+ ${Object.keys(data)
260
+ .map(k => `let ${k} = data['${k}'];`)
261
+ .join('\n')}
262
+ let html = '';\n${result}\nreturn html.trim()\n}`
263
+ )
264
+ delete require.cache[require.resolve(`${__dir}/${CACHE_DIR}/${cache}`)]
265
+ Odac.View.cache[file] = {mtime: mtime, cache: cache}
266
+ }
267
+
268
+ try {
269
+ return await require(`${__dir}/${CACHE_DIR}/${Odac.View.cache[file].cache}`)(
270
+ Odac,
271
+ data,
272
+ key => data[key],
273
+ (...args) => (Odac.Lang ? Odac.Lang.get(...args) : args[0])
274
+ )
275
+ } catch (e) {
276
+ console.error(e)
277
+ return ''
278
+ }
279
+ }
280
+
15
281
  header(header) {
16
282
  this.#header = header
17
283
  return this
@@ -27,34 +293,108 @@ class Mail {
27
293
  return this
28
294
  }
29
295
 
30
- to(email) {
31
- email = {value: [{address: email}]}
32
- this.#to = email
296
+ to(email, name = '') {
297
+ this.#to = {value: [{address: email, name: name}]}
298
+ return this
299
+ }
300
+
301
+ html(content) {
302
+ this.#htmlContent = content
33
303
  return this
34
304
  }
35
305
 
36
- send(data) {
306
+ text(content) {
307
+ this.#textContent = content
308
+ return this
309
+ }
310
+
311
+ #encode(text) {
312
+ if (!text) return ''
313
+ // eslint-disable-next-line
314
+ if (/^[\x00-\x7F]*$/.test(text)) return text
315
+ return '=?UTF-8?B?' + Buffer.from(text).toString('base64') + '?='
316
+ }
317
+
318
+ #stripHtml(html) {
319
+ if (!html) return ''
320
+
321
+ let text = html
322
+ // Recursively remove script and style tags to handle nested injections
323
+ // Single-pass removal for plain text generation.
324
+ // Recursive removal (do-while) is dangerous (ReDoS) and unnecessary for text/plain output.
325
+ text = text.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gim, '')
326
+ text = text.replace(/<[^>]+>/g, '')
327
+
328
+ return text.replace(/\s+/g, ' ').trim()
329
+ }
330
+
331
+ send(data = {}) {
37
332
  return new Promise(resolve => {
38
- if (!fs.existsSync(__dir + '/view/mail/' + this.#template + '.html')) return console.log('Template not found') && false
39
- if (!this.#from || !this.#subject || !this.#to) return console.log('From, Subject and To fields are required') && false
40
- if (!Odac.Var(this.#from.email).is('email')) return console.log('From field is not a valid e-mail address') && false
41
- if (!Odac.Var(this.#to.value[0].address).is('email')) return console.log('To field is not a valid e-mail address') && false
42
- if (!this.#header['From']) this.#header['From'] = `${this.#from.name} <${this.#from.email}>`
43
- if (!this.#header['To']) this.#header['To'] = this.#to
44
- if (!this.#header['Subject']) this.#header['Subject'] = this.#subject
45
- if (!this.#header['Message-ID']) this.#header['Message-ID'] = `<${crypto.randomBytes(16).toString('hex')}-${Date.now()}@odac>`
46
- if (!this.#header['Content-Transfer-Encoding']) this.#header['Content-Transfer-Encoding'] = 'quoted-printable'
47
- if (!this.#header['Date']) this.#header['Date'] = new Date().toUTCString()
48
- if (!this.#header['Content-Type'])
49
- this.#header['Content-Type'] = 'multipart/alternative; boundary="----=' + crypto.randomBytes(32).toString('hex') + '"'
50
- if (!this.#header['X-Mailer']) this.#header['X-Mailer'] = 'Odac'
51
- if (!this.#header['MIME-Version']) this.#header['MIME-Version'] = '1.0'
52
- let content = fs.readFileSync(__dir + '/view/mail/' + this.#template + '.html').toString()
53
- for (const iterator of Object.keys(data)) content = content.replace(new RegExp(`{${iterator}}`, 'g'), data[iterator])
54
- axios
55
- .post(
56
- 'http://127.0.0.1:1453',
57
- {
333
+ ;(async () => {
334
+ try {
335
+ if (!this.#from || !this.#subject || !this.#to) {
336
+ console.error('[Mail] Missing required fields: From, Subject, or To')
337
+ return resolve(false)
338
+ }
339
+
340
+ if (!Odac.Var(this.#from.email).is('email')) {
341
+ console.error('[Mail] From field is not a valid e-mail address')
342
+ return resolve(false)
343
+ }
344
+
345
+ if (!Odac.Var(this.#to.value[0].address).is('email')) {
346
+ console.error('[Mail] To field is not a valid e-mail address')
347
+ return resolve(false)
348
+ }
349
+
350
+ let htmlContent = ''
351
+ let textContent = ''
352
+
353
+ if (this.#template) {
354
+ if (!fs.existsSync(__dir + '/view/mail/' + this.#template + '.html')) {
355
+ console.error(`[Mail] Template not found: ${__dir}/view/mail/${this.#template}.html`)
356
+ return resolve(false)
357
+ }
358
+ htmlContent = await this.#render(__dir + '/view/mail/' + this.#template + '.html', data)
359
+ textContent = this.#stripHtml(htmlContent)
360
+ } else {
361
+ if (this.#htmlContent) htmlContent = this.#htmlContent
362
+ if (this.#textContent) textContent = this.#textContent
363
+
364
+ if (!htmlContent && !textContent) {
365
+ console.error('[Mail] No content provided (Template, HTML, or Text)')
366
+ return resolve(false)
367
+ }
368
+
369
+ // If only HTML is provided, auto-generate text
370
+ if (htmlContent && !textContent) {
371
+ textContent = this.#stripHtml(htmlContent)
372
+ }
373
+ }
374
+
375
+ if (!this.#header['From']) this.#header['From'] = `${this.#encode(this.#from.name)} <${this.#from.email}>`
376
+ if (!this.#header['To']) {
377
+ const t = this.#to.value[0]
378
+ this.#header['To'] = t.name ? `${this.#encode(t.name)} <${t.address}>` : t.address
379
+ }
380
+ if (!this.#header['Subject']) this.#header['Subject'] = this.#encode(this.#subject)
381
+ if (!this.#header['Message-ID']) this.#header['Message-ID'] = `<${nodeCrypto.randomBytes(16).toString('hex')}-${Date.now()}@odac>`
382
+
383
+ if (!this.#header['Date']) this.#header['Date'] = new Date().toUTCString()
384
+ if (!this.#header['Content-Type']) {
385
+ if (htmlContent) {
386
+ this.#header['Content-Type'] =
387
+ 'multipart/alternative; charset=UTF-8; boundary="----=' + nodeCrypto.randomBytes(32).toString('hex') + '"'
388
+ } else {
389
+ this.#header['Content-Type'] = 'text/plain; charset=UTF-8'
390
+ }
391
+ }
392
+ if (!this.#header['X-Mailer']) this.#header['X-Mailer'] = 'ODAC'
393
+ if (!this.#header['MIME-Version']) this.#header['MIME-Version'] = '1.0'
394
+
395
+ const client = new net.Socket()
396
+ const payload = {
397
+ auth: process.env.ODAC_API_KEY,
58
398
  action: 'mail.send',
59
399
  data: [
60
400
  {
@@ -62,22 +402,53 @@ class Mail {
62
402
  from: {value: [{address: this.#from.email, name: this.#from.name}]},
63
403
  to: this.#to,
64
404
  header: this.#header,
65
- html: content,
66
- text: content.replace(/<[^>]*>?/gm, '')
405
+ html: htmlContent,
406
+ text: textContent,
407
+ attachments: []
67
408
  }
68
409
  ]
69
- },
70
- {headers: {Authorization: Odac.Config.system.api.auth}}
71
- )
72
- .then(response => {
73
- resolve(response.data)
74
- })
75
- .catch(error => {
76
- console.log(error)
410
+ }
411
+
412
+ const socketPath = process.env.ODAC_API_SOCKET || '/var/run/odac.sock'
413
+
414
+ if (Odac.Config.debug) console.log(`[Mail] Connecting to Odac Core via Unix Socket: ${socketPath}...`)
415
+
416
+ client.connect(socketPath, () => {
417
+ if (Odac.Config.debug) console.log('[Mail] Connected to Odac Core. Sending payload...')
418
+ client.write(JSON.stringify(payload))
419
+ })
420
+
421
+ client.on('data', data => {
422
+ if (Odac.Config.debug) console.log('[Mail] Received data from server:', data.toString())
423
+ try {
424
+ const response = JSON.parse(data.toString())
425
+ resolve(response)
426
+ } catch (error) {
427
+ console.error('[Mail] Error parsing response:', error)
428
+ resolve(false)
429
+ }
430
+ client.destroy()
431
+ })
432
+
433
+ client.on('error', error => {
434
+ console.error('[Mail] Socket Error:', error)
435
+ resolve(false)
436
+ })
437
+
438
+ client.on('close', () => {
439
+ if (Odac.Config.debug) console.log('[Mail] Connection closed')
440
+ })
441
+ } catch (error) {
442
+ console.error('[Mail] Unexpected error:', error)
77
443
  resolve(false)
78
- })
444
+ }
445
+ })()
79
446
  })
80
447
  }
81
448
  }
82
449
 
83
- module.exports = Mail
450
+ module.exports = new Proxy(Mail, {
451
+ apply(target, thisArg, args) {
452
+ return new target(...args)
453
+ }
454
+ })
package/src/Odac.js CHANGED
@@ -1,9 +1,16 @@
1
1
  module.exports = {
2
2
  init: async function () {
3
3
  global.Odac = this.instance()
4
+ global.Odac.Storage = require('./Storage.js')
5
+ global.Odac.Storage.init()
6
+
4
7
  await global.Odac.Env.init()
5
8
  await global.Odac.Config.init()
6
- await global.Odac.Mysql.init()
9
+ await global.Odac.Database.init()
10
+
11
+ global.Odac.Ipc = require('./Ipc.js')
12
+ await global.Odac.Ipc.init()
13
+
7
14
  await global.Odac.Route.init()
8
15
  await global.Odac.Server.init()
9
16
  global.Odac.instance = this.instance
@@ -18,15 +25,50 @@ module.exports = {
18
25
  _odac.Config = require('./Config.js')
19
26
  _odac.Env = require('./Env.js')
20
27
  _odac.Mail = (...args) => new (require('./Mail.js'))(...args)
21
- _odac.Mysql = require('./Mysql.js')
28
+ _odac.Database = require('./Database.js')
29
+ _odac.DB = _odac.Database
22
30
  _odac.Route = global.Odac?.Route ?? new (require('./Route.js'))()
31
+
32
+ _odac._ipcSubs = []
33
+ const ipcSingleton = require('./Ipc.js')
34
+
35
+ _odac.Ipc = new Proxy(ipcSingleton, {
36
+ get(target, prop) {
37
+ if (prop === 'subscribe') {
38
+ return async (channel, callback) => {
39
+ const res = await target.subscribe(channel, callback)
40
+ _odac._ipcSubs.push({channel, callback})
41
+ return res
42
+ }
43
+ }
44
+ if (prop === 'unsubscribe') {
45
+ return async (channel, callback) => {
46
+ const res = await target.unsubscribe(channel, callback)
47
+ const index = _odac._ipcSubs.findIndex(s => s.channel === channel && s.callback === callback)
48
+ if (index > -1) _odac._ipcSubs.splice(index, 1)
49
+ return res
50
+ }
51
+ }
52
+ const value = target[prop]
53
+ if (typeof value === 'function') return value.bind(target)
54
+ return value
55
+ },
56
+ set(target, prop, value) {
57
+ target[prop] = value
58
+ return true
59
+ }
60
+ })
61
+
23
62
  _odac.Server = require('./Server.js')
63
+ _odac.Storage = require('./Storage.js')
24
64
  _odac.Var = (...args) => new (require('./Var.js'))(...args)
25
65
 
26
66
  if (req) {
27
- _odac.Request = new (require('./Request.js'))(id, req, res, _odac)
28
- _odac.Auth = new (require('./Auth.js'))(_odac.Request)
29
- _odac.Token = new (require('./Token.js'))(_odac.Request)
67
+ if (typeof req === 'object') {
68
+ _odac.Request = new (require('./Request.js'))(id, req, res, _odac)
69
+ _odac.Auth = new (require('./Auth.js'))(_odac.Request)
70
+ _odac.Token = new (require('./Token.js'))(_odac.Request)
71
+ }
30
72
  _odac.Lang = new (require('./Lang.js'))(_odac)
31
73
  if (res) {
32
74
  _odac.View = new (require('./View.js'))(_odac)
@@ -57,53 +99,76 @@ module.exports = {
57
99
  _odac.cleanup = function () {
58
100
  for (const id of _odac._intervals) clearInterval(id)
59
101
  for (const id of _odac._timeouts) clearTimeout(id)
102
+ if (_odac._ipcSubs) {
103
+ for (const sub of _odac._ipcSubs) {
104
+ ipcSingleton.unsubscribe(sub.channel, sub.callback).catch(console.error)
105
+ }
106
+ }
60
107
  _odac._intervals = []
61
108
  _odac._timeouts = []
62
- }
63
-
64
- if (global.Odac?.Route?.class) {
65
- for (const name in global.Odac.Route.class) {
66
- const Module = global.Odac.Route.class[name].module
67
- _odac[name] = typeof Module === 'function' ? new Module(_odac) : Module
68
- }
109
+ _odac._ipcSubs = []
69
110
  }
70
111
 
71
112
  _odac.__ = function (...args) {
72
113
  return _odac.Lang.get(...args)
73
114
  }
74
- _odac.abort = function (code) {
75
- return _odac.Request.abort(code)
76
- }
77
- _odac.cookie = function (key, value, options) {
78
- return _odac.Request.cookie(key, value, options)
79
- }
80
- _odac.direct = function (url) {
81
- return _odac.Request.redirect(url)
115
+ if (typeof req === 'object') {
116
+ _odac.abort = function (code) {
117
+ return _odac.Request.abort(code)
118
+ }
119
+ _odac.cookie = function (key, value, options) {
120
+ return _odac.Request.cookie(key, value, options)
121
+ }
122
+ _odac.direct = function (url) {
123
+ return _odac.Request.redirect(url)
124
+ }
82
125
  }
83
126
  _odac.env = function (key, defaultValue) {
84
127
  return _odac.Env.get(key, defaultValue)
85
128
  }
86
- _odac.return = function (data) {
87
- return _odac.Request.end(data)
88
- }
89
- _odac.request = function (key) {
90
- return _odac.Request.request(key)
91
- }
92
- _odac.set = function (key, value) {
93
- return _odac.Request.set(key, value)
94
- }
95
- _odac.token = function (hash) {
96
- return hash ? _odac.Token.check(hash) : _odac.Token.generate()
97
- }
98
- _odac.validator = function () {
99
- return new (require('./Validator.js'))(_odac.Request)
100
- }
101
- _odac.write = function (value) {
102
- return _odac.Request.write(value)
129
+ if (typeof req === 'object') {
130
+ _odac.return = function (data) {
131
+ return _odac.Request.end(data)
132
+ }
133
+ _odac.request = function (key) {
134
+ return _odac.Request.request(key)
135
+ }
136
+ _odac.set = function (key, value) {
137
+ return _odac.Request.set(key, value)
138
+ }
139
+ _odac.session = function (key, value) {
140
+ return _odac.Request.session(key, value)
141
+ }
142
+ _odac.share = function (key, value) {
143
+ return _odac.Request.share(key, value)
144
+ }
145
+ _odac.token = function (hash) {
146
+ return hash ? _odac.Token.check(hash) : _odac.Token.generate()
147
+ }
148
+ _odac.validator = function () {
149
+ return new (require('./Validator.js'))(_odac.Request)
150
+ }
151
+ _odac.write = function (value) {
152
+ return _odac.Request.write(value)
153
+ }
154
+ _odac.stream = function (input) {
155
+ _odac.Request.clearTimeout()
156
+ return new (require('./Stream'))(_odac.Request.req, _odac.Request.res, input, _odac)
157
+ }
103
158
  }
104
- _odac.stream = function (input) {
105
- _odac.Request.clearTimeout()
106
- return new (require('./Stream'))(_odac.Request.req, _odac.Request.res, input, _odac)
159
+
160
+ if (global.Odac?.Route?.class) {
161
+ _odac.App = {}
162
+ for (const name in global.Odac.Route.class) {
163
+ const Module = global.Odac.Route.class[name].module
164
+ const instance = typeof Module === 'function' ? new Module(_odac) : Module
165
+
166
+ if (_odac[name]) {
167
+ _odac.App[name] = instance
168
+ } else {
169
+ _odac[name] = instance
170
+ }
171
+ }
107
172
  }
108
173
  }
109
174