@nitronjs/framework 0.2.3 → 0.2.4
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/README.md +3 -1
- package/cli/create.js +88 -72
- package/cli/njs.js +13 -6
- package/lib/Auth/Auth.js +167 -0
- package/lib/Build/CssBuilder.js +9 -0
- package/lib/Build/FileAnalyzer.js +16 -0
- package/lib/Build/HydrationBuilder.js +17 -0
- package/lib/Build/Manager.js +15 -0
- package/lib/Build/colors.js +4 -0
- package/lib/Build/plugins.js +84 -20
- package/lib/Console/Commands/DevCommand.js +13 -9
- package/lib/Console/Commands/MakeCommand.js +24 -10
- package/lib/Console/Commands/MigrateCommand.js +0 -1
- package/lib/Console/Commands/MigrateFreshCommand.js +18 -25
- package/lib/Console/Commands/MigrateRollbackCommand.js +6 -3
- package/lib/Console/Commands/MigrateStatusCommand.js +6 -3
- package/lib/Console/Commands/SeedCommand.js +4 -2
- package/lib/Console/Commands/StorageLinkCommand.js +20 -5
- package/lib/Console/Output.js +143 -0
- package/lib/Core/Config.js +2 -1
- package/lib/Core/Paths.js +8 -0
- package/lib/Database/DB.js +141 -51
- package/lib/Database/Drivers/MySQLDriver.js +102 -157
- package/lib/Database/Migration/Checksum.js +3 -8
- package/lib/Database/Migration/MigrationRepository.js +25 -35
- package/lib/Database/Migration/MigrationRunner.js +56 -61
- package/lib/Database/Model.js +157 -83
- package/lib/Database/QueryBuilder.js +31 -0
- package/lib/Database/QueryValidation.js +36 -44
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +12 -31
- package/lib/Date/DateTime.js +9 -0
- package/lib/Encryption/Encryption.js +52 -0
- package/lib/Faker/Faker.js +11 -0
- package/lib/Filesystem/Storage.js +120 -0
- package/lib/HMR/Server.js +79 -9
- package/lib/Hashing/Hash.js +41 -0
- package/lib/Http/Server.js +177 -152
- package/lib/Logging/{Manager.js → Log.js} +68 -80
- package/lib/Mail/Mail.js +187 -0
- package/lib/Route/Router.js +416 -0
- package/lib/Session/File.js +135 -233
- package/lib/Session/Manager.js +117 -171
- package/lib/Session/Memory.js +28 -38
- package/lib/Session/Session.js +71 -107
- package/lib/Support/Str.js +103 -0
- package/lib/Translation/Lang.js +54 -0
- package/lib/View/Client/hmr-client.js +87 -51
- package/lib/View/Client/nitronjs-icon.png +0 -0
- package/lib/View/{Manager.js → View.js} +44 -29
- package/lib/index.d.ts +42 -8
- package/lib/index.js +19 -12
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- package/skeleton/resources/css/global.css +1 -0
- package/skeleton/resources/views/Site/Home.tsx +456 -79
- package/skeleton/tsconfig.json +6 -1
- package/lib/Auth/Manager.js +0 -111
- package/lib/Database/Connection.js +0 -61
- package/lib/Database/Manager.js +0 -162
- package/lib/Encryption/Manager.js +0 -47
- package/lib/Filesystem/Manager.js +0 -74
- package/lib/Hashing/Manager.js +0 -25
- package/lib/Mail/Manager.js +0 -120
- package/lib/Route/Loader.js +0 -80
- package/lib/Route/Manager.js +0 -286
- package/lib/Translation/Manager.js +0 -49
package/lib/Session/Manager.js
CHANGED
|
@@ -5,254 +5,200 @@ import File from "./File.js";
|
|
|
5
5
|
import Session from "./Session.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Singleton that manages all sessions for the application.
|
|
11
|
-
* Handles loading, creation, persistence, and lifecycle.
|
|
12
|
-
*
|
|
13
|
-
* Features:
|
|
14
|
-
* - Cookie-based session IDs (signed)
|
|
15
|
-
* - Pluggable storage drivers (Memory, File)
|
|
16
|
-
* - Automatic session persistence
|
|
17
|
-
* - Lifetime management and expiration
|
|
18
|
-
* - Garbage collection for expired sessions
|
|
8
|
+
* Session manager singleton.
|
|
9
|
+
* Handles session loading, creation, persistence, and garbage collection.
|
|
19
10
|
*/
|
|
20
11
|
class SessionManager {
|
|
21
|
-
static instance = null;
|
|
22
|
-
static #isInternal = false;
|
|
23
|
-
|
|
24
|
-
// Instance properties
|
|
25
|
-
config = null;
|
|
26
|
-
store = null;
|
|
27
|
-
ready = null;
|
|
28
|
-
gcTimer = null;
|
|
29
|
-
|
|
30
|
-
constructor () {
|
|
31
|
-
if (!SessionManager.#isInternal) {
|
|
32
|
-
throw new Error("SessionManager must be initialized with await SessionManager.getInstance()");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
static getSessionConfig() {
|
|
37
|
-
return Config.all("session");
|
|
38
|
-
}
|
|
12
|
+
static #instance = null;
|
|
39
13
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
SessionManager.#isInternal = false;
|
|
44
|
-
|
|
45
|
-
instance.config = this.getSessionConfig();
|
|
46
|
-
instance.config.cookie.signed = true; // Enforce signed cookies
|
|
47
|
-
// Load storage driver based on config
|
|
48
|
-
instance.store = instance.#createDriver(instance.config.driver);
|
|
49
|
-
// Store ready promise (for File driver initialization)
|
|
50
|
-
instance.ready = instance.store.ready || Promise.resolve();
|
|
51
|
-
return instance;
|
|
52
|
-
}
|
|
14
|
+
#config = null;
|
|
15
|
+
#store = null;
|
|
16
|
+
#gcTimer = null;
|
|
53
17
|
|
|
54
18
|
/**
|
|
55
|
-
*
|
|
56
|
-
* @
|
|
57
|
-
* @returns {object} Driver instance
|
|
19
|
+
* Gets the singleton instance.
|
|
20
|
+
* @returns {Promise<SessionManager>}
|
|
58
21
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
case 'memory':
|
|
64
|
-
default:
|
|
65
|
-
return new Memory();
|
|
22
|
+
static async getInstance() {
|
|
23
|
+
if (!this.#instance) {
|
|
24
|
+
this.#instance = new SessionManager();
|
|
25
|
+
await this.#instance.#initialize();
|
|
66
26
|
}
|
|
27
|
+
|
|
28
|
+
return this.#instance;
|
|
67
29
|
}
|
|
68
30
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
31
|
+
/** @private */
|
|
32
|
+
async #initialize() {
|
|
33
|
+
this.#config = Config.all("session");
|
|
34
|
+
this.#config.cookie.signed = true;
|
|
35
|
+
|
|
36
|
+
this.#store = this.#config.driver === "file" ? new File() : new Memory();
|
|
37
|
+
|
|
38
|
+
if (this.#store.ready) {
|
|
39
|
+
await this.#store.ready;
|
|
75
40
|
}
|
|
76
|
-
return SessionManager.instance;
|
|
77
41
|
}
|
|
78
42
|
|
|
79
|
-
// ========================================
|
|
80
|
-
// Session Lifecycle
|
|
81
|
-
// ========================================
|
|
82
|
-
|
|
83
43
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @param {object} request - Fastify request object
|
|
90
|
-
* @param {object} response - Fastify response object
|
|
91
|
-
* @returns {Session} Session instance
|
|
44
|
+
* Loads existing session or creates a new one.
|
|
45
|
+
* @param {import("fastify").FastifyRequest} request
|
|
46
|
+
* @param {import("fastify").FastifyReply} response
|
|
47
|
+
* @returns {Promise<Session>}
|
|
92
48
|
*/
|
|
93
|
-
async load
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const signedCookie = request.cookies[this.config.cookieName];
|
|
98
|
-
|
|
99
|
-
// No cookie → new session
|
|
49
|
+
async load(request, response) {
|
|
50
|
+
const signedCookie = request.cookies[this.#config.cookieName];
|
|
51
|
+
|
|
100
52
|
if (!signedCookie) {
|
|
101
|
-
return this
|
|
53
|
+
return this.#create(response);
|
|
102
54
|
}
|
|
103
55
|
|
|
104
|
-
// Verify cookie signature
|
|
105
56
|
const unsignResult = request.unsignCookie(signedCookie);
|
|
57
|
+
|
|
106
58
|
if (!unsignResult.valid) {
|
|
107
|
-
response.clearCookie(this
|
|
108
|
-
|
|
109
|
-
return this
|
|
59
|
+
response.clearCookie(this.#config.cookieName);
|
|
60
|
+
|
|
61
|
+
return this.#create(response);
|
|
110
62
|
}
|
|
111
63
|
|
|
112
64
|
const sessionId = unsignResult.value;
|
|
113
|
-
const sessionData = await this
|
|
65
|
+
const sessionData = await this.#store.get(sessionId);
|
|
114
66
|
|
|
115
|
-
// Session not found → new session
|
|
116
67
|
if (!sessionData) {
|
|
117
|
-
response.clearCookie(this
|
|
68
|
+
response.clearCookie(this.#config.cookieName);
|
|
118
69
|
|
|
119
|
-
return this
|
|
70
|
+
return this.#create(response);
|
|
120
71
|
}
|
|
121
72
|
|
|
122
|
-
// Check if session expired (lifetime)
|
|
123
73
|
const lastActivity = sessionData.lastActivity || sessionData.createdAt;
|
|
124
|
-
if (Date.now() - lastActivity > this.config.lifetime) {
|
|
125
|
-
await this.store.delete(sessionId);
|
|
126
|
-
response.clearCookie(this.config.cookieName);
|
|
127
74
|
|
|
128
|
-
|
|
75
|
+
if (Date.now() - lastActivity > this.#config.lifetime) {
|
|
76
|
+
await this.#store.delete(sessionId);
|
|
77
|
+
response.clearCookie(this.#config.cookieName);
|
|
78
|
+
|
|
79
|
+
return this.#create(response);
|
|
129
80
|
}
|
|
130
81
|
|
|
131
82
|
return new Session(sessionId, sessionData);
|
|
132
83
|
}
|
|
133
84
|
|
|
134
85
|
/**
|
|
135
|
-
*
|
|
136
|
-
* @param {
|
|
137
|
-
* @returns {Session} New session instance
|
|
86
|
+
* Persists session data to store.
|
|
87
|
+
* @param {Session} session
|
|
138
88
|
*/
|
|
139
|
-
async
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
await this.store.set(id, sessionData);
|
|
147
|
-
|
|
148
|
-
// Set signed cookie
|
|
149
|
-
const cookieOptions = { ...this.config.cookie, signed: true };
|
|
150
|
-
response.setCookie(this.config.cookieName, id, cookieOptions);
|
|
89
|
+
async persist(session) {
|
|
90
|
+
await this.#store.set(session.id, {
|
|
91
|
+
data: session.all(),
|
|
92
|
+
createdAt: session.createdAt,
|
|
93
|
+
lastActivity: Date.now()
|
|
94
|
+
});
|
|
95
|
+
}
|
|
151
96
|
|
|
152
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Deletes old session after regeneration.
|
|
99
|
+
* @param {string} oldId
|
|
100
|
+
*/
|
|
101
|
+
async deleteOld(oldId) {
|
|
102
|
+
if (oldId) {
|
|
103
|
+
await this.#store.delete(oldId);
|
|
104
|
+
}
|
|
153
105
|
}
|
|
154
106
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Gets config for cookie options.
|
|
109
|
+
* @returns {Object}
|
|
110
|
+
*/
|
|
111
|
+
get cookieConfig() {
|
|
112
|
+
return { ...this.#config.cookie, signed: true };
|
|
113
|
+
}
|
|
158
114
|
|
|
159
115
|
/**
|
|
160
|
-
*
|
|
116
|
+
* Gets session cookie name.
|
|
117
|
+
* @returns {string}
|
|
161
118
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
119
|
+
get cookieName() {
|
|
120
|
+
return this.#config.cookieName;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @private */
|
|
124
|
+
async #create(response) {
|
|
125
|
+
const id = crypto.randomBytes(32).toString("hex");
|
|
126
|
+
const sessionData = { data: {}, createdAt: Date.now() };
|
|
127
|
+
|
|
128
|
+
await this.#store.set(id, sessionData);
|
|
129
|
+
response.setCookie(this.#config.cookieName, id, this.cookieConfig);
|
|
130
|
+
|
|
131
|
+
return new Session(id, sessionData);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @private */
|
|
135
|
+
#startGC() {
|
|
136
|
+
if (typeof this.#store.gc !== "function") {
|
|
137
|
+
return;
|
|
165
138
|
}
|
|
166
139
|
|
|
167
|
-
const
|
|
140
|
+
const interval = 15 * 60 * 1000;
|
|
168
141
|
|
|
169
|
-
this
|
|
142
|
+
this.#gcTimer = setInterval(async () => {
|
|
170
143
|
try {
|
|
171
|
-
const deleted = await this
|
|
144
|
+
const deleted = await this.#store.gc(this.#config.lifetime);
|
|
145
|
+
|
|
172
146
|
if (deleted > 0) {
|
|
173
|
-
console.log(`[Session GC
|
|
147
|
+
console.log(`[Session] GC: ${deleted} expired sessions deleted`);
|
|
174
148
|
}
|
|
175
149
|
}
|
|
176
|
-
catch (
|
|
177
|
-
console.error(
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error("[Session] GC error:", err.message);
|
|
178
152
|
}
|
|
179
|
-
},
|
|
153
|
+
}, interval);
|
|
180
154
|
|
|
181
|
-
|
|
182
|
-
this.store.gc(this.config.lifetime).catch(err => {
|
|
183
|
-
console.error('[Session GC] Initial run failed:', err.message);
|
|
184
|
-
});
|
|
155
|
+
this.#store.gc(this.#config.lifetime).catch(() => {});
|
|
185
156
|
}
|
|
186
157
|
|
|
187
158
|
/**
|
|
188
|
-
*
|
|
159
|
+
* Stops the garbage collection timer.
|
|
189
160
|
*/
|
|
190
|
-
|
|
191
|
-
if (this
|
|
192
|
-
clearInterval(this
|
|
193
|
-
this
|
|
161
|
+
stopGC() {
|
|
162
|
+
if (this.#gcTimer) {
|
|
163
|
+
clearInterval(this.#gcTimer);
|
|
164
|
+
this.#gcTimer = null;
|
|
194
165
|
}
|
|
195
166
|
}
|
|
196
167
|
|
|
197
|
-
// ========================================
|
|
198
|
-
// Server Setup
|
|
199
|
-
// ========================================
|
|
200
|
-
|
|
201
168
|
/**
|
|
202
|
-
*
|
|
203
|
-
* @param {
|
|
169
|
+
* Sets up session middleware for Fastify.
|
|
170
|
+
* @param {import("fastify").FastifyInstance} server
|
|
204
171
|
*/
|
|
205
|
-
static async setup
|
|
172
|
+
static async setup(server) {
|
|
206
173
|
const manager = await SessionManager.getInstance();
|
|
207
|
-
|
|
174
|
+
|
|
208
175
|
server.decorateRequest("session", null);
|
|
209
176
|
|
|
210
|
-
|
|
211
|
-
server.addHook('preHandler', async (request, response) => {
|
|
177
|
+
server.addHook("preHandler", async (request, response) => {
|
|
212
178
|
request.session = await manager.load(request, response);
|
|
213
179
|
});
|
|
214
180
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Handle regeneration - set new session cookie
|
|
220
|
-
if (request.session.shouldRegenerate()) {
|
|
221
|
-
response.setCookie(
|
|
222
|
-
manager.config.cookieName,
|
|
223
|
-
request.session.id,
|
|
224
|
-
{ ...manager.config.cookie, signed: true }
|
|
225
|
-
);
|
|
181
|
+
server.addHook("onSend", async (request, response, payload) => {
|
|
182
|
+
if (request.session?.shouldRegenerate()) {
|
|
183
|
+
response.setCookie(manager.cookieName, request.session.id, manager.cookieConfig);
|
|
226
184
|
}
|
|
227
185
|
|
|
228
186
|
return payload;
|
|
229
187
|
});
|
|
230
188
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// Skip persistence for error responses (4xx/5xx)
|
|
236
|
-
if (response.statusCode >= 400) return;
|
|
189
|
+
server.addHook("onResponse", async (request, response) => {
|
|
190
|
+
if (!request.session || response.statusCode >= 400) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
237
193
|
|
|
238
|
-
// Handle regeneration - delete old session
|
|
239
194
|
if (request.session.shouldRegenerate()) {
|
|
240
|
-
|
|
241
|
-
if (oldId) {
|
|
242
|
-
await manager.store.delete(oldId);
|
|
243
|
-
}
|
|
195
|
+
await manager.deleteOld(request.session.getOldId());
|
|
244
196
|
}
|
|
245
197
|
|
|
246
|
-
|
|
247
|
-
await manager.store.set(request.session.id, {
|
|
248
|
-
data: request.session.all(),
|
|
249
|
-
createdAt: request.session.createdAt,
|
|
250
|
-
lastActivity: Date.now(),
|
|
251
|
-
});
|
|
198
|
+
await manager.persist(request.session);
|
|
252
199
|
});
|
|
253
200
|
|
|
254
|
-
|
|
255
|
-
manager.startGarbageCollection();
|
|
201
|
+
manager.#startGC();
|
|
256
202
|
}
|
|
257
203
|
}
|
|
258
204
|
|
package/lib/Session/Memory.js
CHANGED
|
@@ -1,61 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* In-memory session storage using JavaScript Map.
|
|
5
|
-
* Fast but not persistent - all data is lost on server restart.
|
|
6
|
-
*
|
|
7
|
-
* Use cases:
|
|
8
|
-
* - Development and testing
|
|
9
|
-
* - Single-server deployments with acceptable data loss
|
|
10
|
-
*
|
|
11
|
-
* Production: Use File store for persistence or Redis for clustering.
|
|
2
|
+
* In-memory session storage.
|
|
3
|
+
* Fast but not persistent - data is lost on restart.
|
|
12
4
|
*/
|
|
13
|
-
class
|
|
14
|
-
|
|
15
|
-
this.sessions = new Map();
|
|
16
|
-
}
|
|
5
|
+
class MemoryStore {
|
|
6
|
+
#sessions = new Map();
|
|
17
7
|
|
|
18
8
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @param {string} id
|
|
21
|
-
* @returns {
|
|
9
|
+
* Gets session data by ID.
|
|
10
|
+
* @param {string} id
|
|
11
|
+
* @returns {Promise<Object|null>}
|
|
22
12
|
*/
|
|
23
|
-
async get
|
|
24
|
-
return this
|
|
13
|
+
async get(id) {
|
|
14
|
+
return this.#sessions.get(id) || null;
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
/**
|
|
28
|
-
*
|
|
29
|
-
* @param {string} id
|
|
30
|
-
* @param {
|
|
18
|
+
* Stores session data.
|
|
19
|
+
* @param {string} id
|
|
20
|
+
* @param {Object} value
|
|
31
21
|
*/
|
|
32
|
-
async set
|
|
33
|
-
this
|
|
22
|
+
async set(id, value) {
|
|
23
|
+
this.#sessions.set(id, value);
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @param {string} id
|
|
39
|
-
* @returns {boolean}
|
|
27
|
+
* Deletes a session.
|
|
28
|
+
* @param {string} id
|
|
29
|
+
* @returns {Promise<boolean>}
|
|
40
30
|
*/
|
|
41
|
-
async delete
|
|
42
|
-
return this
|
|
31
|
+
async delete(id) {
|
|
32
|
+
return this.#sessions.delete(id);
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
/**
|
|
46
|
-
* Garbage collection -
|
|
47
|
-
* @param {number} lifetime -
|
|
48
|
-
* @returns {number} Number of deleted sessions
|
|
36
|
+
* Garbage collection - removes expired sessions.
|
|
37
|
+
* @param {number} lifetime - Max lifetime in milliseconds
|
|
38
|
+
* @returns {Promise<number>} Number of deleted sessions
|
|
49
39
|
*/
|
|
50
|
-
async gc
|
|
40
|
+
async gc(lifetime) {
|
|
51
41
|
const now = Date.now();
|
|
52
42
|
let deleted = 0;
|
|
53
43
|
|
|
54
|
-
for (const [id,
|
|
55
|
-
const lastActivity =
|
|
56
|
-
|
|
44
|
+
for (const [id, data] of this.#sessions.entries()) {
|
|
45
|
+
const lastActivity = data.lastActivity || data.createdAt;
|
|
46
|
+
|
|
57
47
|
if (now - lastActivity > lifetime) {
|
|
58
|
-
this
|
|
48
|
+
this.#sessions.delete(id);
|
|
59
49
|
deleted++;
|
|
60
50
|
}
|
|
61
51
|
}
|
|
@@ -64,4 +54,4 @@ class Memory {
|
|
|
64
54
|
}
|
|
65
55
|
}
|
|
66
56
|
|
|
67
|
-
export default
|
|
57
|
+
export default MemoryStore;
|