create-berna-stencil 2.4.2 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/create.js +2 -1
- package/docs/Backend.md +33 -8
- package/nginx.conf +69 -0
- package/package.json +2 -1
- package/src/backend/_core/index.php +23 -6
- package/src/backend/config.example.php +13 -4
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Building a website from scratch involves a lot of moving parts: templating engin
|
|
|
12
12
|
- 📁 **Scalable structure** — a clean, opinionated project layout that grows with your needs
|
|
13
13
|
- 🌍 **Open source** — free to use, free to modify, free to share
|
|
14
14
|
|
|
15
|
-

|
|
16
16
|

|
|
17
17
|

|
|
18
18
|
|
package/bin/create.js
CHANGED
|
@@ -47,6 +47,7 @@ const MANDATORY_COPY = [
|
|
|
47
47
|
'_tools',
|
|
48
48
|
'.eleventy.js',
|
|
49
49
|
'.eleventyignore',
|
|
50
|
+
'nginx.conf',
|
|
50
51
|
'src/backend',
|
|
51
52
|
'src/frontend',
|
|
52
53
|
];
|
|
@@ -120,7 +121,7 @@ src/backend/config.php
|
|
|
120
121
|
|
|
121
122
|
const PROJECT_PACKAGE = {
|
|
122
123
|
name: path.basename(targetDir),
|
|
123
|
-
version: '2.
|
|
124
|
+
version: '2.5.0',
|
|
124
125
|
private: true,
|
|
125
126
|
outputDir: 'out',
|
|
126
127
|
"scripts": {
|
package/docs/Backend.md
CHANGED
|
@@ -32,13 +32,23 @@ Copy `config.example.php` to `config.php` and fill in your values:
|
|
|
32
32
|
### config.php <small>(`src/backend/`)</small>
|
|
33
33
|
```php
|
|
34
34
|
return [
|
|
35
|
-
// Default key for protected endpoints that don't have a specific key in
|
|
36
|
-
'
|
|
35
|
+
// Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
|
|
36
|
+
'GENERAL_API_KEY' => 'default-key',
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
'
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
// Per-endpoint keys. For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
39
|
+
'CUSTOM_ENDPOINT_KEYS' => [
|
|
40
|
+
// 'subfolder/endpoint' => 'example-key',
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
// Default CORS origins applied to every endpoint
|
|
44
|
+
'GENERAL_ALLOWED_ORIGINS' => [
|
|
45
|
+
'https://example.com',
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
// Per-endpoint CORS origins. Same path format as CUSTOM_ENDPOINT_KEYS.
|
|
49
|
+
// When an endpoint is listed here, these origins replace GENERAL_ALLOWED_ORIGINS for it.
|
|
50
|
+
'CUSTOM_ENDPOINT_ORIGINS' => [
|
|
51
|
+
// 'subfolder/endpoint' => ['https://app.example.com'],
|
|
42
52
|
],
|
|
43
53
|
|
|
44
54
|
'DB_HOST' => '127.0.0.1',
|
|
@@ -48,7 +58,22 @@ return [
|
|
|
48
58
|
];
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
### API keys
|
|
62
|
+
|
|
63
|
+
`GENERAL_API_KEY` is the fallback key for all protected endpoints. Use `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.
|
|
64
|
+
|
|
65
|
+
> ⚠️ The API key travels in the `X-Api-Key` header on every request. Use it only for server-to-server calls over HTTPS. Never embed it in frontend code, where it would be publicly visible.
|
|
66
|
+
|
|
67
|
+
### CORS (allowed origins)
|
|
68
|
+
|
|
69
|
+
Cross-origin access mirrors the API-key model:
|
|
70
|
+
|
|
71
|
+
- `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any endpoint from a browser.
|
|
72
|
+
- `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint, keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
|
|
73
|
+
|
|
74
|
+
Origins must be exact (scheme + host, no trailing slash), e.g. `https://example.com`. When a request's `Origin` is in the resolved list, it is reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty list sends no CORS header — the most restrictive setting; same-origin requests still work. Use `'*'` as the only element to allow any origin (not recommended for protected endpoints).
|
|
75
|
+
|
|
76
|
+
Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise `GENERAL_ALLOWED_ORIGINS`.
|
|
52
77
|
|
|
53
78
|
## How routing works
|
|
54
79
|
|
|
@@ -102,7 +127,7 @@ Response::success(['visits' => 1024]);
|
|
|
102
127
|
To assign a dedicated key, add it to `config.php`:
|
|
103
128
|
|
|
104
129
|
```php
|
|
105
|
-
'
|
|
130
|
+
'CUSTOM_ENDPOINT_KEYS' => [
|
|
106
131
|
'endpoint' => 'custom-key',
|
|
107
132
|
],
|
|
108
133
|
```
|
package/nginx.conf
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Redirect all HTTP traffic to HTTPS
|
|
2
|
+
server {
|
|
3
|
+
listen 80;
|
|
4
|
+
server_name _;
|
|
5
|
+
return 301 https://$host$request_uri;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
server {
|
|
9
|
+
listen 443 ssl;
|
|
10
|
+
server_name _;
|
|
11
|
+
|
|
12
|
+
# SSL certificate — replace with your own paths.
|
|
13
|
+
# Let's Encrypt default shown below; adjust the domain.
|
|
14
|
+
ssl_certificate /etc/letsencrypt/live/your-domain/fullchain.pem;
|
|
15
|
+
ssl_certificate_key /etc/letsencrypt/live/your-domain/privkey.pem;
|
|
16
|
+
|
|
17
|
+
# Hide the nginx version from response headers
|
|
18
|
+
server_tokens off;
|
|
19
|
+
|
|
20
|
+
# Force HTTPS on future requests (applies site-wide, including /api)
|
|
21
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
22
|
+
|
|
23
|
+
root /var/www/html/out;
|
|
24
|
+
index index.html;
|
|
25
|
+
|
|
26
|
+
# Disable directory listing
|
|
27
|
+
autoindex off;
|
|
28
|
+
|
|
29
|
+
error_page 403 404 /404.html;
|
|
30
|
+
location = /404.html {
|
|
31
|
+
internal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Never expose the backend source directory
|
|
35
|
+
location ^~ /backend/ {
|
|
36
|
+
return 404;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Block server config files anywhere in the tree (.htaccess, web.config).
|
|
40
|
+
# return 404 is served internally as /404.html via error_page above.
|
|
41
|
+
location ~* (^|/)(\.htaccess|web\.config)$ {
|
|
42
|
+
return 404;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Block dotfiles (.git, .env, etc.)
|
|
46
|
+
location ~ /\. {
|
|
47
|
+
return 404;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Route all /api/ requests to the PHP front controller.
|
|
51
|
+
# Security headers for API responses are set by the front controller (PHP).
|
|
52
|
+
location ^~ /api/ {
|
|
53
|
+
try_files $uri =404;
|
|
54
|
+
fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
|
|
55
|
+
fastcgi_param SCRIPT_FILENAME $document_root/backend/_core/index.php;
|
|
56
|
+
include fastcgi_params;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Serve static files, fall back to the 404 page.
|
|
60
|
+
# A location with add_header does not inherit server-level headers,
|
|
61
|
+
# so Strict-Transport-Security is repeated here.
|
|
62
|
+
location / {
|
|
63
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
64
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
65
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
66
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
67
|
+
try_files $uri $uri/ /404.html;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-berna-stencil",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "Michele Garofalo",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"_tools/",
|
|
24
24
|
".eleventy.js",
|
|
25
25
|
".eleventyignore",
|
|
26
|
+
"nginx.conf",
|
|
26
27
|
".gitignore",
|
|
27
28
|
"tsconfig.json",
|
|
28
29
|
"LICENSE",
|
|
@@ -87,14 +87,34 @@ if (!$endpointFile) {
|
|
|
87
87
|
exit;
|
|
88
88
|
}
|
|
89
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
|
+
|
|
90
95
|
// =====================================================
|
|
91
96
|
// 3. HEADERS AND CORS (Only if the endpoint exists)
|
|
92
97
|
// =====================================================
|
|
93
98
|
|
|
94
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');
|
|
95
103
|
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
96
104
|
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
|
|
97
|
-
|
|
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
|
+
}
|
|
98
118
|
|
|
99
119
|
if ($method === 'OPTIONS') {
|
|
100
120
|
http_response_code(204);
|
|
@@ -109,11 +129,8 @@ RateLimiter::check($_SERVER['REMOTE_ADDR'] ?? '', 60, 60);
|
|
|
109
129
|
// =====================================================
|
|
110
130
|
|
|
111
131
|
if ($isProtected) {
|
|
112
|
-
$
|
|
113
|
-
$
|
|
114
|
-
|
|
115
|
-
$validKey = $config['ENDPOINT_KEYS'][$relPath] ?? $config['API_KEY'] ?? '';
|
|
116
|
-
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
|
132
|
+
$validKey = $config['CUSTOM_ENDPOINT_KEYS'][$endpointPath] ?? $config['GENERAL_API_KEY'] ?? '';
|
|
133
|
+
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
|
117
134
|
|
|
118
135
|
if ($validKey === '' || !hash_equals($validKey, $apiKey)) {
|
|
119
136
|
Response::error('Unauthorized. X_API_KEY is incorrect or missing', 401);
|
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
declare(strict_types=1);
|
|
3
3
|
|
|
4
4
|
return [
|
|
5
|
-
// Default key for protected endpoints that don't have a specific key in
|
|
6
|
-
'
|
|
5
|
+
// Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
|
|
6
|
+
'GENERAL_API_KEY' => 'DEFAULT_KEY',
|
|
7
7
|
|
|
8
8
|
// If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
|
|
9
9
|
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
10
|
-
'
|
|
11
|
-
|
|
10
|
+
'CUSTOM_ENDPOINT_KEYS' => [
|
|
11
|
+
'subfolder/example-protected' => 'custom-key',
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
'GENERAL_ALLOWED_ORIGINS' => [
|
|
15
|
+
'*',
|
|
16
|
+
// 'https://example.com',
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
'CUSTOM_ENDPOINT_ORIGINS' => [
|
|
20
|
+
'subfolder/example-protected' => ['https://app.example.com'],
|
|
12
21
|
],
|
|
13
22
|
|
|
14
23
|
// Database configuration
|