@nitronjs/framework 0.1.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 (87) hide show
  1. package/README.md +429 -0
  2. package/cli/create.js +260 -0
  3. package/cli/njs.js +164 -0
  4. package/lib/Auth/Manager.js +111 -0
  5. package/lib/Build/Manager.js +1232 -0
  6. package/lib/Console/Commands/BuildCommand.js +25 -0
  7. package/lib/Console/Commands/DevCommand.js +385 -0
  8. package/lib/Console/Commands/MakeCommand.js +110 -0
  9. package/lib/Console/Commands/MigrateCommand.js +98 -0
  10. package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
  11. package/lib/Console/Commands/SeedCommand.js +92 -0
  12. package/lib/Console/Commands/StorageLinkCommand.js +31 -0
  13. package/lib/Console/Stubs/controller.js +19 -0
  14. package/lib/Console/Stubs/middleware.js +9 -0
  15. package/lib/Console/Stubs/migration.js +23 -0
  16. package/lib/Console/Stubs/model.js +7 -0
  17. package/lib/Console/Stubs/page-hydration.tsx +54 -0
  18. package/lib/Console/Stubs/seeder.js +9 -0
  19. package/lib/Console/Stubs/vendor.tsx +11 -0
  20. package/lib/Core/Config.js +86 -0
  21. package/lib/Core/Environment.js +21 -0
  22. package/lib/Core/Paths.js +188 -0
  23. package/lib/Database/Connection.js +61 -0
  24. package/lib/Database/DB.js +84 -0
  25. package/lib/Database/Drivers/MySQLDriver.js +234 -0
  26. package/lib/Database/Manager.js +162 -0
  27. package/lib/Database/Model.js +161 -0
  28. package/lib/Database/QueryBuilder.js +714 -0
  29. package/lib/Database/QueryValidation.js +62 -0
  30. package/lib/Database/Schema/Blueprint.js +126 -0
  31. package/lib/Database/Schema/Manager.js +116 -0
  32. package/lib/Date/DateTime.js +108 -0
  33. package/lib/Date/Locale.js +68 -0
  34. package/lib/Encryption/Manager.js +47 -0
  35. package/lib/Filesystem/Manager.js +49 -0
  36. package/lib/Hashing/Manager.js +25 -0
  37. package/lib/Http/Server.js +317 -0
  38. package/lib/Logging/Manager.js +153 -0
  39. package/lib/Mail/Manager.js +120 -0
  40. package/lib/Route/Loader.js +81 -0
  41. package/lib/Route/Manager.js +265 -0
  42. package/lib/Runtime/Entry.js +11 -0
  43. package/lib/Session/File.js +299 -0
  44. package/lib/Session/Manager.js +259 -0
  45. package/lib/Session/Memory.js +67 -0
  46. package/lib/Session/Session.js +196 -0
  47. package/lib/Support/Str.js +100 -0
  48. package/lib/Translation/Manager.js +49 -0
  49. package/lib/Validation/MimeTypes.js +39 -0
  50. package/lib/Validation/Validator.js +691 -0
  51. package/lib/View/Manager.js +544 -0
  52. package/lib/View/Templates/default/Home.tsx +262 -0
  53. package/lib/View/Templates/default/MainLayout.tsx +44 -0
  54. package/lib/View/Templates/errors/404.tsx +13 -0
  55. package/lib/View/Templates/errors/500.tsx +13 -0
  56. package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
  57. package/lib/View/Templates/messages/Maintenance.tsx +17 -0
  58. package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
  59. package/lib/index.js +57 -0
  60. package/package.json +47 -0
  61. package/skeleton/.env.example +26 -0
  62. package/skeleton/app/Controllers/HomeController.js +9 -0
  63. package/skeleton/app/Kernel.js +11 -0
  64. package/skeleton/app/Middlewares/Authentication.js +9 -0
  65. package/skeleton/app/Middlewares/Guest.js +9 -0
  66. package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
  67. package/skeleton/app/Models/User.js +7 -0
  68. package/skeleton/config/app.js +4 -0
  69. package/skeleton/config/auth.js +16 -0
  70. package/skeleton/config/database.js +27 -0
  71. package/skeleton/config/hash.js +3 -0
  72. package/skeleton/config/server.js +28 -0
  73. package/skeleton/config/session.js +21 -0
  74. package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
  75. package/skeleton/database/seeders/UserSeeder.js +15 -0
  76. package/skeleton/globals.d.ts +1 -0
  77. package/skeleton/package.json +24 -0
  78. package/skeleton/public/.gitkeep +0 -0
  79. package/skeleton/resources/css/.gitkeep +0 -0
  80. package/skeleton/resources/langs/.gitkeep +0 -0
  81. package/skeleton/resources/views/Site/Home.tsx +66 -0
  82. package/skeleton/routes/web.js +4 -0
  83. package/skeleton/storage/app/private/.gitkeep +0 -0
  84. package/skeleton/storage/app/public/.gitkeep +0 -0
  85. package/skeleton/storage/framework/sessions/.gitkeep +0 -0
  86. package/skeleton/storage/logs/.gitkeep +0 -0
  87. package/skeleton/tsconfig.json +33 -0
