nibula 1.2.4 → 1.2.6
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/.eleventy.js +81 -81
- package/.eleventyignore +3 -3
- package/.github/workflows/publish.yml +37 -0
- package/CHANGELOG.md +179 -148
- package/LICENSE +169 -169
- package/NOTICE +4 -4
- package/README.md +120 -123
- package/bin/create.js +507 -507
- package/bin/nibula.js +317 -305
- package/docs/Assistant CLI.md +65 -65
- package/docs/Backend.md +295 -295
- package/docs/Components.md +84 -84
- package/docs/Creating pages.md +64 -64
- package/docs/Deploy.md +189 -189
- package/docs/Head and SEO.md +138 -138
- package/docs/Javascript.md +50 -50
- package/docs/Styling with SCSS.md +141 -141
- package/nginx.conf +75 -75
- package/package.json +1 -1
- package/src/backend/.htaccess +6 -6
- package/src/backend/_core/composer.json +5 -5
- package/src/backend/_core/composer.lock +492 -492
- package/src/backend/_core/index.js +267 -267
- package/src/backend/_core/index.php +147 -147
- package/src/backend/_core/init.js +52 -52
- package/src/backend/_core/init.php +33 -33
- package/src/backend/_core/modules/RateLimiter.js +58 -58
- package/src/backend/_core/modules/RateLimiter.php +30 -30
- package/src/backend/_core/modules/Response.js +59 -59
- package/src/backend/_core/modules/Response.php +48 -48
- package/src/backend/api/protected/example-protected.js +23 -23
- package/src/backend/api/protected/example-protected.php +16 -16
- package/src/backend/api/public/example-public.js +24 -24
- package/src/backend/api/public/example-public.php +16 -16
- package/src/backend/backend-node.service.example +30 -30
- package/src/backend/database/Database.js +46 -46
- package/src/backend/database/Database.php +23 -23
- package/src/backend/example.config.js +37 -37
- package/src/backend/example.config.php +27 -27
- package/src/backend/package.json +18 -18
- package/src/backend/web.config +16 -16
- package/src/frontend/.htaccess +51 -51
- package/src/frontend/404.njk +17 -17
- package/src/frontend/assets/brand/favicon.svg +54 -54
- package/src/frontend/assets/brand/logo.svg +54 -54
- package/src/frontend/components/global/footer.njk +25 -25
- package/src/frontend/components/global/header.njk +6 -6
- package/src/frontend/components/welcome.njk +114 -114
- package/src/frontend/data/site.json +48 -48
- package/src/frontend/index.njk +8 -8
- package/src/frontend/js/modules/exampleModule.js +2 -2
- package/src/frontend/js/pages/404.js +6 -6
- package/src/frontend/js/pages/homepage.js +6 -6
- package/src/frontend/layouts/base.njk +132 -132
- package/src/frontend/layouts/page-components.njk +13 -13
- package/src/frontend/llms.njk +17 -17
- package/src/frontend/robots.njk +7 -7
- package/src/frontend/scss/modules/_animations.scss +24 -24
- package/src/frontend/scss/modules/_footer.scss +27 -27
- package/src/frontend/scss/modules/_global.scss +47 -47
- package/src/frontend/scss/modules/_header.scss +27 -27
- package/src/frontend/scss/modules/_mobile.scss +29 -29
- package/src/frontend/scss/modules/_root.scss +34 -34
- package/src/frontend/scss/modules/_typography.scss +14 -14
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
- package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
- package/src/frontend/scss/modules/frameworks/_foundation.scss +138 -139
- package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
- package/src/frontend/scss/pages/404.scss +27 -27
- package/src/frontend/scss/pages/homepage.scss +22 -22
- package/src/frontend/sitemap.njk +17 -17
- package/src/frontend/ts/modules/exampleModule.ts +2 -2
- package/src/frontend/ts/pages/404.ts +6 -6
- package/src/frontend/ts/pages/homepage.ts +6 -6
- package/src/frontend/web.config +55 -55
- package/tools/config/messages.json +3 -1
- package/tools/res/templates/template.js +6 -6
- package/tools/res/templates/template.njk +8 -8
- package/tools/res/templates/template.scss +22 -22
- package/tools/res/templates/template.ts +6 -6
- package/tsconfig.json +24 -24
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
define('CORE_ACCESS', true);
|
|
6
|
-
define('CORE_PATH', __DIR__);
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Load dependencies and initial configuration.
|
|
10
|
-
*/
|
|
11
|
-
require_once __DIR__ . '/init.php';
|
|
12
|
-
require_once __DIR__ . '/modules/Response.php';
|
|
13
|
-
|
|
14
|
-
// =====================================================
|
|
15
|
-
// 1. REQUEST PARSING
|
|
16
|
-
// =====================================================
|
|
17
|
-
|
|
18
|
-
$method = $_SERVER['REQUEST_METHOD'];
|
|
19
|
-
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
20
|
-
|
|
21
|
-
$uri = rtrim(preg_replace('#^/api#', '', $uri), '/') ?: '/';
|
|
22
|
-
$parts = array_values(array_filter(explode('/', $uri)));
|
|
23
|
-
|
|
24
|
-
foreach ($parts as $part) {
|
|
25
|
-
if ($part === '..' || $part === '.') {
|
|
26
|
-
Response::error('Bad Request', 400);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// =====================================================
|
|
31
|
-
// 2. ENDPOINT RESOLUTION (ROUTING WITH SUBDIRECTORIES)
|
|
32
|
-
// =====================================================
|
|
33
|
-
|
|
34
|
-
$basePublic = __DIR__ . '/../api/public/';
|
|
35
|
-
$baseProtected = __DIR__ . '/../api/protected/';
|
|
36
|
-
|
|
37
|
-
$endpointFile = null;
|
|
38
|
-
$isProtected = false;
|
|
39
|
-
$requestParams = [];
|
|
40
|
-
|
|
41
|
-
// Temporary variables for the lookup loop
|
|
42
|
-
$checkParts = $parts;
|
|
43
|
-
$params = [];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Dynamic routing logic: find the deepest matching endpoint file.
|
|
47
|
-
* At each iteration, the last URL segment is peeled off and stored
|
|
48
|
-
* as a route parameter until a matching file is found.
|
|
49
|
-
*/
|
|
50
|
-
while (count($checkParts) > 0) {
|
|
51
|
-
$relativePath = implode('/', $checkParts) . '.php';
|
|
52
|
-
|
|
53
|
-
// Check first whether this is a public route
|
|
54
|
-
if (file_exists($basePublic . $relativePath)) {
|
|
55
|
-
$endpointFile = $basePublic . $relativePath;
|
|
56
|
-
$isProtected = false;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Then check whether this is a protected route
|
|
61
|
-
if (file_exists($baseProtected . $relativePath)) {
|
|
62
|
-
$endpointFile = $baseProtected . $relativePath;
|
|
63
|
-
$isProtected = true;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// No file matched: treat the last segment as a route parameter and try again
|
|
68
|
-
array_unshift($params, array_pop($checkParts));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* IF THE ENDPOINT DOES NOT EXIST, RETURN AN HTML 404 PAGE.
|
|
73
|
-
*/
|
|
74
|
-
if (!$endpointFile) {
|
|
75
|
-
http_response_code(404);
|
|
76
|
-
|
|
77
|
-
// Look for the 404.html file generated by Eleventy in the server document root
|
|
78
|
-
$errorPage = $_SERVER['DOCUMENT_ROOT'] . '/404.html';
|
|
79
|
-
|
|
80
|
-
if (file_exists($errorPage)) {
|
|
81
|
-
header('Content-Type: text/html; charset=UTF-8');
|
|
82
|
-
echo file_get_contents($errorPage);
|
|
83
|
-
} else {
|
|
84
|
-
echo "<h1>404 Not Found</h1>";
|
|
85
|
-
echo "The requested URL was not found on this server.";
|
|
86
|
-
}
|
|
87
|
-
exit;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
$base = $isProtected ? $baseProtected : $basePublic;
|
|
91
|
-
$base = str_replace('\\', '/', $base);
|
|
92
|
-
$endpointPath = str_replace('\\', '/', $endpointFile);
|
|
93
|
-
$endpointPath = preg_replace('#\.php$#', '', str_replace($base, '', $endpointPath));
|
|
94
|
-
|
|
95
|
-
// =====================================================
|
|
96
|
-
// 3. HEADERS AND CORS (Only if the endpoint exists)
|
|
97
|
-
// =====================================================
|
|
98
|
-
|
|
99
|
-
header('Content-Type: application/json; charset=UTF-8');
|
|
100
|
-
header('X-Content-Type-Options: nosniff');
|
|
101
|
-
header('X-Frame-Options: DENY');
|
|
102
|
-
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
103
|
-
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
104
|
-
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
|
|
105
|
-
|
|
106
|
-
$originsForEndpoint = $config['CUSTOM_ENDPOINT_ORIGINS'][$endpointPath]
|
|
107
|
-
?? $config['GENERAL_ALLOWED_ORIGINS']
|
|
108
|
-
?? [];
|
|
109
|
-
|
|
110
|
-
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
111
|
-
|
|
112
|
-
if ($origin !== '' && in_array($origin, $originsForEndpoint, true)) {
|
|
113
|
-
header('Access-Control-Allow-Origin: ' . $origin);
|
|
114
|
-
header('Vary: Origin');
|
|
115
|
-
} elseif (in_array('*', $originsForEndpoint, true)) {
|
|
116
|
-
header('Access-Control-Allow-Origin: *');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if ($method === 'OPTIONS') {
|
|
120
|
-
http_response_code(204);
|
|
121
|
-
exit;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
require_once __DIR__ . '/modules/RateLimiter.php';
|
|
125
|
-
RateLimiter::check($_SERVER['REMOTE_ADDR'] ?? '', 60, 60);
|
|
126
|
-
|
|
127
|
-
// =====================================================
|
|
128
|
-
// 4. AUTHENTICATION GUARD
|
|
129
|
-
// =====================================================
|
|
130
|
-
|
|
131
|
-
if ($isProtected) {
|
|
132
|
-
$validKey = $config['CUSTOM_ENDPOINT_KEYS'][$endpointPath] ?? $config['GENERAL_API_KEY'] ?? '';
|
|
133
|
-
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
|
134
|
-
|
|
135
|
-
if ($validKey === '' || !hash_equals($validKey, $apiKey)) {
|
|
136
|
-
Response::error('Unauthorized. X_API_KEY is incorrect or missing', 401);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// =====================================================
|
|
141
|
-
// 5. DISPATCH
|
|
142
|
-
// =====================================================
|
|
143
|
-
|
|
144
|
-
// Parameters are the remaining URL segments not consumed during endpoint resolution
|
|
145
|
-
$requestParams = $params;
|
|
146
|
-
|
|
147
|
-
// Load and execute the matched endpoint file
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
define('CORE_ACCESS', true);
|
|
6
|
+
define('CORE_PATH', __DIR__);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load dependencies and initial configuration.
|
|
10
|
+
*/
|
|
11
|
+
require_once __DIR__ . '/init.php';
|
|
12
|
+
require_once __DIR__ . '/modules/Response.php';
|
|
13
|
+
|
|
14
|
+
// =====================================================
|
|
15
|
+
// 1. REQUEST PARSING
|
|
16
|
+
// =====================================================
|
|
17
|
+
|
|
18
|
+
$method = $_SERVER['REQUEST_METHOD'];
|
|
19
|
+
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
20
|
+
|
|
21
|
+
$uri = rtrim(preg_replace('#^/api#', '', $uri), '/') ?: '/';
|
|
22
|
+
$parts = array_values(array_filter(explode('/', $uri)));
|
|
23
|
+
|
|
24
|
+
foreach ($parts as $part) {
|
|
25
|
+
if ($part === '..' || $part === '.') {
|
|
26
|
+
Response::error('Bad Request', 400);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// =====================================================
|
|
31
|
+
// 2. ENDPOINT RESOLUTION (ROUTING WITH SUBDIRECTORIES)
|
|
32
|
+
// =====================================================
|
|
33
|
+
|
|
34
|
+
$basePublic = __DIR__ . '/../api/public/';
|
|
35
|
+
$baseProtected = __DIR__ . '/../api/protected/';
|
|
36
|
+
|
|
37
|
+
$endpointFile = null;
|
|
38
|
+
$isProtected = false;
|
|
39
|
+
$requestParams = [];
|
|
40
|
+
|
|
41
|
+
// Temporary variables for the lookup loop
|
|
42
|
+
$checkParts = $parts;
|
|
43
|
+
$params = [];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Dynamic routing logic: find the deepest matching endpoint file.
|
|
47
|
+
* At each iteration, the last URL segment is peeled off and stored
|
|
48
|
+
* as a route parameter until a matching file is found.
|
|
49
|
+
*/
|
|
50
|
+
while (count($checkParts) > 0) {
|
|
51
|
+
$relativePath = implode('/', $checkParts) . '.php';
|
|
52
|
+
|
|
53
|
+
// Check first whether this is a public route
|
|
54
|
+
if (file_exists($basePublic . $relativePath)) {
|
|
55
|
+
$endpointFile = $basePublic . $relativePath;
|
|
56
|
+
$isProtected = false;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Then check whether this is a protected route
|
|
61
|
+
if (file_exists($baseProtected . $relativePath)) {
|
|
62
|
+
$endpointFile = $baseProtected . $relativePath;
|
|
63
|
+
$isProtected = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// No file matched: treat the last segment as a route parameter and try again
|
|
68
|
+
array_unshift($params, array_pop($checkParts));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* IF THE ENDPOINT DOES NOT EXIST, RETURN AN HTML 404 PAGE.
|
|
73
|
+
*/
|
|
74
|
+
if (!$endpointFile) {
|
|
75
|
+
http_response_code(404);
|
|
76
|
+
|
|
77
|
+
// Look for the 404.html file generated by Eleventy in the server document root
|
|
78
|
+
$errorPage = $_SERVER['DOCUMENT_ROOT'] . '/404.html';
|
|
79
|
+
|
|
80
|
+
if (file_exists($errorPage)) {
|
|
81
|
+
header('Content-Type: text/html; charset=UTF-8');
|
|
82
|
+
echo file_get_contents($errorPage);
|
|
83
|
+
} else {
|
|
84
|
+
echo "<h1>404 Not Found</h1>";
|
|
85
|
+
echo "The requested URL was not found on this server.";
|
|
86
|
+
}
|
|
87
|
+
exit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$base = $isProtected ? $baseProtected : $basePublic;
|
|
91
|
+
$base = str_replace('\\', '/', $base);
|
|
92
|
+
$endpointPath = str_replace('\\', '/', $endpointFile);
|
|
93
|
+
$endpointPath = preg_replace('#\.php$#', '', str_replace($base, '', $endpointPath));
|
|
94
|
+
|
|
95
|
+
// =====================================================
|
|
96
|
+
// 3. HEADERS AND CORS (Only if the endpoint exists)
|
|
97
|
+
// =====================================================
|
|
98
|
+
|
|
99
|
+
header('Content-Type: application/json; charset=UTF-8');
|
|
100
|
+
header('X-Content-Type-Options: nosniff');
|
|
101
|
+
header('X-Frame-Options: DENY');
|
|
102
|
+
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
103
|
+
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
104
|
+
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
|
|
105
|
+
|
|
106
|
+
$originsForEndpoint = $config['CUSTOM_ENDPOINT_ORIGINS'][$endpointPath]
|
|
107
|
+
?? $config['GENERAL_ALLOWED_ORIGINS']
|
|
108
|
+
?? [];
|
|
109
|
+
|
|
110
|
+
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
111
|
+
|
|
112
|
+
if ($origin !== '' && in_array($origin, $originsForEndpoint, true)) {
|
|
113
|
+
header('Access-Control-Allow-Origin: ' . $origin);
|
|
114
|
+
header('Vary: Origin');
|
|
115
|
+
} elseif (in_array('*', $originsForEndpoint, true)) {
|
|
116
|
+
header('Access-Control-Allow-Origin: *');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ($method === 'OPTIONS') {
|
|
120
|
+
http_response_code(204);
|
|
121
|
+
exit;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
require_once __DIR__ . '/modules/RateLimiter.php';
|
|
125
|
+
RateLimiter::check($_SERVER['REMOTE_ADDR'] ?? '', 60, 60);
|
|
126
|
+
|
|
127
|
+
// =====================================================
|
|
128
|
+
// 4. AUTHENTICATION GUARD
|
|
129
|
+
// =====================================================
|
|
130
|
+
|
|
131
|
+
if ($isProtected) {
|
|
132
|
+
$validKey = $config['CUSTOM_ENDPOINT_KEYS'][$endpointPath] ?? $config['GENERAL_API_KEY'] ?? '';
|
|
133
|
+
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
|
134
|
+
|
|
135
|
+
if ($validKey === '' || !hash_equals($validKey, $apiKey)) {
|
|
136
|
+
Response::error('Unauthorized. X_API_KEY is incorrect or missing', 401);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// =====================================================
|
|
141
|
+
// 5. DISPATCH
|
|
142
|
+
// =====================================================
|
|
143
|
+
|
|
144
|
+
// Parameters are the remaining URL segments not consumed during endpoint resolution
|
|
145
|
+
$requestParams = $params;
|
|
146
|
+
|
|
147
|
+
// Load and execute the matched endpoint file
|
|
148
148
|
require $endpointFile;
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mirror of src/backend/_core/init.php
|
|
5
|
-
*
|
|
6
|
-
* init.php did three things:
|
|
7
|
-
* 1. registered a global exception handler that turns any throwable into a
|
|
8
|
-
* 500 JSON response (verbose in debug, generic in production);
|
|
9
|
-
* 2. registered an error handler that promoted PHP warnings to exceptions;
|
|
10
|
-
* 3. loaded config.php.
|
|
11
|
-
*
|
|
12
|
-
* In Node there is no per-request global handler, so the exception-to-500
|
|
13
|
-
* translation lives in the front controller (index.js), which calls
|
|
14
|
-
* handleException() below. Here we expose config loading and that helper.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Load configuration. dirname(__dirname) in PHP pointed at the folder holding
|
|
22
|
-
* config.php (the backend root). Here that is the parent of _core.
|
|
23
|
-
* If config.js is missing (e.g. fresh clone) we fall back to example.config.js,
|
|
24
|
-
* mirroring the "copy example.config to config" guidance.
|
|
25
|
-
*/
|
|
26
|
-
function loadConfig() {
|
|
27
|
-
const backendRoot = path.join(__dirname, '..');
|
|
28
|
-
const configPath = path.join(backendRoot, 'config.js');
|
|
29
|
-
const examplePath = path.join(backendRoot, 'example.config.js');
|
|
30
|
-
|
|
31
|
-
if (fs.existsSync(configPath)) {
|
|
32
|
-
return require(configPath);
|
|
33
|
-
}
|
|
34
|
-
return require(examplePath);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Turn any thrown error into a 500 response, mirroring set_exception_handler.
|
|
39
|
-
* @param {Error} exception
|
|
40
|
-
* @param {object} config
|
|
41
|
-
* @param {object} Response request-bound helper
|
|
42
|
-
*/
|
|
43
|
-
function handleException(exception, config, Response) {
|
|
44
|
-
const isDebug = (config.APP_ENV || 'production') !== 'production';
|
|
45
|
-
Response.error(
|
|
46
|
-
isDebug ? exception.message : 'Internal server error',
|
|
47
|
-
500,
|
|
48
|
-
isDebug ? { stack: exception.stack } : null
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
module.exports = { loadConfig, handleException };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/_core/init.php
|
|
5
|
+
*
|
|
6
|
+
* init.php did three things:
|
|
7
|
+
* 1. registered a global exception handler that turns any throwable into a
|
|
8
|
+
* 500 JSON response (verbose in debug, generic in production);
|
|
9
|
+
* 2. registered an error handler that promoted PHP warnings to exceptions;
|
|
10
|
+
* 3. loaded config.php.
|
|
11
|
+
*
|
|
12
|
+
* In Node there is no per-request global handler, so the exception-to-500
|
|
13
|
+
* translation lives in the front controller (index.js), which calls
|
|
14
|
+
* handleException() below. Here we expose config loading and that helper.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load configuration. dirname(__dirname) in PHP pointed at the folder holding
|
|
22
|
+
* config.php (the backend root). Here that is the parent of _core.
|
|
23
|
+
* If config.js is missing (e.g. fresh clone) we fall back to example.config.js,
|
|
24
|
+
* mirroring the "copy example.config to config" guidance.
|
|
25
|
+
*/
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
const backendRoot = path.join(__dirname, '..');
|
|
28
|
+
const configPath = path.join(backendRoot, 'config.js');
|
|
29
|
+
const examplePath = path.join(backendRoot, 'example.config.js');
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
return require(configPath);
|
|
33
|
+
}
|
|
34
|
+
return require(examplePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Turn any thrown error into a 500 response, mirroring set_exception_handler.
|
|
39
|
+
* @param {Error} exception
|
|
40
|
+
* @param {object} config
|
|
41
|
+
* @param {object} Response request-bound helper
|
|
42
|
+
*/
|
|
43
|
+
function handleException(exception, config, Response) {
|
|
44
|
+
const isDebug = (config.APP_ENV || 'production') !== 'production';
|
|
45
|
+
Response.error(
|
|
46
|
+
isDebug ? exception.message : 'Internal server error',
|
|
47
|
+
500,
|
|
48
|
+
isDebug ? { stack: exception.stack } : null
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { loadConfig, handleException };
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
require_once __DIR__ . '/vendor/autoload.php';
|
|
6
|
-
require_once __DIR__ . '/modules/Response.php';
|
|
7
|
-
|
|
8
|
-
// --- GESTORE GLOBALE ERRORI E ECCEZIONI ---
|
|
9
|
-
set_exception_handler(function ($exception) use (&$config) {
|
|
10
|
-
$isDebug = ($config['APP_ENV'] ?? 'production') !== 'production';
|
|
11
|
-
Response::error(
|
|
12
|
-
$isDebug ? $exception->getMessage() : 'Internal server error',
|
|
13
|
-
500,
|
|
14
|
-
$isDebug ? ['file' => $exception->getFile(), 'line' => $exception->getLine()] : null
|
|
15
|
-
);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
set_error_handler(function ($severity, $message, $file, $line) {
|
|
19
|
-
if (!(error_reporting() & $severity)) return;
|
|
20
|
-
throw new ErrorException($message, 0, $severity, $file, $line);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// --- CARICAMENTO CONFIGURAZIONE ---
|
|
24
|
-
// dirname(__DIR__) punta alla cartella /api/ dove ora si trova config.php
|
|
25
|
-
$config = require dirname(__DIR__) . '/config.php';
|
|
26
|
-
|
|
27
|
-
// --- CONFIGURAZIONE AMBIENTE ---
|
|
28
|
-
if (($config['APP_ENV'] ?? 'production') === 'production') {
|
|
29
|
-
ini_set('display_errors', '0');
|
|
30
|
-
error_reporting(0);
|
|
31
|
-
} else {
|
|
32
|
-
ini_set('display_errors', '1');
|
|
33
|
-
error_reporting(E_ALL);
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
require_once __DIR__ . '/vendor/autoload.php';
|
|
6
|
+
require_once __DIR__ . '/modules/Response.php';
|
|
7
|
+
|
|
8
|
+
// --- GESTORE GLOBALE ERRORI E ECCEZIONI ---
|
|
9
|
+
set_exception_handler(function ($exception) use (&$config) {
|
|
10
|
+
$isDebug = ($config['APP_ENV'] ?? 'production') !== 'production';
|
|
11
|
+
Response::error(
|
|
12
|
+
$isDebug ? $exception->getMessage() : 'Internal server error',
|
|
13
|
+
500,
|
|
14
|
+
$isDebug ? ['file' => $exception->getFile(), 'line' => $exception->getLine()] : null
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
set_error_handler(function ($severity, $message, $file, $line) {
|
|
19
|
+
if (!(error_reporting() & $severity)) return;
|
|
20
|
+
throw new ErrorException($message, 0, $severity, $file, $line);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --- CARICAMENTO CONFIGURAZIONE ---
|
|
24
|
+
// dirname(__DIR__) punta alla cartella /api/ dove ora si trova config.php
|
|
25
|
+
$config = require dirname(__DIR__) . '/config.php';
|
|
26
|
+
|
|
27
|
+
// --- CONFIGURAZIONE AMBIENTE ---
|
|
28
|
+
if (($config['APP_ENV'] ?? 'production') === 'production') {
|
|
29
|
+
ini_set('display_errors', '0');
|
|
30
|
+
error_reporting(0);
|
|
31
|
+
} else {
|
|
32
|
+
ini_set('display_errors', '1');
|
|
33
|
+
error_reporting(E_ALL);
|
|
34
34
|
}
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mirror of src/backend/_core/modules/RateLimiter.php
|
|
5
|
-
*
|
|
6
|
-
* Same algorithm and same on-disk format: one JSON file per IP under ../../cache,
|
|
7
|
-
* holding an array of request timestamps. Old timestamps outside the window are
|
|
8
|
-
* pruned, the current one appended, and if the count exceeds the limit a 429 is
|
|
9
|
-
* returned (with a Retry-After header) through the request-bound Response helper.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
|
|
16
|
-
class RateLimiter {
|
|
17
|
-
/**
|
|
18
|
-
* @param {string} ip
|
|
19
|
-
* @param {number} maxRequests
|
|
20
|
-
* @param {number} windowSeconds
|
|
21
|
-
* @param {object} Response request-bound helper from createResponse(res)
|
|
22
|
-
* @param {object} res raw http response (to set Retry-After)
|
|
23
|
-
*/
|
|
24
|
-
static check(ip, maxRequests = 60, windowSeconds = 60, Response, res) {
|
|
25
|
-
const cacheDir = path.join(__dirname, '..', '..', 'cache');
|
|
26
|
-
|
|
27
|
-
if (!fs.existsSync(cacheDir)) {
|
|
28
|
-
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const hash = crypto.createHash('md5').update(ip).digest('hex');
|
|
32
|
-
const cacheFile = path.join(cacheDir, 'rl_' + hash + '.json');
|
|
33
|
-
const now = Math.floor(Date.now() / 1000);
|
|
34
|
-
let data = [];
|
|
35
|
-
|
|
36
|
-
if (fs.existsSync(cacheFile)) {
|
|
37
|
-
try {
|
|
38
|
-
data = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) || [];
|
|
39
|
-
} catch (e) {
|
|
40
|
-
data = [];
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
data = data.filter((ts) => ts > (now - windowSeconds));
|
|
45
|
-
data.push(now);
|
|
46
|
-
|
|
47
|
-
// LOCK_EX equivalent: an exclusive write. Node's writeFileSync is atomic
|
|
48
|
-
// enough for this single-process model.
|
|
49
|
-
fs.writeFileSync(cacheFile, JSON.stringify(data));
|
|
50
|
-
|
|
51
|
-
if (data.length > maxRequests) {
|
|
52
|
-
res.setHeader('Retry-After', String(windowSeconds));
|
|
53
|
-
Response.error('Too Many Requests', 429);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
module.exports = RateLimiter;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirror of src/backend/_core/modules/RateLimiter.php
|
|
5
|
+
*
|
|
6
|
+
* Same algorithm and same on-disk format: one JSON file per IP under ../../cache,
|
|
7
|
+
* holding an array of request timestamps. Old timestamps outside the window are
|
|
8
|
+
* pruned, the current one appended, and if the count exceeds the limit a 429 is
|
|
9
|
+
* returned (with a Retry-After header) through the request-bound Response helper.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
class RateLimiter {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} ip
|
|
19
|
+
* @param {number} maxRequests
|
|
20
|
+
* @param {number} windowSeconds
|
|
21
|
+
* @param {object} Response request-bound helper from createResponse(res)
|
|
22
|
+
* @param {object} res raw http response (to set Retry-After)
|
|
23
|
+
*/
|
|
24
|
+
static check(ip, maxRequests = 60, windowSeconds = 60, Response, res) {
|
|
25
|
+
const cacheDir = path.join(__dirname, '..', '..', 'cache');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(cacheDir)) {
|
|
28
|
+
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hash = crypto.createHash('md5').update(ip).digest('hex');
|
|
32
|
+
const cacheFile = path.join(cacheDir, 'rl_' + hash + '.json');
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
let data = [];
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(cacheFile)) {
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) || [];
|
|
39
|
+
} catch (e) {
|
|
40
|
+
data = [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
data = data.filter((ts) => ts > (now - windowSeconds));
|
|
45
|
+
data.push(now);
|
|
46
|
+
|
|
47
|
+
// LOCK_EX equivalent: an exclusive write. Node's writeFileSync is atomic
|
|
48
|
+
// enough for this single-process model.
|
|
49
|
+
fs.writeFileSync(cacheFile, JSON.stringify(data));
|
|
50
|
+
|
|
51
|
+
if (data.length > maxRequests) {
|
|
52
|
+
res.setHeader('Retry-After', String(windowSeconds));
|
|
53
|
+
Response.error('Too Many Requests', 429);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = RateLimiter;
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
class RateLimiter {
|
|
6
|
-
public static function check(string $ip, int $maxRequests = 60, int $windowSeconds = 60): void {
|
|
7
|
-
$cacheDir = __DIR__ . '/../../cache';
|
|
8
|
-
|
|
9
|
-
if (!is_dir($cacheDir)) {
|
|
10
|
-
mkdir($cacheDir, 0755, true);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
$cacheFile = $cacheDir . '/rl_' . md5($ip) . '.json';
|
|
14
|
-
$now = time();
|
|
15
|
-
$data = [];
|
|
16
|
-
|
|
17
|
-
if (file_exists($cacheFile)) {
|
|
18
|
-
$data = json_decode(file_get_contents($cacheFile), true) ?: [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
$data = array_filter($data, fn($ts) => $ts > ($now - $windowSeconds));
|
|
22
|
-
$data[] = $now;
|
|
23
|
-
|
|
24
|
-
file_put_contents($cacheFile, json_encode(array_values($data)), LOCK_EX);
|
|
25
|
-
|
|
26
|
-
if (count($data) > $maxRequests) {
|
|
27
|
-
header('Retry-After: ' . $windowSeconds);
|
|
28
|
-
Response::error('Too Many Requests', 429);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
class RateLimiter {
|
|
6
|
+
public static function check(string $ip, int $maxRequests = 60, int $windowSeconds = 60): void {
|
|
7
|
+
$cacheDir = __DIR__ . '/../../cache';
|
|
8
|
+
|
|
9
|
+
if (!is_dir($cacheDir)) {
|
|
10
|
+
mkdir($cacheDir, 0755, true);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
$cacheFile = $cacheDir . '/rl_' . md5($ip) . '.json';
|
|
14
|
+
$now = time();
|
|
15
|
+
$data = [];
|
|
16
|
+
|
|
17
|
+
if (file_exists($cacheFile)) {
|
|
18
|
+
$data = json_decode(file_get_contents($cacheFile), true) ?: [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
$data = array_filter($data, fn($ts) => $ts > ($now - $windowSeconds));
|
|
22
|
+
$data[] = $now;
|
|
23
|
+
|
|
24
|
+
file_put_contents($cacheFile, json_encode(array_values($data)), LOCK_EX);
|
|
25
|
+
|
|
26
|
+
if (count($data) > $maxRequests) {
|
|
27
|
+
header('Retry-After: ' . $windowSeconds);
|
|
28
|
+
Response::error('Too Many Requests', 429);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
31
|
}
|