free-framework 4.4.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/LICENSE +21 -0
- package/README.md +198 -0
- package/bin/free.js +118 -0
- package/cli/commands/build.js +124 -0
- package/cli/commands/deploy.js +143 -0
- package/cli/commands/devtools.js +210 -0
- package/cli/commands/doctor.js +72 -0
- package/cli/commands/install.js +28 -0
- package/cli/commands/make.js +74 -0
- package/cli/commands/migrate.js +67 -0
- package/cli/commands/new.js +54 -0
- package/cli/commands/serve.js +73 -0
- package/cli/commands/test.js +57 -0
- package/compiler/analyzer.js +102 -0
- package/compiler/generator.js +386 -0
- package/compiler/lexer.js +166 -0
- package/compiler/parser.js +410 -0
- package/database/model.js +6 -0
- package/database/orm.js +379 -0
- package/database/query-builder.js +179 -0
- package/index.js +51 -0
- package/package.json +80 -0
- package/plugins/auth.js +212 -0
- package/plugins/cache.js +85 -0
- package/plugins/chat.js +32 -0
- package/plugins/mail.js +53 -0
- package/plugins/metrics.js +126 -0
- package/plugins/payments.js +59 -0
- package/plugins/queue.js +139 -0
- package/plugins/search.js +51 -0
- package/plugins/storage.js +123 -0
- package/plugins/upload.js +62 -0
- package/router/router.js +57 -0
- package/runtime/app.js +14 -0
- package/runtime/client.js +254 -0
- package/runtime/cluster.js +35 -0
- package/runtime/edge.js +62 -0
- package/runtime/middleware/maintenance.js +54 -0
- package/runtime/middleware/security.js +30 -0
- package/runtime/server.js +130 -0
- package/runtime/validator.js +102 -0
- package/runtime/views/error.free +104 -0
- package/runtime/views/maintenance.free +0 -0
- package/template-engine/renderer.js +24 -0
- package/templates/app-template/.env +23 -0
- package/templates/app-template/app/Exceptions/Handler.js +65 -0
- package/templates/app-template/app/Http/Controllers/AuthController.free +91 -0
- package/templates/app-template/app/Http/Middleware/AuthGuard.js +46 -0
- package/templates/app-template/app/Services/Storage.js +75 -0
- package/templates/app-template/app/Services/Validator.js +91 -0
- package/templates/app-template/app/controllers/AuthController.free +42 -0
- package/templates/app-template/app/middleware/auth.js +25 -0
- package/templates/app-template/app/models/User.free +32 -0
- package/templates/app-template/app/routes.free +12 -0
- package/templates/app-template/app/styles.css +23 -0
- package/templates/app-template/app/views/counter.free +23 -0
- package/templates/app-template/app/views/header.free +13 -0
- package/templates/app-template/config/app.js +32 -0
- package/templates/app-template/config/auth.js +39 -0
- package/templates/app-template/config/database.js +54 -0
- package/templates/app-template/package.json +28 -0
- package/templates/app-template/resources/css/app.css +11 -0
- package/templates/app-template/resources/views/dashboard.free +25 -0
- package/templates/app-template/resources/views/home.free +26 -0
- package/templates/app-template/routes/api.free +22 -0
- package/templates/app-template/routes/web.free +25 -0
- package/templates/app-template/tailwind.config.js +21 -0
- package/templates/app-template/views/about.ejs +47 -0
- package/templates/app-template/views/home.ejs +52 -0
- package/templates/auth/login.html +144 -0
- package/templates/auth/register.html +146 -0
- package/utils/logger.js +20 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime/views/error.free
|
|
3
|
+
*
|
|
4
|
+
* Premium System Diagnostic & Error Interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
component Error {
|
|
8
|
+
style {
|
|
9
|
+
:root {
|
|
10
|
+
--error-primary: #ff3366;
|
|
11
|
+
--error-glow: rgba(255, 51, 102, 0.4);
|
|
12
|
+
}
|
|
13
|
+
.error-container {
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
background: #050505;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
padding: 2rem;
|
|
20
|
+
}
|
|
21
|
+
.error-card {
|
|
22
|
+
max-width: 800px;
|
|
23
|
+
width: 100%;
|
|
24
|
+
background: rgba(15, 15, 15, 0.8);
|
|
25
|
+
backdrop-filter: blur(32px);
|
|
26
|
+
border: 1px solid rgba(255, 51, 102, 0.2);
|
|
27
|
+
border-radius: 32px;
|
|
28
|
+
padding: 4rem;
|
|
29
|
+
box-shadow: 0 40px 100px rgba(0, 0, 0, 0.8), 0 0 40px var(--error-glow);
|
|
30
|
+
text-align: center;
|
|
31
|
+
}
|
|
32
|
+
.error-code {
|
|
33
|
+
font-size: 8rem;
|
|
34
|
+
font-weight: 950;
|
|
35
|
+
line-height: 1;
|
|
36
|
+
background: linear-gradient(135deg, #ff3366, #ff8833);
|
|
37
|
+
-webkit-background-clip: text;
|
|
38
|
+
-webkit-text-fill-color: transparent;
|
|
39
|
+
letter-spacing: -0.05em;
|
|
40
|
+
margin-bottom: 2rem;
|
|
41
|
+
}
|
|
42
|
+
.stack-trace {
|
|
43
|
+
text-align: left;
|
|
44
|
+
background: rgba(0, 0, 0, 0.3);
|
|
45
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
46
|
+
border-radius: 16px;
|
|
47
|
+
padding: 2rem;
|
|
48
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
49
|
+
font-size: 0.75rem;
|
|
50
|
+
color: rgba(255, 255, 255, 0.4);
|
|
51
|
+
overflow-x: auto;
|
|
52
|
+
margin: 2rem 0;
|
|
53
|
+
max-height: 300px;
|
|
54
|
+
}
|
|
55
|
+
.btn-return {
|
|
56
|
+
display: inline-block;
|
|
57
|
+
padding: 1rem 2.5rem;
|
|
58
|
+
background: white;
|
|
59
|
+
color: black;
|
|
60
|
+
font-weight: 900;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
letter-spacing: 0.1em;
|
|
63
|
+
border-radius: 16px;
|
|
64
|
+
transition: 0.3s;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
}
|
|
67
|
+
.btn-return:hover {
|
|
68
|
+
background: var(--error-primary);
|
|
69
|
+
transform: scale(1.05);
|
|
70
|
+
box-shadow: 0 0 30px var(--error-glow);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
div class="error-container" {
|
|
75
|
+
div class="error-card animate-fade-in" {
|
|
76
|
+
div class="error-code" { text "{props.status}" }
|
|
77
|
+
|
|
78
|
+
h1 class="text-3xl font-black mb-4 tracking-tight" {
|
|
79
|
+
text "SYSTEM MALFUNCTION"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
p class="text-gray-400 text-lg mb-8" {
|
|
83
|
+
text "{props.message}"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
condition props.stack {
|
|
87
|
+
div class="stack-trace" {
|
|
88
|
+
loop props.stack line {
|
|
89
|
+
div { text "{line}" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
div class="flex gap-4 justify-center" {
|
|
95
|
+
link "GO BACK HOME" "/" class="btn-return"
|
|
96
|
+
link "DASHBOARD" "/dashboard" class="btn-return bg-transparent border border-white/10 text-white"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
div class="mt-12 pt-12 border-t border-white/5 opacity-30 text-[10px] font-bold tracking-widest uppercase" {
|
|
100
|
+
text "Free Ultra Diagnostics Engine v4.0.2"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* template-engine/renderer.js
|
|
3
|
+
* Simple renderer for the Free framework.
|
|
4
|
+
* Currently wraps EJS but provides a hook for custom syntax.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ejs = require("ejs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
class Renderer {
|
|
11
|
+
static async render(viewName, data = {}, options = {}) {
|
|
12
|
+
const viewsPath = process.env.VIEWS_PATH || path.join(process.cwd(), "views");
|
|
13
|
+
const filePath = path.join(viewsPath, `${viewName}.ejs`);
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
ejs.renderFile(filePath, data, options, (err, str) => {
|
|
17
|
+
if (err) return reject(err);
|
|
18
|
+
resolve(str);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { Renderer };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Free Framework Core Template Configuration
|
|
2
|
+
# Production-ready defaults for Database, Security, and Routing
|
|
3
|
+
|
|
4
|
+
# ── Database (MySQL Recommended) ──────────────────────────────────────────
|
|
5
|
+
DB_CLIENT=mysql2
|
|
6
|
+
DB_HOST=127.0.0.1
|
|
7
|
+
DB_PORT=3306
|
|
8
|
+
DB_USER=root
|
|
9
|
+
DB_PASS=
|
|
10
|
+
DB_NAME=free_db_default
|
|
11
|
+
|
|
12
|
+
# Connection Pooling Settings
|
|
13
|
+
DB_POOL_MIN=5
|
|
14
|
+
DB_POOL_MAX=20
|
|
15
|
+
|
|
16
|
+
# ── Security & Authentication ──────────────────────────────────────────────
|
|
17
|
+
# Change these secrets in production!
|
|
18
|
+
APP_KEY=base64:free_secret_key_change_me_in_production
|
|
19
|
+
JWT_SECRET=super_secret_jwt_key_that_should_be_long_and_random
|
|
20
|
+
|
|
21
|
+
# ── Server configuration ───────────────────────────────────────────────────
|
|
22
|
+
PORT=3000
|
|
23
|
+
NODE_ENV=development
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/Exceptions/Handler.js
|
|
3
|
+
*
|
|
4
|
+
* --------------------------------------------------------------------------
|
|
5
|
+
* Global Exception Handler
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* This file acts as a centralized "catch-all" for any errors thrown in your
|
|
9
|
+
* application. Instead of crashing the node process or showing users a
|
|
10
|
+
* raw stack trace, this handler elegantly formats the response based on the
|
|
11
|
+
* environment (production vs local) and logs it to storage/logs/.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const config = require('../../config/app');
|
|
17
|
+
|
|
18
|
+
module.exports = function GlobalExceptionHandler(err, req, res) {
|
|
19
|
+
// 1. Determine HTTP Status Code (Default to 500 Internal Server Error)
|
|
20
|
+
const status = err.status || 500;
|
|
21
|
+
|
|
22
|
+
// 2. Log the error to storage/logs/free.log immediately
|
|
23
|
+
try {
|
|
24
|
+
const logDir = path.join(process.cwd(), 'storage/logs');
|
|
25
|
+
if (!fs.existsSync(logDir)) {
|
|
26
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timestamp = new Date().toISOString();
|
|
30
|
+
const logMessage = `[${timestamp}] [${req.method} ${req.url}] HTTP ${status}: ${err.message}\n${err.stack || ''}\n\n`;
|
|
31
|
+
|
|
32
|
+
fs.appendFileSync(path.join(logDir, 'free.log'), logMessage);
|
|
33
|
+
} catch (logErr) {
|
|
34
|
+
console.error('CRITICAL: Failed to write to storage/logs/free.log', logErr);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Format the Response (Never show stack traces in production!)
|
|
38
|
+
const isProduction = config.env === 'production' && !config.debug;
|
|
39
|
+
|
|
40
|
+
// Set response headers
|
|
41
|
+
if (!res.headersSent) {
|
|
42
|
+
res.status(status);
|
|
43
|
+
res.header('Content-Type', 'application/json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Special handling for Validation Errors (422)
|
|
47
|
+
if (status === 422 && err.errors) {
|
|
48
|
+
return res.send(JSON.stringify({
|
|
49
|
+
message: 'Unprocessable Entity',
|
|
50
|
+
errors: err.errors
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Standard Error Response
|
|
55
|
+
const responsePayload = {
|
|
56
|
+
message: isProduction && status >= 500 ? 'Internal Server Error' : err.message,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Append full stack trace only if debug mode is ON
|
|
60
|
+
if (!isProduction && err.stack) {
|
|
61
|
+
responsePayload.stack = err.stack.split('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
res.send(JSON.stringify(responsePayload));
|
|
65
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/Http/Controllers/AuthController.free
|
|
3
|
+
*
|
|
4
|
+
* --------------------------------------------------------------------------
|
|
5
|
+
* Authentication Controller
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* This controller handles massive-scale user registration and login.
|
|
9
|
+
* It strictly adheres to enterprise security standards:
|
|
10
|
+
* 1. Plain text passwords are NEVER saved. (bcrypt is used).
|
|
11
|
+
* 2. Sessions are stateless and highly scalable via JSON Web Tokens (JWT).
|
|
12
|
+
*
|
|
13
|
+
* @security Ensure the JWT_SECRET in config/auth.js is extremely strong.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Handles new user registration via POST /api/register
|
|
17
|
+
action register {
|
|
18
|
+
// 1. Validate incoming data
|
|
19
|
+
const { name, email, password } = body;
|
|
20
|
+
if (!name || !email || !password) {
|
|
21
|
+
throw new Error("Missing required fields: name, email, password");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Hash the password using bcrypt dynamically
|
|
25
|
+
// The salt rounds are pulled from config (defaults to 10 for performance/security balance)
|
|
26
|
+
const bcrypt = require('bcryptjs');
|
|
27
|
+
const authConfig = require('../../config/auth');
|
|
28
|
+
const hashedPassword = await bcrypt.hash(password, authConfig.bcrypt_rounds || 10);
|
|
29
|
+
|
|
30
|
+
// 3. Save the new user to the database
|
|
31
|
+
// The ORM's 'create' method automatically sanitizes inputs to prevent SQL Injection
|
|
32
|
+
const user = await User.create( { name, email, password: hashedPassword });
|
|
33
|
+
|
|
34
|
+
// 4. Generate the JWT stateless token
|
|
35
|
+
const jwt = require('jsonwebtoken');
|
|
36
|
+
const payload = { id: user.id, email: user.email, role: user.role };
|
|
37
|
+
const secret = authConfig.jwt_secret;
|
|
38
|
+
const options = { expiresIn: authConfig.jwt_expires_in || '7d' };
|
|
39
|
+
|
|
40
|
+
const token = jwt.sign(payload, secret, options);
|
|
41
|
+
|
|
42
|
+
// 5. Return success. CRITICAL: Never return the password hash in the response!
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
message: "User registered securely.",
|
|
46
|
+
token,
|
|
47
|
+
user: { id: user.id, name: user.name, email: user.email }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handles user login via POST /api/login
|
|
52
|
+
action login {
|
|
53
|
+
// 1. Validate inputs
|
|
54
|
+
const { email, password } = body;
|
|
55
|
+
if (!email || !password) {
|
|
56
|
+
throw new Error("Missing credentials");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Look up the user by email (SQL Injection Proof via ORM query builder)
|
|
60
|
+
const userQuery = await User.query().where( { email }).get();
|
|
61
|
+
const user = userQuery[0];
|
|
62
|
+
|
|
63
|
+
if (!user) {
|
|
64
|
+
throw new Error("Invalid credentials");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Prevent Timing Attacks & Verify Hash
|
|
68
|
+
// We strictly use bcrypt.compare instead of manual string comparison
|
|
69
|
+
const bcrypt = require('bcryptjs');
|
|
70
|
+
const isMatch = await bcrypt.compare(password, user.password);
|
|
71
|
+
|
|
72
|
+
if (!isMatch) {
|
|
73
|
+
throw new Error("Invalid credentials");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Issue the secure JWT Token
|
|
77
|
+
const authConfig = require('../../config/auth');
|
|
78
|
+
const jwt = require('jsonwebtoken');
|
|
79
|
+
const token = jwt.sign(
|
|
80
|
+
{ id: user.id, email: user.email, role: user.role },
|
|
81
|
+
authConfig.jwt_secret,
|
|
82
|
+
{ expiresIn: authConfig.jwt_expires_in || '7d' }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 5. Return the payload for the frontend to store (e.g., in localStorage or cookies)
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
token,
|
|
89
|
+
user: { id: user.id, name: user.name, email: user.email }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/Http/Middleware/AuthGuard.js
|
|
3
|
+
*
|
|
4
|
+
* --------------------------------------------------------------------------
|
|
5
|
+
* JWT Authentication Guard
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* This middleware protects routes from unauthenticated access.
|
|
9
|
+
* It intercepts incoming HTTP requests, extracts the Bearer token from the
|
|
10
|
+
* Authorization header, and verifies its cryptographic signature using jsonwebtoken.
|
|
11
|
+
*
|
|
12
|
+
* Usage: Attach this middleware to any sensitive API or Web route.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const jwt = require('jsonwebtoken');
|
|
16
|
+
const authConfig = require('../../../config/auth');
|
|
17
|
+
|
|
18
|
+
module.exports = async function AuthGuard(req, res, next) {
|
|
19
|
+
// 1. Extract the token from the "Authorization: Bearer <token>" header
|
|
20
|
+
const authHeader = req.headers['authorization'];
|
|
21
|
+
const token = authHeader && authHeader.split(' ')[1];
|
|
22
|
+
|
|
23
|
+
// 2. Reject immediately if no token is found (401 Unauthorized)
|
|
24
|
+
if (!token) {
|
|
25
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
26
|
+
res.end(JSON.stringify({ error: 'Unauthorized: No secure token provided' }));
|
|
27
|
+
return; // HALT the request lifecycle
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 3. Cryptographically verify the token's validity and expiration
|
|
32
|
+
// If the token was tampered with, this will throw an error immediately.
|
|
33
|
+
const decoded = jwt.verify(token, authConfig.jwt_secret);
|
|
34
|
+
|
|
35
|
+
// 4. Attach the decoded user payload securely to the request object
|
|
36
|
+
// so that subsequent controllers can access `req.user.id`, etc.
|
|
37
|
+
req.user = decoded;
|
|
38
|
+
|
|
39
|
+
// 5. Allow the request to proceed to the next middleware or controller
|
|
40
|
+
next();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Token was invalid, expired, or tampered with (403 Forbidden)
|
|
43
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
44
|
+
res.end(JSON.stringify({ error: 'Forbidden: Invalid or expired authentication token' }));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/Services/Storage.js
|
|
3
|
+
*
|
|
4
|
+
* --------------------------------------------------------------------------
|
|
5
|
+
* Enterprise File Storage & Uploads
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* This service provides a clean Laravel-like API for saving uploaded files
|
|
9
|
+
* directly to the 'storage/uploads' directory.
|
|
10
|
+
*
|
|
11
|
+
* Example usage in a controller:
|
|
12
|
+
* const path = await Storage.put('avatars', req.file);
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
class Storage {
|
|
20
|
+
/**
|
|
21
|
+
* Internal method to ensure the storage directory exists.
|
|
22
|
+
*/
|
|
23
|
+
static _ensureDir(subFolder) {
|
|
24
|
+
const dest = path.join(process.cwd(), 'storage/uploads', subFolder || '');
|
|
25
|
+
if (!fs.existsSync(dest)) {
|
|
26
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
return dest;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Saves a raw buffer or a multipart file object to the disk.
|
|
33
|
+
* @param {string} directory - The sub-directory (e.g., 'avatars').
|
|
34
|
+
* @param {Object|Buffer} file - The file data. If using HyperExpress multipart, it should be the file stream/buffer.
|
|
35
|
+
* @param {string} [filename] - Optional custom name. Otherwise, a secure random name is generated.
|
|
36
|
+
* @returns {string} The relative path to the saved file.
|
|
37
|
+
*/
|
|
38
|
+
static async put(directory, fileBuffer, filename = null) {
|
|
39
|
+
const destDir = this._ensureDir(directory);
|
|
40
|
+
|
|
41
|
+
// Generate a cryptographically secure random filename if none provided
|
|
42
|
+
// This prevents overwriting files and Directory Traversal attacks.
|
|
43
|
+
const safeFilename = filename || crypto.randomBytes(16).toString('hex') + '.bin';
|
|
44
|
+
const finalPath = path.join(destDir, safeFilename);
|
|
45
|
+
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
fs.writeFile(finalPath, fileBuffer, (err) => {
|
|
48
|
+
if (err) return reject(err);
|
|
49
|
+
// Return a relative path that can be stored in the database
|
|
50
|
+
resolve(`storage/uploads/${directory ? directory + '/' : ''}${safeFilename}`);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Checks if a file exists.
|
|
57
|
+
*/
|
|
58
|
+
static exists(filepath) {
|
|
59
|
+
return fs.existsSync(path.join(process.cwd(), filepath));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Deletes a file.
|
|
64
|
+
*/
|
|
65
|
+
static async delete(filepath) {
|
|
66
|
+
const fullPath = path.join(process.cwd(), filepath);
|
|
67
|
+
if (fs.existsSync(fullPath)) {
|
|
68
|
+
fs.unlinkSync(fullPath);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = Storage;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/Services/Validator.js
|
|
3
|
+
*
|
|
4
|
+
* --------------------------------------------------------------------------
|
|
5
|
+
* Enterprise Input Validation
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* This service provides Laravel-style input validation.
|
|
9
|
+
* Use it to ensure incoming request data is completely sanitized and correctly
|
|
10
|
+
* formatted before it ever touches your Controllers or Database.
|
|
11
|
+
*
|
|
12
|
+
* Example usage:
|
|
13
|
+
* const validated = Validator.make(req.body, { email: 'required|email' });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
class Validator {
|
|
17
|
+
/**
|
|
18
|
+
* Core validation engine.
|
|
19
|
+
* @param {Object} data - The incoming data (e.g., req.body)
|
|
20
|
+
* @param {Object} rules - The validation rules (e.g., { name: 'required|string', age: 'number|min:18' })
|
|
21
|
+
* @throws {Error} - Throws a detailed 422 Unprocessable Entity error if validation fails.
|
|
22
|
+
* @returns {Object} - The sanitized, validated data.
|
|
23
|
+
*/
|
|
24
|
+
static make(data, rules) {
|
|
25
|
+
let errors = {};
|
|
26
|
+
let validatedData = {};
|
|
27
|
+
|
|
28
|
+
for (const [field, ruleString] of Object.entries(rules)) {
|
|
29
|
+
const fieldRules = ruleString.split('|');
|
|
30
|
+
const value = data[field];
|
|
31
|
+
validatedData[field] = value; // Keep the value if it passes
|
|
32
|
+
|
|
33
|
+
for (const rule of fieldRules) {
|
|
34
|
+
// 1. Required Rule
|
|
35
|
+
if (rule === 'required' && (value === undefined || value === null || value === '')) {
|
|
36
|
+
if (!errors[field]) errors[field] = [];
|
|
37
|
+
errors[field].push(`The ${field} field is required.`);
|
|
38
|
+
break; // Stop further validation for this field if it's missing
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Skip other rules if the field is empty but not required
|
|
42
|
+
if (value === undefined || value === null || value === '') continue;
|
|
43
|
+
|
|
44
|
+
// 2. Email Rule
|
|
45
|
+
if (rule === 'email') {
|
|
46
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
47
|
+
if (!emailRegex.test(value)) {
|
|
48
|
+
if (!errors[field]) errors[field] = [];
|
|
49
|
+
errors[field].push(`The ${field} must be a valid email address.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. String Rule
|
|
54
|
+
if (rule === 'string' && typeof value !== 'string') {
|
|
55
|
+
if (!errors[field]) errors[field] = [];
|
|
56
|
+
errors[field].push(`The ${field} must be a string.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Number Rule
|
|
60
|
+
if (rule === 'number' && isNaN(Number(value))) {
|
|
61
|
+
if (!errors[field]) errors[field] = [];
|
|
62
|
+
errors[field].push(`The ${field} must be a number.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 5. Min/Max Length or Value Rules (e.g., min:8, max:255)
|
|
66
|
+
if (rule.startsWith('min:')) {
|
|
67
|
+
const min = parseFloat(rule.split(':')[1]);
|
|
68
|
+
if (typeof value === 'string' && value.length < min) {
|
|
69
|
+
if (!errors[field]) errors[field] = [];
|
|
70
|
+
errors[field].push(`The ${field} must be at least ${min} characters.`);
|
|
71
|
+
} else if (typeof value === 'number' && value < min) {
|
|
72
|
+
if (!errors[field]) errors[field] = [];
|
|
73
|
+
errors[field].push(`The ${field} must be at least ${min}.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Object.keys(errors).length > 0) {
|
|
80
|
+
// Throw a custom validation error that the Global Handler will catch
|
|
81
|
+
const error = new Error('Validation Failed');
|
|
82
|
+
error.status = 422;
|
|
83
|
+
error.errors = errors;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return validatedData;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = Validator;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthController.free
|
|
3
|
+
* Default Authentication Logic (Login, Register).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
action register {
|
|
7
|
+
const { name, email, password } = body;
|
|
8
|
+
if (!name || !email || !password) throw new Error("Missing credentials");
|
|
9
|
+
|
|
10
|
+
const bcrypt = require('bcryptjs');
|
|
11
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
12
|
+
|
|
13
|
+
const user = await User.create( { name, email, password: hashedPassword });
|
|
14
|
+
|
|
15
|
+
// Generate JWT
|
|
16
|
+
const jwt = require('jsonwebtoken');
|
|
17
|
+
const secret = process.env.JWT_SECRET || 'super_secret_jwt_key_that_should_be_long_and_random';
|
|
18
|
+
const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, secret, { expiresIn: '7d' });
|
|
19
|
+
|
|
20
|
+
return { success: true, token, user: { id: user.id, name: user.name, email: user.email } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
action login {
|
|
24
|
+
const { email, password } = body;
|
|
25
|
+
if (!email || !password) throw new Error("Missing credentials");
|
|
26
|
+
|
|
27
|
+
const userQuery = await User.query().where( { email }).get();
|
|
28
|
+
const user = userQuery[0];
|
|
29
|
+
|
|
30
|
+
if (!user) throw new Error("Invalid credentials");
|
|
31
|
+
|
|
32
|
+
const bcrypt = require('bcryptjs');
|
|
33
|
+
const isMatch = await bcrypt.compare(password, user.password);
|
|
34
|
+
if (!isMatch) throw new Error("Invalid credentials");
|
|
35
|
+
|
|
36
|
+
// Generate JWT
|
|
37
|
+
const jwt = require('jsonwebtoken');
|
|
38
|
+
const secret = process.env.JWT_SECRET || 'super_secret_jwt_key_that_should_be_long_and_random';
|
|
39
|
+
const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, secret, { expiresIn: '7d' });
|
|
40
|
+
|
|
41
|
+
return { success: true, token, user: { id: user.id, name: user.name, email: user.email } };
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/middleware/auth.js
|
|
3
|
+
* Default JWT Authentication Guard
|
|
4
|
+
*/
|
|
5
|
+
const jwt = require('jsonwebtoken');
|
|
6
|
+
|
|
7
|
+
module.exports = async function authGuard(req, res, next) {
|
|
8
|
+
const authHeader = req.headers['authorization'];
|
|
9
|
+
const token = authHeader && authHeader.split(' ')[1];
|
|
10
|
+
|
|
11
|
+
if (!token) {
|
|
12
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
13
|
+
res.end(JSON.stringify({ error: 'Unauthorized: No token provided' }));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'super_secret_jwt_key_that_should_be_long_and_random');
|
|
19
|
+
req.user = decoded; // Attach user payload to request
|
|
20
|
+
next();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
23
|
+
res.end(JSON.stringify({ error: 'Forbidden: Invalid or expired token' }));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app/Models/User.free
|
|
3
|
+
*
|
|
4
|
+
* --------------------------------------------------------------------------
|
|
5
|
+
* User Model
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* The Free Framework ORM allows you to define database schemas and interact
|
|
9
|
+
* with rows as JavaScript objects. This file represents the 'users' table.
|
|
10
|
+
*
|
|
11
|
+
* Security: The 'password' field should ALWAYS be hashed before saving
|
|
12
|
+
* using bcrypt. Never store plain text passwords.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
model User {
|
|
16
|
+
// Unique identifier (Primary Key is automatically generated by the Framework)
|
|
17
|
+
|
|
18
|
+
// Standard string fields
|
|
19
|
+
name string
|
|
20
|
+
|
|
21
|
+
// The 'unique' modifier ensures the database prevents duplicate emails
|
|
22
|
+
email string unique
|
|
23
|
+
|
|
24
|
+
// The password hash. NEVER select this field to send to the frontend!
|
|
25
|
+
password string
|
|
26
|
+
|
|
27
|
+
// Role-Based Access Control (RBAC). Default is 'user', admins would be 'admin'
|
|
28
|
+
role string default "user"
|
|
29
|
+
|
|
30
|
+
// Framework automatically adds 'createdAt' and 'updatedAt' timestamps
|
|
31
|
+
timestamps
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Application Routes
|
|
3
|
+
* Powered by Free Framework MVC
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
get "/" -> Home
|
|
7
|
+
|
|
8
|
+
// Example of a protected view route (Optional Server-Side checking logic can go here)
|
|
9
|
+
get "/dashboard" {
|
|
10
|
+
// You can access req.user if middleware is attached globally or per route
|
|
11
|
+
return Dashboard;
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
body {
|
|
7
|
+
@apply bg-[#050505] text-white selection:bg-primary/30 font-sans;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@layer components {
|
|
12
|
+
.glass {
|
|
13
|
+
@apply bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.btn-premium {
|
|
17
|
+
@apply px-6 py-3 bg-gradient-to-r from-primary to-secondary text-black font-black rounded-xl hover:scale-105 active:scale-95 transition-all duration-300 shadow-lg shadow-primary/20;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.text-glow {
|
|
21
|
+
@apply bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent drop-shadow-[0_0_15px_rgba(0, 255, 136, 0.3)];
|
|
22
|
+
}
|
|
23
|
+
}
|