@@ -0,0 +1,317 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import dotenv from "dotenv";
3
+ import path from "node:path";
4
+ import fastify from "fastify";
5
+ import fastifyCors from "@fastify/cors";
6
+ import fastifyStatic from "@fastify/static";
7
+ import fastifyCookie from "@fastify/cookie";
8
+ import fastifyHelmet from "@fastify/helmet";
9
+ import fastifyMultipart from "@fastify/multipart";
10
+ import Paths from "../Core/Paths.js";
11
+ import Config from "../Core/Config.js";
12
+ import Route from "../Route/Manager.js";
13
+ import View from "../View/Manager.js";
14
+ import Auth from "../Auth/Manager.js";
15
+ import SessionManager from "../Session/Manager.js";
16
+ import DB from "../Database/DB.js";
17
+ import Log from "../Logging/Manager.js";
18
+ import Loader from "../Route/Loader.js";
19
+
20
+ const IS_DEV = process.env.APP_DEV === "true";
21
+
22
+ class Server {
23
+ static #server;
24
+ static #serverConfigs;
25
+
26
+ static async start() {
27
+ dotenv.config({ quiet: true });
28
+
29
+ // Initialize config system first (loads all configs once)
30
+ await Config.initialize();
31
+
32
+ // Load project configs dynamically
33
+ this.#serverConfigs = Config.all("server");
34
+
35
+ await Loader.initialize();
36
+
37
+ // Load project routes dynamically
38
+ await import(Paths.routeUrl("web"));
39
+
40
+ await this.#createServer();
41
+ await DB.setup();
42
+ await SessionManager.setup(this.#server);
43
+ Auth.setup(this.#server);
44
+ await Route.setup(this.#server);
45
+ View.setup(this.#server);
46
+ await this.#listen();
47
+ }
48
+
49
+ static async #createServer() {
50
+ this.#server = fastify({
51
+ logger: false,
52
+ bodyLimit: this.#serverConfigs.web_server.bodyLimit,
53
+ trustProxy: this.#serverConfigs.web_server.trustProxy,
54
+ requestIdHeader: "x-request-id",
55
+ exposeHeadRoutes: false,
56
+ requestTimeout: 60000,
57
+ connectionTimeout: 120000,
58
+ routerOptions: {
59
+ ignoreTrailingSlash: true,
60
+ maxParamLength: this.#serverConfigs.web_server.maxParamLength
61
+ }
62
+ });
63
+
64
+ this.#registerPlugins();
65
+ this.#registerHooks();
66
+ this.#registerShutdownHandlers();
67
+ }
68
+
69
+ static #registerPlugins() {
70
+ this.#server.register(fastifyCors, {
71
+ origin: this.#getCorsOrigin.bind(this),
72
+ credentials: this.#serverConfigs.cors.origin === "*" ? false : this.#serverConfigs.cors.credentials === true
73
+ });
74
+
75
+ this.#server.register(fastifyStatic, {
76
+ root: Paths.public,
77
+ decorateReply: false,
78
+ index: false
79
+ });
80
+
81
+ this.#server.register(fastifyCookie, {
82
+ secret: process.env.APP_KEY,
83
+ hook: "onRequest"
84
+ });
85
+
86
+ this.#server.register(fastifyHelmet, {
87
+ contentSecurityPolicy: false,
88
+ crossOriginEmbedderPolicy: false,
89
+ crossOriginOpenerPolicy: { policy: "same-origin" },
90
+ crossOriginResourcePolicy: { policy: "same-origin" },
91
+ dnsPrefetchControl: { allow: false },
92
+ frameguard: { action: "deny" },
93
+ hidePoweredBy: true,
94
+ hsts: !IS_DEV,
95
+ ieNoOpen: true,
96
+ noSniff: true,
97
+ originAgentCluster: true,
98
+ permittedCrossDomainPolicies: { permittedPolicies: "none" },
99
+ referrerPolicy: { policy: "strict-origin-when-cross-origin" },
100
+ xssFilter: true
101
+ });
102
+
103
+ this.#server.register(fastifyMultipart, {
104
+ limits: {
105
+ fieldNameSize: this.#serverConfigs.web_server.multipart.fieldNameSize,
106
+ fieldSize: this.#serverConfigs.web_server.multipart.fieldSize,
107
+ fields: this.#serverConfigs.web_server.multipart.maxFields,
108
+ fileSize: this.#serverConfigs.web_server.multipart.maxFileSize,
109
+ files: this.#serverConfigs.web_server.multipart.maxFiles,
110
+ parts: this.#serverConfigs.web_server.multipart.maxParts
111
+ },
112
+ attachFieldsToBody: this.#serverConfigs.web_server.multipart.attachFieldsToBody
113
+ });
114
+ }
115
+
116
+ static #getCorsOrigin(requestOrigin, cb) {
117
+ if (!requestOrigin) return cb(null, true);
118
+
119
+ const { origin } = this.#serverConfigs.cors;
120
+
121
+ if (origin === "*") return cb(null, true);
122
+
123
+ if (origin === "auto") {
124
+ const url = process.env.APP_URL;
125
+ const port = process.env.APP_PORT;
126
+ if (!url) return cb(null, false);
127
+
128
+ const appOrigin = !port || port === "80" || port === "443" ? url : `${url}:${port}`;
129
+ return cb(null, requestOrigin === appOrigin);
130
+ }
131
+
132
+ if (typeof origin === "string") return cb(null, requestOrigin === origin);
133
+ if (Array.isArray(origin)) return cb(null, origin.includes(requestOrigin));
134
+
135
+ return cb(null, false);
136
+ }
137
+
138
+ static #parseBracketNotation(data) {
139
+ if (!data || typeof data !== "object") {
140
+ return data;
141
+ }
142
+
143
+ const hasBrackets = Object.keys(data).some(key => key.includes("["));
144
+ if (!hasBrackets) {
145
+ return data;
146
+ }
147
+
148
+ const result = {};
149
+
150
+ for (const [key, value] of Object.entries(data)) {
151
+ if (!key.includes("[")) {
152
+ result[key] = value;
153
+ continue;
154
+ }
155
+
156
+ const parts = key.replace(/\]/g, "").split("[");
157
+ let current = result;
158
+
159
+ for (let i = 0; i < parts.length - 1; i++) {
160
+ const part = parts[i];
161
+ const nextPart = parts[i + 1];
162
+ const isNextIndex = /^\d+$/.test(nextPart);
163
+
164
+ if (current[part] === undefined) {
165
+ current[part] = isNextIndex ? [] : {};
166
+ }
167
+
168
+ current = current[part];
169
+ }
170
+
171
+ const lastPart = parts[parts.length - 1];
172
+ current[lastPart] = value;
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ static #registerHooks() {
179
+ this.#server.addHook("preHandler", async (req) => {
180
+ if (req.isMultipart() && req.body) {
181
+ const normalized = {};
182
+ const files = {};
183
+
184
+ for (const [key, field] of Object.entries(req.body)) {
185
+ if (field && typeof field === 'object') {
186
+ if (field.toBuffer) {
187
+ if (!field.filename) continue;
188
+
189
+ files[key] = field;
190
+ }
191
+ else if ('value' in field) {
192
+ normalized[key] = field.value;
193
+ }
194
+ else {
195
+ normalized[key] = field;
196
+ }
197
+ }
198
+ else {
199
+ normalized[key] = field;
200
+ }
201
+ }
202
+
203
+ req.body = this.#parseBracketNotation({ ...normalized, ...files });
204
+ }
205
+ });
206
+
207
+ this.#server.addHook("onResponse", async (req, res) => {
208
+ Log.info("HTTP request processed", {
209
+ method: req.method,
210
+ url: req.url,
211
+ statusCode: res.statusCode,
212
+ responseTime: `${res.elapsedTime}ms`,
213
+ ip: req.ip,
214
+ userAgent: req.headers["user-agent"] || "-",
215
+ referer: req.headers["referer"] || "-",
216
+ contentLength: res.getHeader("content-length") || "-",
217
+ requestId: req.id
218
+ });
219
+ });
220
+ }
221
+
222
+ static #registerShutdownHandlers() {
223
+ const shutdown = async () => {
224
+ const sessionManager = await SessionManager.getInstance();
225
+ sessionManager.stopGarbageCollection();
226
+ await DB.close();
227
+ await this.#server.close();
228
+ process.exit(0);
229
+ };
230
+
231
+ process.on("SIGTERM", shutdown);
232
+ process.on("SIGINT", shutdown);
233
+ }
234
+
235
+ static async #listen() {
236
+ try {
237
+ const url = process.env.APP_URL ? new URL(process.env.APP_URL) : null;
238
+ const host = url ? url.hostname : "127.0.0.1";
239
+ const port = Number(process.env.APP_PORT) || 3000;
240
+ const address = await this.#server.listen({ host, port });
241
+
242
+ this.#printBanner({ success: true, address, host, port });
243
+
244
+ Log.info("Server started successfully!", {
245
+ address,
246
+ host,
247
+ port,
248
+ environment: IS_DEV ? "development" : "production"
249
+ });
250
+ }
251
+ catch (err) {
252
+ this.#printBanner({ success: false, error: err });
253
+
254
+ Log.fatal("Server startup failed", {
255
+ error: err.message,
256
+ code: err.code,
257
+ stack: err.stack
258
+ });
259
+
260
+ process.exit(1);
261
+ }
262
+ }
263
+
264
+ static #printBanner({ success, address, host, port, error }) {
265
+ const { log } = this.#serverConfigs;
266
+ if (log.channel !== "none" && log.channel !== "file") return;
267
+
268
+ const color = success ? "\x1b[32m" : "\x1b[31m";
269
+ const reset = "\x1b[0m";
270
+ const bold = "\x1b[1m";
271
+ const boxWidth = 70;
272
+
273
+ const logo = `
274
+ ${color}███╗ ██╗██╗████████╗██████╗ ██████╗ ███╗ ██╗ ██╗███████╗
275
+ ████╗ ██║██║╚══██╔══╝██╔══██╗██╔═══██╗████╗ ██║ ██║██╔════╝
276
+ ██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██╔██╗ ██║ ██║███████╗
277
+ ██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║╚██╗██║██ ██║╚════██║
278
+ ██║ ╚████║██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║╚█████╔╝███████║
279
+ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚════╝ ╚══════╝${reset}
280
+ `;
281
+
282
+ const stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, "");
283
+ const pad = (content, length) => {
284
+ const visible = stripAnsi(content);
285
+ return content + " ".repeat(Math.max(0, length - visible.length));
286
+ };
287
+
288
+ const lines = success
289
+ ? [
290
+ `${color}${bold}✓${reset} ${bold}Server started successfully!${reset}`,
291
+ "",
292
+ `${bold}Address:${reset} ${address}`,
293
+ `${bold}Host:${reset} ${host}`,
294
+ `${bold}Port:${reset} ${port}`,
295
+ `${bold}Mode:${reset} ${IS_DEV ? "development" : "production"}`
296
+ ]
297
+ : [
298
+ `${color}${bold}✕${reset} ${bold}Server failed to start${reset}`,
299
+ "",
300
+ `${bold}Error:${reset} ${error?.message || error}`,
301
+ ...(error?.code ? [`${bold}Code:${reset} ${error.code}`] : [])
302
+ ];
303
+
304
+ console.log(logo);
305
+ console.log(`${color}┌${"─".repeat(boxWidth - 2)}┐${reset}`);
306
+
307
+ for (const line of lines) {
308
+ console.log(`${color}│${reset} ${pad(line, boxWidth - 4)} ${color}│${reset}`);
309
+ }
310
+
311
+ console.log(`${color}│${reset} ${" ".repeat(boxWidth - 4)} ${color}│${reset}`);
312
+ console.log(`${color}└${"─".repeat(boxWidth - 2)}┘${reset}`);
313
+ console.log();
314
+ }
315
+ }
316
+
317
+ export default Server;
@@ -0,0 +1,153 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Config from "../Core/Config.js";
4
+
5
+ class LogManager {
6
+ static #levels = {
7
+ debug: 0,
8
+ info: 1,
9
+ warn: 2,
10
+ error: 3,
11
+ fatal: 4
12
+ };
13
+
14
+ static debug(message, context = {}) {
15
+ this.#log("debug", message, context);
16
+ }
17
+
18
+ static info(message, context = {}) {
19
+ this.#log("info", message, context);
20
+ }
21
+
22
+ static warn(message, context = {}) {
23
+ this.#log("warn", message, context);
24
+ }
25
+
26
+ static error(message, context = {}) {
27
+ this.#log("error", message, context);
28
+ }
29
+
30
+ static fatal(message, context = {}) {
31
+ this.#log("fatal", message, context);
32
+ }
33
+
34
+ static #getConfig() {
35
+ return {
36
+ channel: Config.get("server.log.channel", "console"),
37
+ level: Config.get("server.log.level", "info"),
38
+ file: Config.get("server.log.file", "./storage/logs/app.log"),
39
+ sync: Config.get("server.log.sync", false)
40
+ };
41
+ }
42
+
43
+ static #log(level, message, context) {
44
+ try {
45
+ const config = this.#getConfig();
46
+
47
+ if (!this.#shouldLog(level, config)) {
48
+ return;
49
+ }
50
+
51
+ const payload = {
52
+ level,
53
+ message,
54
+ context,
55
+ timestamp: new Date().toISOString(),
56
+ pid: process.pid
57
+ };
58
+
59
+ const channel = config.channel;
60
+
61
+ if (channel === "none") {
62
+ return;
63
+ }
64
+
65
+ if (channel === "console") {
66
+ this.#logToConsole(payload);
67
+ }
68
+
69
+ if (channel === "file") {
70
+ this.#logToFile(payload, config);
71
+ }
72
+ } catch (e) {
73
+ console.error("Logger failure:", e.message);
74
+ }
75
+ }
76
+
77
+ static #shouldLog(level, config) {
78
+ const configLevel = config.level;
79
+ return this.#levels[level] >= this.#levels[configLevel];
80
+ }
81
+
82
+ static #logToConsole({ level, message, context, timestamp }) {
83
+ const colors = {
84
+ debug: "\x1b[90m",
85
+ info: "\x1b[36m",
86
+ warn: "\x1b[33m",
87
+ error: "\x1b[31m",
88
+ fatal: "\x1b[35m"
89
+ };
90
+
91
+ const icons = {
92
+ debug: "◆",
93
+ info: "●",
94
+ warn: "⚠",
95
+ error: "✕",
96
+ fatal: "■"
97
+ };
98
+
99
+ const color = colors[level] || "";
100
+ const icon = icons[level] || "";
101
+ const reset = "\x1b[0m";
102
+ const bold = "\x1b[1m";
103
+ const dim = "\x1b[2m";
104
+
105
+ const date = new Date(timestamp);
106
+ const time = date.toLocaleTimeString("en-US", { hour12: false });
107
+ const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
108
+ const width = process.stdout.columns || 80;
109
+
110
+ console.log(`${color}┌${"─".repeat(width - 2)}${reset}`);
111
+ console.log(`${color}│${reset} ${color}${icon}${reset} ${bold}${level.toUpperCase()}${reset}`);
112
+ console.log(`${color}│${reset} ${dim}${dateStr} at ${time}${reset}`);
113
+ console.log(`${color}├${"─".repeat(width - 2)}${reset}`);
114
+ console.log(`${color}│${reset}`);
115
+ console.log(`${color}│${reset} ${bold}${message}${reset}`);
116
+ console.log(`${color}│${reset}`);
117
+
118
+ if (Object.keys(context).length > 0) {
119
+ console.log(`${color}│${reset} ${dim}Context:${reset}`);
120
+ const contextJson = JSON.stringify(context, null, 2);
121
+ contextJson.split("\n").forEach(line => {
122
+ console.log(`${color}│${reset} ${dim}${line}${reset}`);
123
+ });
124
+ console.log(`${color}│${reset}`);
125
+ }
126
+
127
+ console.log(`${color}└${"─".repeat(width - 2)}${reset}`);
128
+ console.log();
129
+ }
130
+
131
+ static #logToFile(payload, config) {
132
+ const filePath = path.resolve(process.cwd(), config.file);
133
+ const dir = path.dirname(filePath);
134
+
135
+ if (!fs.existsSync(dir)) {
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ }
138
+
139
+ const logLine = JSON.stringify(payload) + "\n";
140
+
141
+ if (config.sync) {
142
+ fs.appendFileSync(filePath, logLine);
143
+ } else {
144
+ fs.appendFile(filePath, logLine, (err) => {
145
+ if (err) {
146
+ console.error("Failed to write log:", err.message);
147
+ }
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ export default LogManager;
@@ -0,0 +1,120 @@
1
+ import nodemailer from "nodemailer";
2
+ import ViewManager from "../View/Manager.js";
3
+
4
+ class MailManager {
5
+ static from(email) {
6
+ return MailManager.#fromService(new MailManager(), email);
7
+ }
8
+
9
+ from(email) {
10
+ return MailManager.#fromService(this, email);
11
+ }
12
+
13
+ static to(email) {
14
+ return MailManager.#toService(new MailManager(), email);
15
+ }
16
+
17
+ to(email) {
18
+ return MailManager.#toService(this, email);
19
+ }
20
+
21
+ subject(subject) {
22
+ this.subject = subject;
23
+ return this;
24
+ }
25
+
26
+ attachment(attachment) {
27
+ if (!this.attachments) {
28
+ this.attachments = [];
29
+ }
30
+ this.attachments.push(attachment);
31
+ return this;
32
+ }
33
+
34
+ calendar(icsContent) {
35
+ this.calendarContent = icsContent;
36
+ return this;
37
+ }
38
+
39
+ text(message) {
40
+ this.text = message;
41
+ return this;
42
+ }
43
+
44
+ view(res, view, data = null) {
45
+ this.view = ViewManager.renderFile(view, data);
46
+ return this;
47
+ }
48
+
49
+ async send(transportCallback = null) {
50
+ let transporter;
51
+
52
+ if (typeof transportCallback === "function") {
53
+ const transportData = {
54
+ MAIL_HOST: null,
55
+ MAIL_PORT: null,
56
+ MAIL_USERNAME: null,
57
+ MAIL_PASSWORD: null,
58
+ MAIL_SECURE: null
59
+ };
60
+ transportCallback(transportData);
61
+ transporter = MailManager.#createTransport(transportData);
62
+ } else {
63
+ transporter = MailManager.#createTransport(process.env);
64
+ }
65
+
66
+ const response = await MailManager.#sendMail(this, transporter);
67
+ return response;
68
+ }
69
+
70
+ static #fromService(object, email) {
71
+ object.fromAddress = email;
72
+ return object;
73
+ }
74
+
75
+ static #toService(object, email) {
76
+ object.toAddress = email;
77
+ return object;
78
+ }
79
+
80
+ static #createTransport(transportData) {
81
+ return nodemailer.createTransport({
82
+ host: transportData.MAIL_HOST,
83
+ port: transportData.MAIL_PORT,
84
+ secure: transportData.MAIL_SECURE || false,
85
+ auth: {
86
+ user: transportData.MAIL_USERNAME,
87
+ pass: transportData.MAIL_PASSWORD
88
+ }
89
+ });
90
+ }
91
+
92
+ static async #sendMail(object, transporter) {
93
+ const mailOptions = {
94
+ from: object.fromAddress || null,
95
+ to: object.toAddress || null,
96
+ subject: object.subject || null,
97
+ text: object.text || null,
98
+ html: object.view || null,
99
+ attachments: object.attachments || undefined,
100
+ alternatives: object.calendarContent
101
+ ? [
102
+ {
103
+ contentType: 'text/calendar; method=REQUEST; charset="UTF-8"',
104
+ content: object.calendarContent,
105
+ component: "VEVENT",
106
+ headers: {
107
+ "Content-Class": "urn:content-classes:calendarmessage"
108
+ }
109
+ }
110
+ ]
111
+ : undefined
112
+ };
113
+
114
+ const response = await transporter.sendMail(mailOptions);
115
+ transporter.close();
116
+ return response;
117
+ }
118
+ }
119
+
120
+ export default MailManager;
@@ -0,0 +1,81 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import Paths from "../Core/Paths.js";
4
+
5
+ const IS_DEV = process.env.APP_DEV === "true";
6
+
7
+ const DIRECTORIES = [
8
+ Paths.controllers,
9
+ Paths.middlewares
10
+ ];
11
+
12
+ class Loader {
13
+ #registry = new Map();
14
+ #initialized = false;
15
+
16
+ async initialize() {
17
+ if (this.#initialized || !IS_DEV) return;
18
+
19
+ for (const dir of DIRECTORIES) {
20
+ await this.#loadDirectory(dir);
21
+ }
22
+
23
+ this.#initialized = true;
24
+ }
25
+
26
+ wrapHandler(handler) {
27
+ if (!IS_DEV) return handler;
28
+
29
+ const info = this.#registry.get(handler);
30
+ if (!info) return handler;
31
+
32
+ const { filePath, methodName } = info;
33
+
34
+ return async (request, response, param) => {
35
+ const module = await import(`file://${filePath}?t=${Date.now()}`);
36
+ return module.default[methodName](request, response, param);
37
+ };
38
+ }
39
+
40
+ async #loadDirectory(dir) {
41
+ if (!fs.existsSync(dir)) return;
42
+
43
+ for (const filePath of this.#getJsFiles(dir)) {
44
+ try {
45
+ const module = await import(`file://${filePath}`);
46
+ const Class = module.default;
47
+
48
+ if (!Class || typeof Class !== "function") continue;
49
+
50
+ for (const name of Object.getOwnPropertyNames(Class)) {
51
+ if (typeof Class[name] === "function" && !["length", "name", "prototype"].includes(name)) {
52
+ this.#registry.set(Class[name], { filePath, methodName: name });
53
+ }
54
+ }
55
+ }
56
+ catch {
57
+ //
58
+ }
59
+ }
60
+ }
61
+
62
+ #getJsFiles(dir) {
63
+ const files = [];
64
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
65
+
66
+ for (const entry of entries) {
67
+ const fullPath = path.join(dir, entry.name);
68
+
69
+ if (entry.isDirectory()) {
70
+ files.push(...this.#getJsFiles(fullPath));
71
+ }
72
+ else if (entry.name.endsWith(".js")) {
73
+ files.push(fullPath);
74
+ }
75
+ }
76
+
77
+ return files;
78
+ }
79
+ }
80
+
81
+ export default new Loader();