@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.
- package/README.md +429 -0
- package/cli/create.js +260 -0
- package/cli/njs.js +164 -0
- package/lib/Auth/Manager.js +111 -0
- package/lib/Build/Manager.js +1232 -0
- package/lib/Console/Commands/BuildCommand.js +25 -0
- package/lib/Console/Commands/DevCommand.js +385 -0
- package/lib/Console/Commands/MakeCommand.js +110 -0
- package/lib/Console/Commands/MigrateCommand.js +98 -0
- package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
- package/lib/Console/Commands/SeedCommand.js +92 -0
- package/lib/Console/Commands/StorageLinkCommand.js +31 -0
- package/lib/Console/Stubs/controller.js +19 -0
- package/lib/Console/Stubs/middleware.js +9 -0
- package/lib/Console/Stubs/migration.js +23 -0
- package/lib/Console/Stubs/model.js +7 -0
- package/lib/Console/Stubs/page-hydration.tsx +54 -0
- package/lib/Console/Stubs/seeder.js +9 -0
- package/lib/Console/Stubs/vendor.tsx +11 -0
- package/lib/Core/Config.js +86 -0
- package/lib/Core/Environment.js +21 -0
- package/lib/Core/Paths.js +188 -0
- package/lib/Database/Connection.js +61 -0
- package/lib/Database/DB.js +84 -0
- package/lib/Database/Drivers/MySQLDriver.js +234 -0
- package/lib/Database/Manager.js +162 -0
- package/lib/Database/Model.js +161 -0
- package/lib/Database/QueryBuilder.js +714 -0
- package/lib/Database/QueryValidation.js +62 -0
- package/lib/Database/Schema/Blueprint.js +126 -0
- package/lib/Database/Schema/Manager.js +116 -0
- package/lib/Date/DateTime.js +108 -0
- package/lib/Date/Locale.js +68 -0
- package/lib/Encryption/Manager.js +47 -0
- package/lib/Filesystem/Manager.js +49 -0
- package/lib/Hashing/Manager.js +25 -0
- package/lib/Http/Server.js +317 -0
- package/lib/Logging/Manager.js +153 -0
- package/lib/Mail/Manager.js +120 -0
- package/lib/Route/Loader.js +81 -0
- package/lib/Route/Manager.js +265 -0
- package/lib/Runtime/Entry.js +11 -0
- package/lib/Session/File.js +299 -0
- package/lib/Session/Manager.js +259 -0
- package/lib/Session/Memory.js +67 -0
- package/lib/Session/Session.js +196 -0
- package/lib/Support/Str.js +100 -0
- package/lib/Translation/Manager.js +49 -0
- package/lib/Validation/MimeTypes.js +39 -0
- package/lib/Validation/Validator.js +691 -0
- package/lib/View/Manager.js +544 -0
- package/lib/View/Templates/default/Home.tsx +262 -0
- package/lib/View/Templates/default/MainLayout.tsx +44 -0
- package/lib/View/Templates/errors/404.tsx +13 -0
- package/lib/View/Templates/errors/500.tsx +13 -0
- package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
- package/lib/View/Templates/messages/Maintenance.tsx +17 -0
- package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
- package/lib/index.js +57 -0
- package/package.json +47 -0
- package/skeleton/.env.example +26 -0
- package/skeleton/app/Controllers/HomeController.js +9 -0
- package/skeleton/app/Kernel.js +11 -0
- package/skeleton/app/Middlewares/Authentication.js +9 -0
- package/skeleton/app/Middlewares/Guest.js +9 -0
- package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
- package/skeleton/app/Models/User.js +7 -0
- package/skeleton/config/app.js +4 -0
- package/skeleton/config/auth.js +16 -0
- package/skeleton/config/database.js +27 -0
- package/skeleton/config/hash.js +3 -0
- package/skeleton/config/server.js +28 -0
- package/skeleton/config/session.js +21 -0
- package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
- package/skeleton/database/seeders/UserSeeder.js +15 -0
- package/skeleton/globals.d.ts +1 -0
- package/skeleton/package.json +24 -0
- package/skeleton/public/.gitkeep +0 -0
- package/skeleton/resources/css/.gitkeep +0 -0
- package/skeleton/resources/langs/.gitkeep +0 -0
- package/skeleton/resources/views/Site/Home.tsx +66 -0
- package/skeleton/routes/web.js +4 -0
- package/skeleton/storage/app/private/.gitkeep +0 -0
- package/skeleton/storage/app/public/.gitkeep +0 -0
- package/skeleton/storage/framework/sessions/.gitkeep +0 -0
- package/skeleton/storage/logs/.gitkeep +0 -0
- package/skeleton/tsconfig.json +33 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import Config from "../Core/Config.js";
|
|
3
|
+
import Memory from "./Memory.js";
|
|
4
|
+
import File from "./File.js";
|
|
5
|
+
import Session from "./Session.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SessionManager
|
|
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
|
|
19
|
+
*/
|
|
20
|
+
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
|
+
}
|
|
39
|
+
|
|
40
|
+
static async createInstance() {
|
|
41
|
+
SessionManager.#isInternal = true;
|
|
42
|
+
const instance = new SessionManager();
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create storage driver instance
|
|
56
|
+
* @param {string} driver - Driver name (memory, file)
|
|
57
|
+
* @returns {object} Driver instance
|
|
58
|
+
*/
|
|
59
|
+
#createDriver(driver) {
|
|
60
|
+
switch (driver) {
|
|
61
|
+
case 'file':
|
|
62
|
+
return new File();
|
|
63
|
+
case 'memory':
|
|
64
|
+
default:
|
|
65
|
+
return new Memory();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get singleton instance
|
|
71
|
+
*/
|
|
72
|
+
static async getInstance () {
|
|
73
|
+
if (!SessionManager.instance) {
|
|
74
|
+
SessionManager.instance = await SessionManager.createInstance();
|
|
75
|
+
}
|
|
76
|
+
return SessionManager.instance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ========================================
|
|
80
|
+
// Session Lifecycle
|
|
81
|
+
// ========================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load existing session or create a new one
|
|
85
|
+
*
|
|
86
|
+
* Validates cookie signature, checks session lifetime,
|
|
87
|
+
* and retrieves session data from the store.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} request - Fastify request object
|
|
90
|
+
* @param {object} response - Fastify response object
|
|
91
|
+
* @returns {Session} Session instance
|
|
92
|
+
*/
|
|
93
|
+
async load (request, response) {
|
|
94
|
+
// Wait for store initialization (File driver directory setup)
|
|
95
|
+
await this.ready;
|
|
96
|
+
|
|
97
|
+
const signedCookie = request.cookies[this.config.cookieName];
|
|
98
|
+
|
|
99
|
+
// No cookie → new session
|
|
100
|
+
if (!signedCookie) {
|
|
101
|
+
return this.create(response);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Verify cookie signature
|
|
105
|
+
const unsignResult = request.unsignCookie(signedCookie);
|
|
106
|
+
if (!unsignResult.valid) {
|
|
107
|
+
response.clearCookie(this.config.cookieName);
|
|
108
|
+
|
|
109
|
+
return this.create(response);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sessionId = unsignResult.value;
|
|
113
|
+
const sessionData = await this.store.get(sessionId);
|
|
114
|
+
|
|
115
|
+
// Session not found → new session
|
|
116
|
+
if (!sessionData) {
|
|
117
|
+
response.clearCookie(this.config.cookieName);
|
|
118
|
+
|
|
119
|
+
return this.create(response);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if session expired (lifetime)
|
|
123
|
+
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
|
+
|
|
128
|
+
return this.create(response);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Session(sessionId, sessionData);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create new session
|
|
136
|
+
* @param {object} response - Fastify response
|
|
137
|
+
* @returns {Session} New session instance
|
|
138
|
+
*/
|
|
139
|
+
async create (response) {
|
|
140
|
+
const id = crypto.randomBytes(32).toString("hex");
|
|
141
|
+
const sessionData = {
|
|
142
|
+
data: {},
|
|
143
|
+
createdAt: Date.now(),
|
|
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);
|
|
151
|
+
|
|
152
|
+
return new Session(id, sessionData);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ========================================
|
|
156
|
+
// Garbage Collection
|
|
157
|
+
// ========================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start automatic garbage collection (every 15 minutes)
|
|
161
|
+
*/
|
|
162
|
+
startGarbageCollection () {
|
|
163
|
+
if (typeof this.store.gc !== 'function') {
|
|
164
|
+
return; // Store doesn't support GC
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const gcInterval = 15 * 60 * 1000; // 15 minutes
|
|
168
|
+
|
|
169
|
+
this.gcTimer = setInterval(async () => {
|
|
170
|
+
try {
|
|
171
|
+
const deleted = await this.store.gc(this.config.lifetime);
|
|
172
|
+
if (deleted > 0) {
|
|
173
|
+
console.log(`[Session GC] Deleted ${deleted} expired sessions`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error('[Session GC] Error:', error.message);
|
|
178
|
+
}
|
|
179
|
+
}, gcInterval);
|
|
180
|
+
|
|
181
|
+
// Run immediately on startup
|
|
182
|
+
this.store.gc(this.config.lifetime).catch(err => {
|
|
183
|
+
console.error('[Session GC] Initial run failed:', err.message);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Stop garbage collection (graceful shutdown)
|
|
189
|
+
*/
|
|
190
|
+
stopGarbageCollection () {
|
|
191
|
+
if (this.gcTimer) {
|
|
192
|
+
clearInterval(this.gcTimer);
|
|
193
|
+
this.gcTimer = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ========================================
|
|
198
|
+
// Server Setup
|
|
199
|
+
// ========================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Setup session middleware for Fastify server
|
|
203
|
+
* @param {object} server - Fastify instance
|
|
204
|
+
*/
|
|
205
|
+
static async setup (server) {
|
|
206
|
+
const manager = await SessionManager.getInstance();
|
|
207
|
+
|
|
208
|
+
server.decorateRequest("session", null);
|
|
209
|
+
|
|
210
|
+
// Load session before handling request
|
|
211
|
+
server.addHook('preHandler', async (request, response) => {
|
|
212
|
+
request.session = await manager.load(request, response);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Handle cookie changes before response is sent
|
|
216
|
+
server.addHook('onSend', async (request, response, payload) => {
|
|
217
|
+
if (!request.session) return payload;
|
|
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
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return payload;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Persist session after sending response
|
|
232
|
+
server.addHook('onResponse', async (request, response) => {
|
|
233
|
+
if (!request.session) return;
|
|
234
|
+
|
|
235
|
+
// Skip persistence for error responses (4xx/5xx)
|
|
236
|
+
if (response.statusCode >= 400) return;
|
|
237
|
+
|
|
238
|
+
// Handle regeneration - delete old session
|
|
239
|
+
if (request.session.shouldRegenerate()) {
|
|
240
|
+
const oldId = request.session.getOldId();
|
|
241
|
+
if (oldId) {
|
|
242
|
+
await manager.store.delete(oldId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Persist session data
|
|
247
|
+
await manager.store.set(request.session.id, {
|
|
248
|
+
data: request.session.all(),
|
|
249
|
+
createdAt: request.session.createdAt,
|
|
250
|
+
lastActivity: Date.now(),
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Start garbage collection
|
|
255
|
+
manager.startGarbageCollection();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default SessionManager;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Session Store
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
class Memory {
|
|
14
|
+
constructor () {
|
|
15
|
+
this.sessions = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get session data by ID
|
|
20
|
+
* @param {string} id - Session ID
|
|
21
|
+
* @returns {object|null} Session data or null if not found
|
|
22
|
+
*/
|
|
23
|
+
async get (id) {
|
|
24
|
+
return this.sessions.get(id) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Save session data
|
|
29
|
+
* @param {string} id - Session ID
|
|
30
|
+
* @param {object} value - Session data
|
|
31
|
+
*/
|
|
32
|
+
async set (id, value) {
|
|
33
|
+
this.sessions.set(id, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Delete session
|
|
38
|
+
* @param {string} id - Session ID
|
|
39
|
+
* @returns {boolean} True if deleted
|
|
40
|
+
*/
|
|
41
|
+
async delete (id) {
|
|
42
|
+
return this.sessions.delete(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Garbage collection - remove expired sessions
|
|
47
|
+
* @param {number} lifetime - Session lifetime in milliseconds
|
|
48
|
+
* @returns {number} Number of deleted sessions
|
|
49
|
+
*/
|
|
50
|
+
async gc (lifetime) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
let deleted = 0;
|
|
53
|
+
|
|
54
|
+
for (const [id, sessionData] of this.sessions.entries()) {
|
|
55
|
+
const lastActivity = sessionData.lastActivity || sessionData.createdAt;
|
|
56
|
+
|
|
57
|
+
if (now - lastActivity > lifetime) {
|
|
58
|
+
this.sessions.delete(id);
|
|
59
|
+
deleted++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return deleted;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default Memory;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session
|
|
5
|
+
*
|
|
6
|
+
* Represents a single user session with secure data storage.
|
|
7
|
+
* All data is private and accessed through controlled methods.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Private data storage (no external mutation)
|
|
11
|
+
* - CSRF token generation and verification
|
|
12
|
+
* - Flash messages (one-time messages)
|
|
13
|
+
* - Session regeneration (security)
|
|
14
|
+
* - Session destruction (logout)
|
|
15
|
+
*/
|
|
16
|
+
class Session {
|
|
17
|
+
// Private fields
|
|
18
|
+
#id;
|
|
19
|
+
#data;
|
|
20
|
+
#createdAt;
|
|
21
|
+
#lastActivity;
|
|
22
|
+
#flags = {
|
|
23
|
+
shouldRegenerate: false,
|
|
24
|
+
oldId: null
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
constructor (id, sessionData) {
|
|
28
|
+
this.#id = id;
|
|
29
|
+
this.#data = sessionData.data || {};
|
|
30
|
+
this.#createdAt = sessionData.createdAt || Date.now();
|
|
31
|
+
this.#lastActivity = sessionData.lastActivity || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ========================================
|
|
35
|
+
// Getters
|
|
36
|
+
// ========================================
|
|
37
|
+
|
|
38
|
+
get id () {
|
|
39
|
+
return this.#id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get createdAt () {
|
|
43
|
+
return this.#createdAt;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get lastActivity () {
|
|
47
|
+
return this.#lastActivity;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ========================================
|
|
51
|
+
// Data Management
|
|
52
|
+
// ========================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get a session value
|
|
56
|
+
*/
|
|
57
|
+
get (key) {
|
|
58
|
+
return this.#data[key];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set a session value
|
|
63
|
+
*/
|
|
64
|
+
set (key, value) {
|
|
65
|
+
this.#data[key] = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all session data (copy to prevent mutations)
|
|
70
|
+
*/
|
|
71
|
+
all () {
|
|
72
|
+
return { ...this.#data };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ========================================
|
|
76
|
+
// CSRF Protection
|
|
77
|
+
// ========================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate new CSRF token
|
|
81
|
+
* @returns {string} Generated token
|
|
82
|
+
*/
|
|
83
|
+
generateCsrfToken () {
|
|
84
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
85
|
+
this.set('_csrf', token);
|
|
86
|
+
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get CSRF token (generate if doesn't exist)
|
|
92
|
+
* @returns {string} CSRF token
|
|
93
|
+
*/
|
|
94
|
+
getCsrfToken () {
|
|
95
|
+
return this.get('_csrf') || this.generateCsrfToken();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Verify CSRF token (timing-safe comparison)
|
|
100
|
+
* @param {string} token - Token to verify
|
|
101
|
+
* @returns {boolean} True if valid
|
|
102
|
+
*/
|
|
103
|
+
verifyCsrfToken (token) {
|
|
104
|
+
const sessionToken = this.get('_csrf');
|
|
105
|
+
|
|
106
|
+
if (!sessionToken || !token) return false;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return crypto.timingSafeEqual(
|
|
110
|
+
Buffer.from(sessionToken, 'hex'),
|
|
111
|
+
Buffer.from(token, 'hex')
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ========================================
|
|
120
|
+
// Flash Messages
|
|
121
|
+
// ========================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Store a flash message (available only for the next request)
|
|
125
|
+
* @param {string} key - Message key
|
|
126
|
+
* @param {any} value - Message value
|
|
127
|
+
*/
|
|
128
|
+
flash (key, value) {
|
|
129
|
+
if (!this.#data._flash) {
|
|
130
|
+
this.#data._flash = {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.#data._flash[key] = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get and remove a flash message
|
|
138
|
+
* @param {string} key - Message key
|
|
139
|
+
* @returns {any} Message value or undefined
|
|
140
|
+
*/
|
|
141
|
+
getFlash (key) {
|
|
142
|
+
if (!this.#data._flash?.[key]) return undefined;
|
|
143
|
+
|
|
144
|
+
const value = this.#data._flash[key];
|
|
145
|
+
|
|
146
|
+
delete this.#data._flash[key];
|
|
147
|
+
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ========================================
|
|
152
|
+
// Lifecycle Management
|
|
153
|
+
// ========================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Regenerate session ID and CSRF token
|
|
157
|
+
*
|
|
158
|
+
* Prevents session fixation attacks by creating a new session ID.
|
|
159
|
+
* Call this after login, logout, or any privilege escalation.
|
|
160
|
+
*
|
|
161
|
+
* @returns {object} {oldId, newId}
|
|
162
|
+
*/
|
|
163
|
+
regenerate () {
|
|
164
|
+
// Guard: Prevent concurrent regeneration (idempotent)
|
|
165
|
+
if (this.#flags.shouldRegenerate) {
|
|
166
|
+
return {
|
|
167
|
+
oldId: this.#flags.oldId,
|
|
168
|
+
newId: this.#id
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.#flags.oldId = this.#id;
|
|
173
|
+
this.#id = crypto.randomBytes(32).toString('hex');
|
|
174
|
+
this.#flags.shouldRegenerate = true;
|
|
175
|
+
this.generateCsrfToken();
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
oldId: this.#flags.oldId,
|
|
179
|
+
newId: this.#id
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ========================================
|
|
184
|
+
// Internal Flags (used by Manager)
|
|
185
|
+
// ========================================
|
|
186
|
+
|
|
187
|
+
shouldRegenerate () {
|
|
188
|
+
return this.#flags.shouldRegenerate;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getOldId () {
|
|
192
|
+
return this.#flags.oldId;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default Session;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
class Str {
|
|
2
|
+
static random(length) {
|
|
3
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
+
let result = "";
|
|
5
|
+
const charactersLength = characters.length;
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < length; i++) {
|
|
8
|
+
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static slug(text) {
|
|
15
|
+
const normalizedInput = text
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.normalize("NFD")
|
|
18
|
+
.replace(/[\u0300-\u036f]/g, "");
|
|
19
|
+
|
|
20
|
+
const slug = normalizedInput
|
|
21
|
+
.replace(/ /g, "-")
|
|
22
|
+
.replace(/ı/g, "i")
|
|
23
|
+
.replace(/[^\w-]+/g, "");
|
|
24
|
+
|
|
25
|
+
return slug;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static ucfirst(str) {
|
|
29
|
+
if (!str) return "";
|
|
30
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static lcfirst(str) {
|
|
34
|
+
if (!str) return "";
|
|
35
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static camel(str) {
|
|
39
|
+
return str
|
|
40
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
41
|
+
.replace(/^(.)/, (c) => c.toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static pascal(str) {
|
|
45
|
+
return str
|
|
46
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
47
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static snake(str) {
|
|
51
|
+
return str
|
|
52
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
53
|
+
.replace(/[-\s]+/g, "_")
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static kebab(str) {
|
|
58
|
+
return str
|
|
59
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
60
|
+
.replace(/[_\s]+/g, "-")
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static limit(str, length, suffix = "...") {
|
|
65
|
+
if (!str || str.length <= length) return str;
|
|
66
|
+
return str.substring(0, length) + suffix;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static words(str, count, suffix = "...") {
|
|
70
|
+
const wordsArray = str.split(/\s+/);
|
|
71
|
+
if (wordsArray.length <= count) return str;
|
|
72
|
+
return wordsArray.slice(0, count).join(" ") + suffix;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static contains(haystack, needle) {
|
|
76
|
+
return haystack.includes(needle);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static startsWith(haystack, needle) {
|
|
80
|
+
return haystack.startsWith(needle);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static endsWith(haystack, needle) {
|
|
84
|
+
return haystack.endsWith(needle);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static replaceFirst(str, search, replace) {
|
|
88
|
+
const pos = str.indexOf(search);
|
|
89
|
+
if (pos === -1) return str;
|
|
90
|
+
return str.substring(0, pos) + replace + str.substring(pos + search.length);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static replaceLast(str, search, replace) {
|
|
94
|
+
const pos = str.lastIndexOf(search);
|
|
95
|
+
if (pos === -1) return str;
|
|
96
|
+
return str.substring(0, pos) + replace + str.substring(pos + search.length);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default Str;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import Paths from "../Core/Paths.js";
|
|
4
|
+
|
|
5
|
+
class TranslationManager {
|
|
6
|
+
static #cache = new Map();
|
|
7
|
+
|
|
8
|
+
static get(req, key, params = null) {
|
|
9
|
+
const currentLang = req.language;
|
|
10
|
+
const translation = this.#getTranslation(currentLang, key);
|
|
11
|
+
|
|
12
|
+
if (params) {
|
|
13
|
+
return this.#replaceParams(translation, params);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return translation;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static #getTranslation(lang, key) {
|
|
20
|
+
const langPath = path.join(Paths.langs, lang + ".json");
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(langPath)) {
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let translations = this.#cache.get(lang);
|
|
27
|
+
|
|
28
|
+
if (!translations) {
|
|
29
|
+
const langFile = fs.readFileSync(langPath, { encoding: "utf-8", flag: "r" });
|
|
30
|
+
translations = JSON.parse(langFile);
|
|
31
|
+
this.#cache.set(lang, translations);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return translations[key] || key;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static #replaceParams(text, params) {
|
|
38
|
+
Object.keys(params).forEach((key) => {
|
|
39
|
+
text = text.replaceAll(":" + key, params[key]);
|
|
40
|
+
});
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static clearCache() {
|
|
45
|
+
this.#cache.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default TranslationManager;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const MIME_TYPES = {
|
|
2
|
+
jpg: "image/jpeg",
|
|
3
|
+
jpeg: "image/jpeg",
|
|
4
|
+
png: "image/png",
|
|
5
|
+
gif: "image/gif",
|
|
6
|
+
webp: "image/webp",
|
|
7
|
+
svg: "image/svg+xml",
|
|
8
|
+
bmp: "image/bmp",
|
|
9
|
+
ico: "image/x-icon",
|
|
10
|
+
pdf: "application/pdf",
|
|
11
|
+
doc: "application/msword",
|
|
12
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
13
|
+
xls: "application/vnd.ms-excel",
|
|
14
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
15
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
16
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
17
|
+
txt: "text/plain",
|
|
18
|
+
csv: "text/csv",
|
|
19
|
+
zip: "application/zip",
|
|
20
|
+
rar: "application/x-rar-compressed",
|
|
21
|
+
"7z": "application/x-7z-compressed",
|
|
22
|
+
tar: "application/x-tar",
|
|
23
|
+
gz: "application/gzip",
|
|
24
|
+
mp3: "audio/mpeg",
|
|
25
|
+
wav: "audio/wav",
|
|
26
|
+
ogg: "audio/ogg",
|
|
27
|
+
m4a: "audio/mp4",
|
|
28
|
+
mp4: "video/mp4",
|
|
29
|
+
avi: "video/x-msvideo",
|
|
30
|
+
mov: "video/quicktime",
|
|
31
|
+
wmv: "video/x-ms-wmv",
|
|
32
|
+
flv: "video/x-flv",
|
|
33
|
+
webm: "video/webm",
|
|
34
|
+
json: "application/json",
|
|
35
|
+
xml: "application/xml",
|
|
36
|
+
js: "application/javascript",
|
|
37
|
+
css: "text/css",
|
|
38
|
+
html: "text/html"
|
|
39
|
+
};
|