nibula 2.4.5 → 2.4.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/backend/_core/index.js +199 -236
- package/src/backend/_core/index.php +0 -43
- package/src/backend/_core/init.js +16 -36
- package/src/backend/_core/init.php +0 -4
- package/src/backend/_core/modules/RateLimiter.js +40 -51
- package/src/backend/_core/modules/Response.js +35 -48
- package/src/backend/api/protected/example-protected.js +11 -18
- package/src/backend/api/public/example-public.js +11 -19
- package/src/backend/database/Database.js +0 -17
- package/src/backend/example.config.js +23 -24
- package/src/backend/example.config.php +2 -3
- package/src/frontend/scss/pages/404.scss +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to Nibula are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.4.6] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Restored `404.scss` **.not-found** selector to center the not found text
|
|
13
|
+
- Removed all comments in backend files
|
|
14
|
+
|
|
8
15
|
## [2.4.5] - 2026-08-26
|
|
9
16
|
|
|
10
17
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nibula",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
4
4
|
"description": "A beginner-friendly static site generator built on Eleventy that stays close to vanilla HTML, CSS and JS — with a page-management CLI, an optional PHP backend, and SEO files generated for you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"11ty",
|
|
@@ -1,267 +1,230 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* 2. endpoint resolution (dynamic routing with subdirectories + route params)
|
|
12
|
-
* 3. headers + CORS
|
|
13
|
-
* 4. rate limiting
|
|
14
|
-
* 5. authentication guard for protected endpoints
|
|
15
|
-
* 6. dispatch to the matched endpoint file
|
|
16
|
-
*
|
|
17
|
-
* Endpoints keep the same authoring style as PHP: a file that receives
|
|
18
|
-
* { method, requestParams, Response, ... } and calls Response.success/error.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const http = require('http');
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
const crypto = require('crypto');
|
|
25
|
-
|
|
26
|
-
const { createResponse, HALT } = require('./modules/Response');
|
|
27
|
-
const RateLimiter = require('./modules/RateLimiter');
|
|
28
|
-
const { loadConfig, handleException } = require('./init');
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
|
|
8
|
+
const { createResponse, HALT } = require("./modules/Response");
|
|
9
|
+
const RateLimiter = require("./modules/RateLimiter");
|
|
10
|
+
const { loadConfig, handleException } = require("./init");
|
|
29
11
|
|
|
30
12
|
const CORE_PATH = __dirname;
|
|
31
13
|
|
|
32
|
-
// Load configuration once (init.php loaded it per invocation; here per process).
|
|
33
14
|
const config = loadConfig();
|
|
34
15
|
|
|
35
|
-
|
|
36
|
-
const APP_ENV = config.APP_ENV || 'production';
|
|
16
|
+
const APP_ENV = config.APP_ENV || "production";
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// is two levels up from _core. We default to that, so 404.html is found without
|
|
41
|
-
// any extra configuration; override with the DOCUMENT_ROOT env var if needed.
|
|
42
|
-
// (equivalent of $_SERVER['DOCUMENT_ROOT'] in the PHP front controller).
|
|
43
|
-
const DOCUMENT_ROOT = process.env.DOCUMENT_ROOT || path.join(CORE_PATH, '..', '..');
|
|
18
|
+
const DOCUMENT_ROOT =
|
|
19
|
+
process.env.DOCUMENT_ROOT || path.join(CORE_PATH, "..", "..");
|
|
44
20
|
|
|
45
|
-
const basePublic = path.join(CORE_PATH,
|
|
46
|
-
const baseProtected = path.join(CORE_PATH,
|
|
21
|
+
const basePublic = path.join(CORE_PATH, "..", "api", "public");
|
|
22
|
+
const baseProtected = path.join(CORE_PATH, "..", "api", "protected");
|
|
47
23
|
|
|
48
|
-
/**
|
|
49
|
-
* hash_equals equivalent: constant-time string comparison, safe on unequal
|
|
50
|
-
* lengths (timingSafeEqual throws if buffers differ in length).
|
|
51
|
-
*/
|
|
52
24
|
function hashEquals(known, given) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
25
|
+
const a = Buffer.from(String(known));
|
|
26
|
+
const b = Buffer.from(String(given));
|
|
27
|
+
if (a.length !== b.length) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return crypto.timingSafeEqual(a, b);
|
|
59
31
|
}
|
|
60
32
|
|
|
61
|
-
/** Serve the static 404 page if available, else a plain fallback. */
|
|
62
33
|
function send404(res) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
34
|
+
res.statusCode = 404;
|
|
35
|
+
const errorPage = DOCUMENT_ROOT ? path.join(DOCUMENT_ROOT, "404.html") : "";
|
|
36
|
+
if (errorPage && fs.existsSync(errorPage)) {
|
|
37
|
+
res.setHeader("Content-Type", "text/html; charset=UTF-8");
|
|
38
|
+
res.end(fs.readFileSync(errorPage));
|
|
39
|
+
} else {
|
|
40
|
+
res.setHeader("Content-Type", "text/html; charset=UTF-8");
|
|
41
|
+
res.end(
|
|
42
|
+
"<h1>404 Not Found</h1>The requested URL was not found on this server.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
72
45
|
}
|
|
73
46
|
|
|
74
|
-
/** Read and JSON-parse the request body (available to endpoints as ctx.body). */
|
|
75
47
|
function readBody(req) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
req.on("data", (c) => chunks.push(c));
|
|
51
|
+
req.on("end", () => {
|
|
52
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
53
|
+
if (!raw) return resolve({ raw: "", json: null });
|
|
54
|
+
let json = null;
|
|
55
|
+
try {
|
|
56
|
+
json = JSON.parse(raw);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
json = null;
|
|
59
|
+
}
|
|
60
|
+
resolve({ raw, json });
|
|
87
61
|
});
|
|
62
|
+
req.on("error", () => resolve({ raw: "", json: null }));
|
|
63
|
+
});
|
|
88
64
|
}
|
|
89
65
|
|
|
90
66
|
async function handleRequest(req, res) {
|
|
91
|
-
|
|
67
|
+
const Response = createResponse(res);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const method = req.method;
|
|
71
|
+
const parsedUrl = new URL(req.url, "http://localhost");
|
|
72
|
+
let uri = parsedUrl.pathname;
|
|
73
|
+
|
|
74
|
+
uri = uri.replace(/^\/api/, "");
|
|
75
|
+
uri = uri.replace(/\/+$/, "");
|
|
76
|
+
if (uri === "") uri = "/";
|
|
77
|
+
|
|
78
|
+
const parts = uri.split("/").filter((p) => p !== "");
|
|
79
|
+
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part === ".." || part === ".") {
|
|
82
|
+
Response.error("Bad Request", 400);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let endpointFile = null;
|
|
87
|
+
let isProtected = false;
|
|
88
|
+
|
|
89
|
+
const checkParts = parts.slice();
|
|
90
|
+
const params = [];
|
|
91
|
+
|
|
92
|
+
while (checkParts.length > 0) {
|
|
93
|
+
const relativePath = checkParts.join("/") + ".js";
|
|
94
|
+
|
|
95
|
+
const publicCandidate = path.join(basePublic, relativePath);
|
|
96
|
+
if (fs.existsSync(publicCandidate)) {
|
|
97
|
+
endpointFile = publicCandidate;
|
|
98
|
+
isProtected = false;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
92
101
|
|
|
102
|
+
const protectedCandidate = path.join(baseProtected, relativePath);
|
|
103
|
+
if (fs.existsSync(protectedCandidate)) {
|
|
104
|
+
endpointFile = protectedCandidate;
|
|
105
|
+
isProtected = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
params.unshift(checkParts.pop());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!endpointFile) {
|
|
113
|
+
send404(res);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const base = isProtected ? baseProtected : basePublic;
|
|
118
|
+
let endpointPath = endpointFile
|
|
119
|
+
.split(path.sep)
|
|
120
|
+
.join("/")
|
|
121
|
+
.replace(base.split(path.sep).join("/"), "")
|
|
122
|
+
.replace(/\.js$/, "")
|
|
123
|
+
.replace(/^\//, "");
|
|
124
|
+
|
|
125
|
+
res.setHeader("Content-Type", "application/json; charset=UTF-8");
|
|
126
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
127
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
128
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
129
|
+
res.setHeader(
|
|
130
|
+
"Access-Control-Allow-Methods",
|
|
131
|
+
"GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
132
|
+
);
|
|
133
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Api-Key");
|
|
134
|
+
|
|
135
|
+
const originsForEndpoint =
|
|
136
|
+
(config.CUSTOM_ENDPOINT_ORIGINS &&
|
|
137
|
+
config.CUSTOM_ENDPOINT_ORIGINS[endpointPath]) ||
|
|
138
|
+
config.GENERAL_ALLOWED_ORIGINS ||
|
|
139
|
+
[];
|
|
140
|
+
|
|
141
|
+
const origin = req.headers["origin"] || "";
|
|
142
|
+
|
|
143
|
+
if (origin !== "" && originsForEndpoint.includes(origin)) {
|
|
144
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
145
|
+
res.setHeader("Vary", "Origin");
|
|
146
|
+
} else if (originsForEndpoint.includes("*")) {
|
|
147
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (method === "OPTIONS") {
|
|
151
|
+
res.statusCode = 204;
|
|
152
|
+
res.end();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Rate limit: 60 requests / 60 seconds per IP
|
|
157
|
+
const ip =
|
|
158
|
+
(req.headers["x-forwarded-for"] || "").split(",")[0].trim() ||
|
|
159
|
+
req.socket.remoteAddress ||
|
|
160
|
+
"";
|
|
161
|
+
RateLimiter.check(ip, 60, 60, Response, res);
|
|
162
|
+
|
|
163
|
+
if (isProtected) {
|
|
164
|
+
const validKey =
|
|
165
|
+
(config.CUSTOM_ENDPOINT_KEYS &&
|
|
166
|
+
config.CUSTOM_ENDPOINT_KEYS[endpointPath]) ||
|
|
167
|
+
config.GENERAL_API_KEY ||
|
|
168
|
+
"";
|
|
169
|
+
const apiKey = req.headers["x-api-key"] || "";
|
|
170
|
+
|
|
171
|
+
if (validKey === "" || !hashEquals(validKey, apiKey)) {
|
|
172
|
+
Response.error("Unauthorized. X_API_KEY is incorrect or missing", 403);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const handler = require(endpointFile);
|
|
177
|
+
|
|
178
|
+
const context = {
|
|
179
|
+
method, // HTTP method
|
|
180
|
+
requestParams: params, // leftover URL segments (route params)
|
|
181
|
+
Response, // request-bound Response helper
|
|
182
|
+
query: Object.fromEntries(parsedUrl.searchParams), // parsed query string
|
|
183
|
+
body: body.json, // parsed JSON body (or null)
|
|
184
|
+
rawBody: body.raw, // raw body string
|
|
185
|
+
headers: req.headers,
|
|
186
|
+
req,
|
|
187
|
+
res,
|
|
188
|
+
config,
|
|
189
|
+
CORE_PATH,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await handler(context);
|
|
193
|
+
|
|
194
|
+
if (!res.writableEnded) {
|
|
195
|
+
res.end();
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err === HALT) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
93
201
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const part of parts) {
|
|
109
|
-
if (part === '..' || part === '.') {
|
|
110
|
-
Response.error('Bad Request', 400);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// =====================================================
|
|
115
|
-
// 2. ENDPOINT RESOLUTION (ROUTING WITH SUBDIRECTORIES)
|
|
116
|
-
// =====================================================
|
|
117
|
-
let endpointFile = null;
|
|
118
|
-
let isProtected = false;
|
|
119
|
-
|
|
120
|
-
const checkParts = parts.slice();
|
|
121
|
-
const params = [];
|
|
122
|
-
|
|
123
|
-
while (checkParts.length > 0) {
|
|
124
|
-
const relativePath = checkParts.join('/') + '.js';
|
|
125
|
-
|
|
126
|
-
const publicCandidate = path.join(basePublic, relativePath);
|
|
127
|
-
if (fs.existsSync(publicCandidate)) {
|
|
128
|
-
endpointFile = publicCandidate;
|
|
129
|
-
isProtected = false;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const protectedCandidate = path.join(baseProtected, relativePath);
|
|
134
|
-
if (fs.existsSync(protectedCandidate)) {
|
|
135
|
-
endpointFile = protectedCandidate;
|
|
136
|
-
isProtected = true;
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// No match: peel the last segment as a route param and retry.
|
|
141
|
-
params.unshift(checkParts.pop());
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// No endpoint → HTML 404 page.
|
|
145
|
-
if (!endpointFile) {
|
|
146
|
-
send404(res);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Compute the endpoint path (relative, without extension) used as the key
|
|
151
|
-
// for CUSTOM_ENDPOINT_KEYS / CUSTOM_ENDPOINT_ORIGINS.
|
|
152
|
-
const base = isProtected ? baseProtected : basePublic;
|
|
153
|
-
let endpointPath = endpointFile
|
|
154
|
-
.split(path.sep).join('/')
|
|
155
|
-
.replace(base.split(path.sep).join('/'), '')
|
|
156
|
-
.replace(/\.js$/, '')
|
|
157
|
-
.replace(/^\//, '');
|
|
158
|
-
|
|
159
|
-
// =====================================================
|
|
160
|
-
// 3. HEADERS AND CORS (only if the endpoint exists)
|
|
161
|
-
// =====================================================
|
|
162
|
-
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
163
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
164
|
-
res.setHeader('X-Frame-Options', 'DENY');
|
|
165
|
-
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
166
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
167
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key');
|
|
168
|
-
|
|
169
|
-
const originsForEndpoint =
|
|
170
|
-
(config.CUSTOM_ENDPOINT_ORIGINS && config.CUSTOM_ENDPOINT_ORIGINS[endpointPath]) ||
|
|
171
|
-
config.GENERAL_ALLOWED_ORIGINS ||
|
|
172
|
-
[];
|
|
173
|
-
|
|
174
|
-
const origin = req.headers['origin'] || '';
|
|
175
|
-
|
|
176
|
-
if (origin !== '' && originsForEndpoint.includes(origin)) {
|
|
177
|
-
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
178
|
-
res.setHeader('Vary', 'Origin');
|
|
179
|
-
} else if (originsForEndpoint.includes('*')) {
|
|
180
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (method === 'OPTIONS') {
|
|
184
|
-
res.statusCode = 204;
|
|
185
|
-
res.end();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Rate limit: 60 requests / 60 seconds per IP (same defaults as PHP).
|
|
190
|
-
const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
|
191
|
-
|| req.socket.remoteAddress
|
|
192
|
-
|| '';
|
|
193
|
-
RateLimiter.check(ip, 60, 60, Response, res);
|
|
194
|
-
|
|
195
|
-
// =====================================================
|
|
196
|
-
// 4. AUTHENTICATION GUARD
|
|
197
|
-
// =====================================================
|
|
198
|
-
if (isProtected) {
|
|
199
|
-
const validKey =
|
|
200
|
-
(config.CUSTOM_ENDPOINT_KEYS && config.CUSTOM_ENDPOINT_KEYS[endpointPath]) ||
|
|
201
|
-
config.GENERAL_API_KEY ||
|
|
202
|
-
'';
|
|
203
|
-
const apiKey = req.headers['x-api-key'] || '';
|
|
204
|
-
|
|
205
|
-
if (validKey === '' || !hashEquals(validKey, apiKey)) {
|
|
206
|
-
Response.error('Unauthorized. X_API_KEY is incorrect or missing', 403);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// =====================================================
|
|
211
|
-
// 5. DISPATCH
|
|
212
|
-
// =====================================================
|
|
213
|
-
const body = await readBody(req);
|
|
214
|
-
|
|
215
|
-
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
216
|
-
const handler = require(endpointFile);
|
|
217
|
-
|
|
218
|
-
const context = {
|
|
219
|
-
method, // HTTP method
|
|
220
|
-
requestParams: params, // leftover URL segments (route params)
|
|
221
|
-
Response, // request-bound Response helper
|
|
222
|
-
query: Object.fromEntries(parsedUrl.searchParams), // parsed query string
|
|
223
|
-
body: body.json, // parsed JSON body (or null)
|
|
224
|
-
rawBody: body.raw, // raw body string
|
|
225
|
-
headers: req.headers,
|
|
226
|
-
req,
|
|
227
|
-
res,
|
|
228
|
-
config,
|
|
229
|
-
CORE_PATH,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
await handler(context);
|
|
233
|
-
|
|
234
|
-
// If the endpoint returned without sending a response, mirror PHP's
|
|
235
|
-
// implicit end of script: nothing more to do. Close if still open.
|
|
236
|
-
if (!res.writableEnded) {
|
|
237
|
-
res.end();
|
|
238
|
-
}
|
|
239
|
-
} catch (err) {
|
|
240
|
-
if (err === HALT) {
|
|
241
|
-
// A response was already sent; unwind quietly (≈ PHP exit).
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
// set_exception_handler equivalent: any real error → 500.
|
|
245
|
-
try {
|
|
246
|
-
handleException(err, config, Response);
|
|
247
|
-
} catch (inner) {
|
|
248
|
-
if (inner === HALT) return;
|
|
249
|
-
if (!res.writableEnded) {
|
|
250
|
-
res.statusCode = 500;
|
|
251
|
-
res.end(JSON.stringify({ status: 'error', message: 'Internal server error', code: 500 }));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
202
|
+
handleException(err, config, Response);
|
|
203
|
+
} catch (inner) {
|
|
204
|
+
if (inner === HALT) return;
|
|
205
|
+
if (!res.writableEnded) {
|
|
206
|
+
res.statusCode = 500;
|
|
207
|
+
res.end(
|
|
208
|
+
JSON.stringify({
|
|
209
|
+
status: "error",
|
|
210
|
+
message: "Internal server error",
|
|
211
|
+
code: 500,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
254
215
|
}
|
|
216
|
+
}
|
|
255
217
|
}
|
|
256
218
|
|
|
257
|
-
const HOST = process.env.HOST ||
|
|
258
|
-
const PORT = parseInt(process.env.PORT ||
|
|
219
|
+
const HOST = process.env.HOST || "127.0.0.1";
|
|
220
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
259
221
|
|
|
260
222
|
const server = http.createServer(handleRequest);
|
|
261
223
|
|
|
262
224
|
server.listen(PORT, HOST, () => {
|
|
263
|
-
|
|
264
|
-
|
|
225
|
+
console.log(
|
|
226
|
+
`[backend-node] listening on http://${HOST}:${PORT} (env: ${APP_ENV})`,
|
|
227
|
+
);
|
|
265
228
|
});
|
|
266
229
|
|
|
267
230
|
module.exports = server;
|
|
@@ -5,16 +5,9 @@ declare(strict_types=1);
|
|
|
5
5
|
define('CORE_ACCESS', true);
|
|
6
6
|
define('CORE_PATH', __DIR__);
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Load dependencies and initial configuration.
|
|
10
|
-
*/
|
|
11
8
|
require_once __DIR__ . '/init.php';
|
|
12
9
|
require_once __DIR__ . '/modules/Response.php';
|
|
13
10
|
|
|
14
|
-
// =====================================================
|
|
15
|
-
// 1. REQUEST PARSING
|
|
16
|
-
// =====================================================
|
|
17
|
-
|
|
18
11
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
19
12
|
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
20
13
|
|
|
@@ -27,26 +20,15 @@ foreach ($parts as $part) {
|
|
|
27
20
|
}
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
// =====================================================
|
|
31
|
-
// 2. ENDPOINT RESOLUTION (ROUTING WITH SUBDIRECTORIES)
|
|
32
|
-
// =====================================================
|
|
33
|
-
|
|
34
23
|
$basePublic = __DIR__ . '/../api/public/';
|
|
35
24
|
$baseProtected = __DIR__ . '/../api/protected/';
|
|
36
25
|
|
|
37
26
|
$endpointFile = null;
|
|
38
27
|
$isProtected = false;
|
|
39
28
|
$requestParams = [];
|
|
40
|
-
|
|
41
|
-
// Temporary variables for the lookup loop
|
|
42
29
|
$checkParts = $parts;
|
|
43
30
|
$params = [];
|
|
44
31
|
|
|
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
32
|
while (count($checkParts) > 0) {
|
|
51
33
|
$relativePath = implode('/', $checkParts) . '.php';
|
|
52
34
|
|
|
@@ -57,30 +39,19 @@ while (count($checkParts) > 0) {
|
|
|
57
39
|
break;
|
|
58
40
|
}
|
|
59
41
|
|
|
60
|
-
// Then check whether this is a protected route
|
|
61
42
|
if (file_exists($baseProtected . $relativePath)) {
|
|
62
43
|
$endpointFile = $baseProtected . $relativePath;
|
|
63
44
|
$isProtected = true;
|
|
64
45
|
break;
|
|
65
46
|
}
|
|
66
47
|
|
|
67
|
-
// No file matched: treat the last segment as a route parameter and try again
|
|
68
48
|
array_unshift($params, array_pop($checkParts));
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
/**
|
|
72
|
-
* IF THE ENDPOINT DOES NOT EXIST, RETURN AN HTML 404 PAGE.
|
|
73
|
-
*/
|
|
74
51
|
if (!$endpointFile) {
|
|
75
52
|
http_response_code(404);
|
|
76
53
|
header_remove('WWW-Authenticate');
|
|
77
54
|
|
|
78
|
-
/**
|
|
79
|
-
* In a Nibula build the backend lives at out/backend/, so the site root
|
|
80
|
-
* (out/) is two levels up from _core. DOCUMENT_ROOT is preferred when it
|
|
81
|
-
* resolves, but on shared hosting it often points elsewhere, so the
|
|
82
|
-
* relative path is used as a fallback. Same resolution as index.js.
|
|
83
|
-
*/
|
|
84
55
|
$documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
|
|
85
56
|
$errorPage = $documentRoot . '/404.html';
|
|
86
57
|
|
|
@@ -104,10 +75,6 @@ $base = str_replace('\\', '/', $base);
|
|
|
104
75
|
$endpointPath = str_replace('\\', '/', $endpointFile);
|
|
105
76
|
$endpointPath = preg_replace('#\.php$#', '', str_replace($base, '', $endpointPath));
|
|
106
77
|
|
|
107
|
-
// =====================================================
|
|
108
|
-
// 3. HEADERS AND CORS (Only if the endpoint exists)
|
|
109
|
-
// =====================================================
|
|
110
|
-
|
|
111
78
|
header('Content-Type: application/json; charset=UTF-8');
|
|
112
79
|
header_remove('WWW-Authenticate');
|
|
113
80
|
header('X-Content-Type-Options: nosniff');
|
|
@@ -137,10 +104,6 @@ if ($method === 'OPTIONS') {
|
|
|
137
104
|
require_once __DIR__ . '/modules/RateLimiter.php';
|
|
138
105
|
RateLimiter::check($_SERVER['REMOTE_ADDR'] ?? '', 60, 60);
|
|
139
106
|
|
|
140
|
-
// =====================================================
|
|
141
|
-
// 4. AUTHENTICATION GUARD
|
|
142
|
-
// =====================================================
|
|
143
|
-
|
|
144
107
|
if ($isProtected) {
|
|
145
108
|
$validKey = $config['CUSTOM_ENDPOINT_KEYS'][$endpointPath] ?? $config['GENERAL_API_KEY'] ?? '';
|
|
146
109
|
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
|
@@ -150,12 +113,6 @@ if ($isProtected) {
|
|
|
150
113
|
}
|
|
151
114
|
}
|
|
152
115
|
|
|
153
|
-
// =====================================================
|
|
154
|
-
// 5. DISPATCH
|
|
155
|
-
// =====================================================
|
|
156
|
-
|
|
157
|
-
// Parameters are the remaining URL segments not consumed during endpoint resolution
|
|
158
116
|
$requestParams = $params;
|
|
159
117
|
|
|
160
|
-
// Load and execute the matched endpoint file
|
|
161
118
|
require $endpointFile;
|
|
@@ -1,37 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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');
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
19
5
|
|
|
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
6
|
function loadConfig() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
7
|
+
const backendRoot = path.join(__dirname, "..");
|
|
8
|
+
const configPath = path.join(backendRoot, "config.js");
|
|
9
|
+
const examplePath = path.join(backendRoot, "example.config.js");
|
|
30
10
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
if (fs.existsSync(configPath)) {
|
|
12
|
+
return require(configPath);
|
|
13
|
+
}
|
|
14
|
+
return require(examplePath);
|
|
35
15
|
}
|
|
36
16
|
|
|
37
17
|
/**
|
|
@@ -41,12 +21,12 @@ function loadConfig() {
|
|
|
41
21
|
* @param {object} Response request-bound helper
|
|
42
22
|
*/
|
|
43
23
|
function handleException(exception, config, Response) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
const isDebug = (config.APP_ENV || "production") !== "production";
|
|
25
|
+
Response.error(
|
|
26
|
+
isDebug ? exception.message : "Internal server error",
|
|
27
|
+
500,
|
|
28
|
+
isDebug ? { stack: exception.stack } : null,
|
|
29
|
+
);
|
|
50
30
|
}
|
|
51
31
|
|
|
52
32
|
module.exports = { loadConfig, handleException };
|
|
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|
|
5
5
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
6
6
|
require_once __DIR__ . '/modules/Response.php';
|
|
7
7
|
|
|
8
|
-
// --- GESTORE GLOBALE ERRORI E ECCEZIONI ---
|
|
9
8
|
set_exception_handler(function ($exception) use (&$config) {
|
|
10
9
|
$isDebug = ($config['APP_ENV'] ?? 'production') !== 'production';
|
|
11
10
|
Response::error(
|
|
@@ -20,11 +19,8 @@ set_error_handler(function ($severity, $message, $file, $line) {
|
|
|
20
19
|
throw new ErrorException($message, 0, $severity, $file, $line);
|
|
21
20
|
});
|
|
22
21
|
|
|
23
|
-
// --- CARICAMENTO CONFIGURAZIONE ---
|
|
24
|
-
// dirname(__DIR__) punta alla cartella /api/ dove ora si trova config.php
|
|
25
22
|
$config = require dirname(__DIR__) . '/config.php';
|
|
26
23
|
|
|
27
|
-
// --- CONFIGURAZIONE AMBIENTE ---
|
|
28
24
|
if (($config['APP_ENV'] ?? 'production') === 'production') {
|
|
29
25
|
ini_set('display_errors', '0');
|
|
30
26
|
error_reporting(0);
|
|
@@ -1,58 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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');
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
15
6
|
|
|
16
7
|
class RateLimiter {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
res.setHeader('Retry-After', String(windowSeconds));
|
|
53
|
-
Response.error('Too Many Requests', 429);
|
|
54
|
-
}
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} ip
|
|
10
|
+
* @param {number} maxRequests
|
|
11
|
+
* @param {number} windowSeconds
|
|
12
|
+
* @param {object} Response request-bound helper from createResponse(res)
|
|
13
|
+
* @param {object} res raw http response (to set Retry-After)
|
|
14
|
+
*/
|
|
15
|
+
static check(ip, maxRequests = 60, windowSeconds = 60, Response, res) {
|
|
16
|
+
const cacheDir = path.join(__dirname, "..", "..", "cache");
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(cacheDir)) {
|
|
19
|
+
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hash = crypto.createHash("md5").update(ip).digest("hex");
|
|
23
|
+
const cacheFile = path.join(cacheDir, "rl_" + hash + ".json");
|
|
24
|
+
const now = Math.floor(Date.now() / 1000);
|
|
25
|
+
let data = [];
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(cacheFile)) {
|
|
28
|
+
try {
|
|
29
|
+
data = JSON.parse(fs.readFileSync(cacheFile, "utf8")) || [];
|
|
30
|
+
} catch (e) {
|
|
31
|
+
data = [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data = data.filter((ts) => ts > now - windowSeconds);
|
|
36
|
+
data.push(now);
|
|
37
|
+
|
|
38
|
+
fs.writeFileSync(cacheFile, JSON.stringify(data));
|
|
39
|
+
|
|
40
|
+
if (data.length > maxRequests) {
|
|
41
|
+
res.setHeader("Retry-After", String(windowSeconds));
|
|
42
|
+
Response.error("Too Many Requests", 429);
|
|
55
43
|
}
|
|
44
|
+
}
|
|
56
45
|
}
|
|
57
46
|
|
|
58
47
|
module.exports = RateLimiter;
|
|
@@ -1,59 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Mirror of src/backend/_core/modules/Response.php
|
|
5
|
-
*
|
|
6
|
-
* In PHP the Response helper writes JSON and calls exit() to stop execution.
|
|
7
|
-
* Node has no per-request exit, so each method writes to the response and then
|
|
8
|
-
* throws the HALT sentinel. The front controller catches HALT and stops the
|
|
9
|
-
* dispatch, reproducing the same "the endpoint stops here" behaviour.
|
|
10
|
-
*
|
|
11
|
-
* A fresh, response-bound helper is created per request via createResponse(res)
|
|
12
|
-
* so overlapping async requests never clash (PHP was process-per-request).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// Sentinel thrown to unwind execution after a response has been sent (≈ PHP exit).
|
|
16
|
-
const HALT = Symbol('RESPONSE_HALT');
|
|
3
|
+
const HALT = Symbol("RESPONSE_HALT");
|
|
17
4
|
|
|
18
5
|
function jsonEncode(obj) {
|
|
19
|
-
|
|
20
|
-
// matching PHP's JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES.
|
|
21
|
-
return JSON.stringify(obj);
|
|
6
|
+
return JSON.stringify(obj);
|
|
22
7
|
}
|
|
23
8
|
|
|
24
9
|
function createResponse(res) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
10
|
+
return {
|
|
11
|
+
success(data = null, code = 200) {
|
|
12
|
+
res.statusCode = code;
|
|
13
|
+
res.setHeader("Content-Type", "application/json; charset=UTF-8");
|
|
14
|
+
res.end(
|
|
15
|
+
jsonEncode({
|
|
16
|
+
status: "success",
|
|
17
|
+
data: data,
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
throw HALT;
|
|
21
|
+
},
|
|
35
22
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
23
|
+
error(message, code = 400, details = null) {
|
|
24
|
+
res.statusCode = code;
|
|
25
|
+
res.setHeader("Content-Type", "application/json; charset=UTF-8");
|
|
26
|
+
const body = {
|
|
27
|
+
status: "error",
|
|
28
|
+
message: message,
|
|
29
|
+
code: code,
|
|
30
|
+
};
|
|
31
|
+
if (details !== null) {
|
|
32
|
+
body.details = details;
|
|
33
|
+
}
|
|
34
|
+
res.end(jsonEncode(body));
|
|
35
|
+
throw HALT;
|
|
36
|
+
},
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
noContent() {
|
|
39
|
+
res.statusCode = 204;
|
|
40
|
+
res.end();
|
|
41
|
+
throw HALT;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
57
44
|
}
|
|
58
45
|
|
|
59
46
|
module.exports = { createResponse, HALT };
|
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mirror of src/backend/api/protected/example-protected.php
|
|
5
|
-
*
|
|
6
|
-
* Protected endpoint: the API key check (X-Api-Key header) runs automatically in
|
|
7
|
-
* the front controller before this file is loaded, so here we just handle logic.
|
|
8
|
-
*/
|
|
1
|
+
"use strict";
|
|
9
2
|
|
|
10
3
|
module.exports = ({ method, requestParams, Response }) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
if (method !== "GET") {
|
|
5
|
+
Response.error("Method not allowed", 405);
|
|
6
|
+
}
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
//
|
|
9
|
+
// Your endpoint logic here. You can access route parameters in requestParams
|
|
10
|
+
//
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
Response.success({
|
|
13
|
+
message: "Protected endpoint is working",
|
|
14
|
+
params: requestParams,
|
|
15
|
+
});
|
|
23
16
|
};
|
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mirror of src/backend/api/public/example-public.php
|
|
5
|
-
*
|
|
6
|
-
* Public endpoint: no API key required. Receives the request context and calls
|
|
7
|
-
* the request-bound Response helper, exactly like the PHP file used the global
|
|
8
|
-
* Response class and $method / $requestParams.
|
|
9
|
-
*/
|
|
1
|
+
"use strict";
|
|
10
2
|
|
|
11
3
|
module.exports = ({ method, requestParams, Response }) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
if (method !== "GET") {
|
|
5
|
+
Response.error("Method not allowed", 405);
|
|
6
|
+
}
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
//
|
|
9
|
+
// Your endpoint logic here. You can access route parameters in requestParams
|
|
10
|
+
//
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
Response.success({
|
|
13
|
+
message: "Public endpoint is working",
|
|
14
|
+
params: requestParams,
|
|
15
|
+
});
|
|
24
16
|
};
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Mirror of src/backend/database/Database.php
|
|
5
|
-
*
|
|
6
|
-
* PDO singleton → a single shared mysql2 connection pool. Config is read from
|
|
7
|
-
* ../config.js (same relative location as the PHP version's ../config.php).
|
|
8
|
-
* Options map the PDO attributes as closely as mysql2 allows:
|
|
9
|
-
* - ERRMODE_EXCEPTION → mysql2 rejects/throws on error by default
|
|
10
|
-
* - FETCH_ASSOC → rowsAsArray:false (objects keyed by column) is default
|
|
11
|
-
* - EMULATE_PREPARES false → native prepared statements via pool.execute()
|
|
12
|
-
*
|
|
13
|
-
* Requires the "mysql2" package (npm install mysql2). getInstance() returns the
|
|
14
|
-
* promise-based pool; use pool.execute(sql, params) for prepared statements.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
3
|
const path = require('path');
|
|
18
4
|
|
|
19
5
|
let instance = null;
|
|
@@ -21,11 +7,8 @@ let instance = null;
|
|
|
21
7
|
class Database {
|
|
22
8
|
static getInstance() {
|
|
23
9
|
if (instance === null) {
|
|
24
|
-
// Lazy require so the backend boots even without a DB configured.
|
|
25
|
-
// eslint-disable-next-line global-require
|
|
26
10
|
const mysql = require('mysql2/promise');
|
|
27
11
|
|
|
28
|
-
// eslint-disable-next-line global-require
|
|
29
12
|
const config = require(path.join(__dirname, '..', 'config.js'));
|
|
30
13
|
|
|
31
14
|
instance = mysql.createPool({
|
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Mirror of src/backend/example.config.php
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* fill in real values. config.js is git-ignored and stays local.
|
|
6
|
+
* Copy this file to config.js and fill in real values. config.js is git-ignored and stays local.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
module.exports = {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
|
|
11
|
+
GENERAL_API_KEY: "DEFAULT_KEY",
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// If you want to restrict access to protected endpoints to specific clients, define custom keys per endpoint.
|
|
14
|
+
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
15
|
+
CUSTOM_ENDPOINT_KEYS: {
|
|
16
|
+
"subfolder/example-protected": "custom-key",
|
|
17
|
+
},
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
GENERAL_ALLOWED_ORIGINS: [
|
|
20
|
+
"*",
|
|
21
|
+
// 'https://example.com',
|
|
22
|
+
],
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
CUSTOM_ENDPOINT_ORIGINS: {
|
|
25
|
+
"subfolder/example-protected": ["https://app.example.com"],
|
|
26
|
+
},
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
// Database configuration
|
|
29
|
+
DB_HOST: "127.0.0.1",
|
|
30
|
+
DB_NAME: "example_db",
|
|
31
|
+
DB_USER: "root",
|
|
32
|
+
DB_PASS: "",
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// Environment: 'production' hides error details; anything else = debug.
|
|
35
|
+
APP_ENV: "production",
|
|
37
36
|
};
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
declare(strict_types=1);
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Mirror of src/backend/example.config.
|
|
5
|
+
* Mirror of src/backend/example.config.php
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* fill in real values. config.php is git-ignored and stays local.
|
|
7
|
+
* Copy this file to config.php and fill in real values. config.php is git-ignored and stays local.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
return [
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
// @import "../modules/moduleName";
|
|
9
9
|
|
|
10
10
|
// Add any custom rule specific to this page below:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
.not-found {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
align-items: center;
|
|
16
|
+
min-height: 60vh;
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|