create-berna-stencil 2.0.5 → 2.0.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/_tools/res/templates/template.js +3 -3
- package/bin/create.js +213 -35
- package/docs/Backend.md +13 -106
- package/package.json +2 -2
- package/src/backend/config.example.php +3 -2
- package/src/backend/config.php +2 -1
- package/src/frontend/components/welcome.njk +157 -194
- package/src/frontend/js/pages/404.js +5 -3
- package/src/frontend/js/pages/homepage.js +5 -3
- package/src/frontend/llms.njk +1 -1
- package/src/frontend/scss/modules/_global.scss +6 -0
- package/src/frontend/scss/pages/404.scss +1 -1
- package/src/frontend/scss/pages/homepage.scss +1 -1
- package/src/backend/api/protected/auth-system.php +0 -67
- package/src/backend/api/public/auth/login.php +0 -38
- package/src/backend/api/public/auth/register.php +0 -44
- package/src/backend/database/migrations/create_users_table.sql +0 -9
- package/src/backend/database/models/User.php +0 -61
- /package/src/backend/api/protected/{subfolder/example-protected.php → example-protected.php} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
//===========================
|
|
2
2
|
// JAVASCRIPT MODULES IMPORTS
|
|
3
|
-
|
|
3
|
+
//===========================
|
|
4
4
|
|
|
5
5
|
// Call anywhere
|
|
6
6
|
import { showNotification } from '../modules/notification.js';
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// Uncomment of pre-existing modules
|
|
9
9
|
// import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
|
|
10
10
|
// import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
11
11
|
|
package/bin/create.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
5
6
|
|
|
6
7
|
const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
7
8
|
const templateDir = path.join(__dirname, '..');
|
|
@@ -15,7 +16,7 @@ const COPY_TARGETS = [
|
|
|
15
16
|
|
|
16
17
|
const PROJECT_PACKAGE = {
|
|
17
18
|
name: path.basename(targetDir),
|
|
18
|
-
version: '2.0.
|
|
19
|
+
version: '2.0.6',
|
|
19
20
|
private: true,
|
|
20
21
|
scripts: {
|
|
21
22
|
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
@@ -54,12 +55,63 @@ out/
|
|
|
54
55
|
src/backend/config.php
|
|
55
56
|
`;
|
|
56
57
|
|
|
58
|
+
// Framework configurations
|
|
59
|
+
const FRAMEWORKS = {
|
|
60
|
+
bootstrap: {
|
|
61
|
+
label: 'Bootstrap',
|
|
62
|
+
scss: 'bootstrap',
|
|
63
|
+
njk: [
|
|
64
|
+
'<script src="/js/bootstrap.bundle.min.js" defer></script>',
|
|
65
|
+
],
|
|
66
|
+
eleventy: [
|
|
67
|
+
'"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
|
|
68
|
+
'"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
bulma: {
|
|
72
|
+
label: 'Bulma',
|
|
73
|
+
scss: 'bulma',
|
|
74
|
+
njk: [],
|
|
75
|
+
eleventy: [],
|
|
76
|
+
},
|
|
77
|
+
foundation: {
|
|
78
|
+
label: 'Foundation',
|
|
79
|
+
scss: 'foundation',
|
|
80
|
+
njk: ['<script src="/js/foundation.min.js" defer></script>'],
|
|
81
|
+
eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
|
|
82
|
+
},
|
|
83
|
+
uikit: {
|
|
84
|
+
label: 'UIkit',
|
|
85
|
+
scss: 'uikit',
|
|
86
|
+
njk: [
|
|
87
|
+
'<script src="/js/uikit.min.js" defer></script>',
|
|
88
|
+
'<script src="/js/uikit-icons.min.js" defer></script>',
|
|
89
|
+
],
|
|
90
|
+
eleventy: [
|
|
91
|
+
'"node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",',
|
|
92
|
+
'"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
none: {
|
|
96
|
+
label: 'None',
|
|
97
|
+
scss: null,
|
|
98
|
+
njk: [],
|
|
99
|
+
eleventy: [],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const ALL_FRAMEWORKS = ['bootstrap', 'bulma', 'foundation', 'uikit'];
|
|
104
|
+
|
|
57
105
|
const { writeSync } = require('fs');
|
|
58
106
|
|
|
59
107
|
function log(msg) {
|
|
60
108
|
writeSync(1, msg + '\n');
|
|
61
109
|
}
|
|
62
110
|
|
|
111
|
+
function escapeRegex(str) {
|
|
112
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
function copyRecursive(src, dest) {
|
|
64
116
|
const stat = fs.statSync(src);
|
|
65
117
|
if (stat.isDirectory()) {
|
|
@@ -85,45 +137,171 @@ function deleteFileRecursive(dir, filename) {
|
|
|
85
137
|
}
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
140
|
+
function slashComment(content, line) {
|
|
141
|
+
content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
142
|
+
return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
|
|
90
143
|
}
|
|
91
144
|
|
|
92
|
-
|
|
145
|
+
function slashUncomment(content, line) {
|
|
146
|
+
return content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function njkComment(content, line) {
|
|
150
|
+
content = content.split(`{# ${line} #}`).join(line);
|
|
151
|
+
return content.split(line).join(`{# ${line} #}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function njkUncomment(content, line) {
|
|
155
|
+
return content.split(`{# ${line} #}`).join(line);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function applyFramework(framework) {
|
|
159
|
+
const config = FRAMEWORKS[framework];
|
|
160
|
+
|
|
161
|
+
const globalScssPath = path.join(targetDir, 'src/frontend/scss/modules/_global.scss');
|
|
162
|
+
if (fs.existsSync(globalScssPath)) {
|
|
163
|
+
let content = fs.readFileSync(globalScssPath, 'utf8');
|
|
164
|
+
ALL_FRAMEWORKS.forEach(fw => {
|
|
165
|
+
content = slashComment(content, `@import "../modules/frameworks/${fw}";`);
|
|
166
|
+
});
|
|
167
|
+
if (config.scss) {
|
|
168
|
+
content = slashUncomment(content, `@import "../modules/frameworks/${config.scss}";`);
|
|
169
|
+
}
|
|
170
|
+
fs.writeFileSync(globalScssPath, content);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const baseNjkPath = path.join(targetDir, 'src/frontend/components/layouts/base.njk');
|
|
174
|
+
if (fs.existsSync(baseNjkPath)) {
|
|
175
|
+
let content = fs.readFileSync(baseNjkPath, 'utf8');
|
|
176
|
+
ALL_FRAMEWORKS.forEach(fw => {
|
|
177
|
+
FRAMEWORKS[fw].njk.forEach(line => {
|
|
178
|
+
content = njkComment(content, line);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
config.njk.forEach(line => {
|
|
182
|
+
content = njkUncomment(content, line);
|
|
183
|
+
});
|
|
184
|
+
fs.writeFileSync(baseNjkPath, content);
|
|
185
|
+
}
|
|
93
186
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
187
|
+
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
188
|
+
if (fs.existsSync(eleventyPath)) {
|
|
189
|
+
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
190
|
+
ALL_FRAMEWORKS.forEach(fw => {
|
|
191
|
+
FRAMEWORKS[fw].eleventy.forEach(line => {
|
|
192
|
+
content = slashComment(content, line);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
config.eleventy.forEach(line => {
|
|
196
|
+
content = slashUncomment(content, line);
|
|
197
|
+
});
|
|
198
|
+
fs.writeFileSync(eleventyPath, content);
|
|
100
199
|
}
|
|
101
200
|
}
|
|
102
201
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
202
|
+
function askFramework() {
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
const choices = [
|
|
205
|
+
{ label: 'Bootstrap', value: 'bootstrap' },
|
|
206
|
+
{ label: 'Bulma', value: 'bulma' },
|
|
207
|
+
{ label: 'Foundation', value: 'foundation' },
|
|
208
|
+
{ label: 'UIkit', value: 'uikit' },
|
|
209
|
+
{ label: 'None', value: 'none' }
|
|
210
|
+
];
|
|
211
|
+
let selectedIndex = 0;
|
|
212
|
+
|
|
213
|
+
log('\n>> Select a CSS framework (Use arrow keys and press Enter):\n');
|
|
214
|
+
|
|
215
|
+
const render = (firstTime = false) => {
|
|
216
|
+
if (!firstTime) {
|
|
217
|
+
process.stdout.write(`\x1B[${choices.length}A`);
|
|
218
|
+
}
|
|
219
|
+
let output = '';
|
|
220
|
+
choices.forEach((choice, index) => {
|
|
221
|
+
if (index === selectedIndex) {
|
|
222
|
+
output += ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`;
|
|
223
|
+
} else {
|
|
224
|
+
output += ` * ${choice.label}\x1B[K\n`;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
process.stdout.write(output);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
readline.emitKeypressEvents(process.stdin);
|
|
231
|
+
if (process.stdin.isTTY) {
|
|
232
|
+
process.stdin.setRawMode(true);
|
|
233
|
+
}
|
|
234
|
+
process.stdin.resume();
|
|
235
|
+
|
|
236
|
+
const onKeyPress = (str, key) => {
|
|
237
|
+
if (key.ctrl && key.name === 'c') {
|
|
238
|
+
process.exit();
|
|
239
|
+
} else if (key.name === 'up') {
|
|
240
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
|
|
241
|
+
render();
|
|
242
|
+
} else if (key.name === 'down') {
|
|
243
|
+
selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
|
|
244
|
+
render();
|
|
245
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
246
|
+
process.stdin.removeListener('keypress', onKeyPress);
|
|
247
|
+
if (process.stdin.isTTY) {
|
|
248
|
+
process.stdin.setRawMode(false);
|
|
249
|
+
}
|
|
250
|
+
process.stdin.pause();
|
|
251
|
+
resolve(choices[selectedIndex].value);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
process.stdin.on('keypress', onKeyPress);
|
|
256
|
+
render(true);
|
|
257
|
+
});
|
|
109
258
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
fs.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
log(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
259
|
+
|
|
260
|
+
async function init() {
|
|
261
|
+
if (!fs.existsSync(targetDir)) {
|
|
262
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
log(`\n>> Creating berna-stencil project in ${targetDir}\n`);
|
|
266
|
+
|
|
267
|
+
for (const target of COPY_TARGETS) {
|
|
268
|
+
const src = path.join(templateDir, target);
|
|
269
|
+
const dest = path.join(targetDir, target);
|
|
270
|
+
if (fs.existsSync(src)) {
|
|
271
|
+
copyRecursive(src, dest);
|
|
272
|
+
log(`+ ${target}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const configDest = path.join(targetDir, 'src/backend/config.php');
|
|
277
|
+
const configExample = path.join(targetDir, 'src/backend/config.example.php');
|
|
278
|
+
if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
|
|
279
|
+
fs.copyFileSync(configExample, configDest);
|
|
280
|
+
log('+ src/backend/config.php');
|
|
281
|
+
}
|
|
282
|
+
deleteFileRecursive(targetDir, 'config.example.php');
|
|
283
|
+
|
|
284
|
+
fs.writeFileSync(
|
|
285
|
+
path.join(targetDir, 'package.json'),
|
|
286
|
+
JSON.stringify(PROJECT_PACKAGE, null, 2)
|
|
287
|
+
);
|
|
288
|
+
log('+ package.json');
|
|
289
|
+
|
|
290
|
+
fs.writeFileSync(
|
|
291
|
+
path.join(targetDir, '.gitignore'),
|
|
292
|
+
GITIGNORE_CONTENT
|
|
293
|
+
);
|
|
294
|
+
log('+ .gitignore');
|
|
295
|
+
|
|
296
|
+
const framework = await askFramework();
|
|
297
|
+
applyFramework(framework);
|
|
298
|
+
|
|
299
|
+
log(`\n>> Done! Now run:\n`);
|
|
300
|
+
if (process.argv[2]) {
|
|
301
|
+
log(`cd ${process.argv[2]}`);
|
|
302
|
+
}
|
|
303
|
+
log('npm install');
|
|
304
|
+
log('npm run serve\n');
|
|
127
305
|
}
|
|
128
|
-
|
|
129
|
-
|
|
306
|
+
|
|
307
|
+
init();
|
package/docs/Backend.md
CHANGED
|
@@ -9,7 +9,7 @@ src/backend/
|
|
|
9
9
|
├── api/
|
|
10
10
|
│ ├── public/ # Endpoints accessible without an API key
|
|
11
11
|
│ └── protected/ # Endpoints requiring X-Api-Key header
|
|
12
|
-
├──
|
|
12
|
+
├── db/
|
|
13
13
|
│ ├── Database.php
|
|
14
14
|
│ ├── models/
|
|
15
15
|
│ └── migrations/
|
|
@@ -26,11 +26,13 @@ Copy `config.example.php` to `config.php` and fill in your values:
|
|
|
26
26
|
### config.php <small>(`src/backend/`)</small>
|
|
27
27
|
```php
|
|
28
28
|
return [
|
|
29
|
-
|
|
30
|
-
'API_KEY' => '
|
|
29
|
+
// Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
|
|
30
|
+
'API_KEY' => 'default-key',
|
|
31
31
|
|
|
32
|
+
// If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
|
|
33
|
+
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
32
34
|
'ENDPOINT_KEYS' => [
|
|
33
|
-
'
|
|
35
|
+
'example-protected' => 'custom-key',
|
|
34
36
|
],
|
|
35
37
|
|
|
36
38
|
'DB_HOST' => '127.0.0.1',
|
|
@@ -57,7 +59,7 @@ Every endpoint file has access to:
|
|
|
57
59
|
|
|
58
60
|
Create a `.php` file anywhere inside `api/public/`
|
|
59
61
|
|
|
60
|
-
### api/public/
|
|
62
|
+
### api/public/example.php
|
|
61
63
|
```php
|
|
62
64
|
<?php
|
|
63
65
|
declare(strict_types=1);
|
|
@@ -73,13 +75,11 @@ $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
|
73
75
|
Response::success(['id' => $id]);
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
Reachable at `/api/posts` or `/api/posts/42`
|
|
77
|
-
|
|
78
78
|
## Creating a protected endpoint
|
|
79
79
|
|
|
80
80
|
Create a `.php` file inside `api/protected/`. The API key check happens automatically before your file runs.
|
|
81
81
|
|
|
82
|
-
### api/protected/
|
|
82
|
+
### api/protected/example.php
|
|
83
83
|
```php
|
|
84
84
|
<?php
|
|
85
85
|
declare(strict_types=1);
|
|
@@ -97,7 +97,7 @@ To assign a dedicated key, add it to `config.php`:
|
|
|
97
97
|
|
|
98
98
|
```php
|
|
99
99
|
'ENDPOINT_KEYS' => [
|
|
100
|
-
'
|
|
100
|
+
'endpoint' => 'custom-key',
|
|
101
101
|
],
|
|
102
102
|
```
|
|
103
103
|
|
|
@@ -109,102 +109,9 @@ Response::error($message, $code, $details); // default 400
|
|
|
109
109
|
Response::noContent(); // 204
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
## Handling multiple methods
|
|
113
|
-
|
|
114
|
-
```php
|
|
115
|
-
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
116
|
-
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
117
|
-
|
|
118
|
-
switch ($method) {
|
|
119
|
-
case 'GET':
|
|
120
|
-
Response::success(['id' => $id]);
|
|
121
|
-
break;
|
|
122
|
-
|
|
123
|
-
case 'POST':
|
|
124
|
-
if (empty($input['title'])) Response::error('Missing title', 400);
|
|
125
|
-
Response::success(['message' => 'Created'], 201);
|
|
126
|
-
break;
|
|
127
|
-
|
|
128
|
-
case 'DELETE':
|
|
129
|
-
if (!$id) Response::error('ID required', 400);
|
|
130
|
-
Response::success(['message' => 'Deleted']);
|
|
131
|
-
break;
|
|
132
|
-
|
|
133
|
-
default:
|
|
134
|
-
Response::error('Method not allowed', 405);
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Using the database
|
|
139
|
-
|
|
140
|
-
### database/models/Post.php
|
|
141
|
-
```php
|
|
142
|
-
<?php
|
|
143
|
-
declare(strict_types=1);
|
|
144
|
-
|
|
145
|
-
require_once __DIR__ . '/../Database.php';
|
|
146
|
-
|
|
147
|
-
class Post {
|
|
148
|
-
private PDO $db;
|
|
149
|
-
|
|
150
|
-
public function __construct() {
|
|
151
|
-
$this->db = Database::getInstance();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
public function getAll(): array {
|
|
155
|
-
return $this->db->query("SELECT * FROM posts")->fetchAll();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
public function getById(int $id): ?array {
|
|
159
|
-
$stmt = $this->db->prepare("SELECT * FROM posts WHERE id = :id");
|
|
160
|
-
$stmt->execute(['id' => $id]);
|
|
161
|
-
return $stmt->fetch() ?: null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
public function create(string $title): int {
|
|
165
|
-
$stmt = $this->db->prepare("INSERT INTO posts (title) VALUES (:title)");
|
|
166
|
-
$stmt->execute(['title' => htmlspecialchars(strip_tags(trim($title)))]);
|
|
167
|
-
return (int)$this->db->lastInsertId();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Then use it inside an endpoint:
|
|
173
|
-
|
|
174
|
-
```php
|
|
175
|
-
require_once __DIR__ . '/../../database/models/Post.php';
|
|
176
|
-
|
|
177
|
-
$post = new Post();
|
|
178
|
-
Response::success($post->getAll());
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Migrations live in `database/migrations/` as plain SQL files — run them manually against your database.
|
|
182
|
-
|
|
183
|
-
## Calling endpoints from the frontend
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
// Public
|
|
187
|
-
const res = await fetch('/api/posts/42');
|
|
188
|
-
|
|
189
|
-
// Protected
|
|
190
|
-
const res = await fetch('/api/admin/stats', {
|
|
191
|
-
headers: { 'X-Api-Key': 'secret-stats-key' }
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// POST
|
|
195
|
-
const res = await fetch('/api/posts', {
|
|
196
|
-
method: 'POST',
|
|
197
|
-
headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'your-key' },
|
|
198
|
-
body: JSON.stringify({ title: 'Hello world' })
|
|
199
|
-
});
|
|
200
|
-
```
|
|
201
|
-
|
|
202
112
|
## Pre-built endpoints
|
|
203
113
|
|
|
204
|
-
| Route |
|
|
205
|
-
|
|
206
|
-
| `/api/example-public` |
|
|
207
|
-
| `/api/
|
|
208
|
-
| `/api/auth/register` | No | `POST` | Register a new user |
|
|
209
|
-
| `/api/auth/login` | No | `POST` | Login and retrieve user data |
|
|
210
|
-
| `/api/auth-system` | Yes | `GET POST PUT PATCH DELETE` | Full CRUD on users |
|
|
114
|
+
| Route | Method | Description |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
|
|
117
|
+
| `/api/example-protected` | `GET` | Example endpoint that requires X-API-KEY |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-berna-stencil",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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",
|
|
@@ -58,4 +58,4 @@
|
|
|
58
58
|
"assistant": "node _tools/assistant.js",
|
|
59
59
|
"postinstall": "cd src/backend/_core && composer install --quiet"
|
|
60
60
|
}
|
|
61
|
-
}
|
|
61
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
declare(strict_types=1);
|
|
3
3
|
|
|
4
4
|
return [
|
|
5
|
-
|
|
5
|
+
// Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
|
|
6
|
+
'API_KEY' => 'DEFAULT_KEY',
|
|
6
7
|
|
|
7
8
|
// If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
|
|
8
9
|
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
9
10
|
'ENDPOINT_KEYS' => [
|
|
10
|
-
'subfolder/example-protected' => '
|
|
11
|
+
'subfolder/example-protected' => 'custom-key',
|
|
11
12
|
],
|
|
12
13
|
|
|
13
14
|
// Database configuration
|
package/src/backend/config.php
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
declare(strict_types=1);
|
|
3
3
|
|
|
4
4
|
return [
|
|
5
|
-
|
|
5
|
+
// Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
|
|
6
|
+
'API_KEY' => 'DEFAULT_KEY',
|
|
6
7
|
|
|
7
8
|
// If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
|
|
8
9
|
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
@@ -41,26 +41,26 @@
|
|
|
41
41
|
|
|
42
42
|
<div id="content-welcome" class="tab-content active">
|
|
43
43
|
<div class="grid" style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
44
|
-
<a href="https://
|
|
44
|
+
<a href="https://github.com/rhaastrake/berna-stencil" class="card" target="_blank" rel="noopener noreferrer" style="flex: 1; min-width: 250px;">
|
|
45
45
|
<svg class="card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
46
|
-
<path d="
|
|
47
|
-
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
|
46
|
+
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
|
48
47
|
</svg>
|
|
49
|
-
<h3>
|
|
50
|
-
<p>
|
|
51
|
-
<span class="card-link">
|
|
48
|
+
<h3>Github repository</h3>
|
|
49
|
+
<p>Community-driven. Contributions, issues and PRs are welcome.</p>
|
|
50
|
+
<span class="card-link">Open the repository
|
|
52
51
|
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="margin-left:4px;">
|
|
53
52
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
|
54
53
|
</svg>
|
|
55
54
|
</span>
|
|
56
55
|
</a>
|
|
57
|
-
<a href="https://github.com/
|
|
56
|
+
<a href="https://github.com/Rhaastrake/Berna-Stencil/tree/main/docs" class="card" target="_blank" rel="noopener noreferrer" style="flex: 1; min-width: 250px;">
|
|
58
57
|
<svg class="card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
59
|
-
<path d="
|
|
58
|
+
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
|
59
|
+
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
|
60
60
|
</svg>
|
|
61
|
-
<h3>
|
|
62
|
-
<p>
|
|
63
|
-
<span class="card-link">
|
|
61
|
+
<h3>Documentation</h3>
|
|
62
|
+
<p>Everything you need to get started, from setup to advanced topics and customizations</p>
|
|
63
|
+
<span class="card-link">Go to documentation
|
|
64
64
|
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="margin-left:4px;">
|
|
65
65
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
|
66
66
|
</svg>
|
|
@@ -237,13 +237,13 @@ body {
|
|
|
237
237
|
<h4>examplePage.js <small>(<code>src/frontend/js/pages/</code>)</small></h4>
|
|
238
238
|
<pre><code class="language-js">import { showNotification } from '../modules/notification.js';
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
243
|
+
initNormalizePhoneNumber();
|
|
244
|
+
});
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
showNotification("Page loaded", "success", 3000);</code></pre>
|
|
247
247
|
|
|
248
248
|
<h2>Modules</h2>
|
|
249
249
|
<p>Modules live in <code>src/frontend/js/modules/</code>. Some must be called inside <code>DOMContentLoaded</code> as they interact with the DOM; others create elements dynamically and can be called anywhere.</p>
|
|
@@ -287,10 +287,11 @@ body {
|
|
|
287
287
|
<p>Create a new <code>.js</code> file in <code>src/frontend/js/modules/</code>. You can organize them into subfolders freely.</p>
|
|
288
288
|
<p>Use ESM syntax — esbuild handles the bundling:</p>
|
|
289
289
|
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
<h4>yourModule.js <small>(<code>src/frontend/js/modules/</code>)</small></h4>
|
|
291
|
+
<pre><code class="language-js">
|
|
292
|
+
export function yourFunction() {
|
|
293
|
+
// ...
|
|
294
|
+
}</code></pre>
|
|
294
295
|
|
|
295
296
|
<p>Then import it in the pages that need it:</p>
|
|
296
297
|
|
|
@@ -403,17 +404,17 @@ body {
|
|
|
403
404
|
|
|
404
405
|
<h3>site.json <small>(<code>src/frontend/data/</code>)</small></h3>
|
|
405
406
|
<pre><code>"site_name": "Site name",
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
407
|
+
"title": "Site title",
|
|
408
|
+
"description": "Site description",
|
|
409
|
+
"keywords": "keyword1, keyword2, keyword3",
|
|
410
|
+
"domain": "yoursite.com",
|
|
411
|
+
"url": "https://yoursite.com",
|
|
412
|
+
"lang": "en",
|
|
413
|
+
"author": "Name and surname",
|
|
414
|
+
"data_bs_theme": "dark",
|
|
415
|
+
"favicon": "/assets/brand/favicon.svg",
|
|
416
|
+
"logo": "/assets/brand/logo.svg",
|
|
417
|
+
...</code></pre>
|
|
417
418
|
|
|
418
419
|
<h2>Per-page SEO and CDN</h2>
|
|
419
420
|
<p>Each page entry is keyed by its camelCase <code>title</code> from the front matter:</p>
|
|
@@ -421,20 +422,20 @@ body {
|
|
|
421
422
|
|
|
422
423
|
<h3>site.json <small>(<code>src/frontend/data/</code>)</small></h3>
|
|
423
424
|
<pre><code>"pages": {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
425
|
+
...
|
|
426
|
+
"examplePage": {
|
|
427
|
+
"seo": {
|
|
428
|
+
"title": "Example Page",
|
|
429
|
+
"description": "description"
|
|
430
|
+
},
|
|
431
|
+
"cdn": {
|
|
432
|
+
// You can leave the [] empty
|
|
433
|
+
"css": ["https://example1.com/lib.min.css", "https://example2.com/lib.min.css"],
|
|
434
|
+
"js": ["https://example1.com/lib.min.js", "https://example2.com/lib.min.js"]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
...
|
|
438
|
+
}</code></pre>
|
|
438
439
|
|
|
439
440
|
<h2>AI & SEO bots</h2>
|
|
440
441
|
<p><code>llms.txt</code> and <code>robots.txt</code> are generated automatically from <code>site.json</code> via their respective <code>.njk</code> files — no manual editing needed.</p>
|
|
@@ -454,20 +455,21 @@ body {
|
|
|
454
455
|
<h3>Customizing llms.txt</h3>
|
|
455
456
|
<p><code>src/llms.njk</code> ships with a base template — <strong>replace the placeholders with your own content</strong>:</p>
|
|
456
457
|
|
|
457
|
-
<pre><code
|
|
458
|
+
<pre><code>{% raw %}# {{ site.site_name }}
|
|
458
459
|
|
|
459
|
-
|
|
460
|
+
> {{ site.description }}
|
|
460
461
|
|
|
461
|
-
|
|
462
|
+
Built by {{ site.author }} — {{ site.url }}
|
|
462
463
|
|
|
463
|
-
|
|
464
|
+
## Pages
|
|
464
465
|
|
|
465
|
-
|
|
466
|
+
- {{ site.url }}: Homepage
|
|
466
467
|
|
|
467
|
-
|
|
468
|
+
## Notes
|
|
468
469
|
|
|
469
|
-
|
|
470
|
-
|
|
470
|
+
- Language: {{ site.lang }}
|
|
471
|
+
- All content may be used for AI indexing unless otherwise stated
|
|
472
|
+
{% endraw %}</code></pre>
|
|
471
473
|
|
|
472
474
|
<blockquote>The more accurate and detailed your <code>llms.txt</code>, the better AI models will understand and reference your site.</blockquote>
|
|
473
475
|
|
|
@@ -494,52 +496,72 @@ body {
|
|
|
494
496
|
|
|
495
497
|
<div id="content-backend" class="tab-content">
|
|
496
498
|
<div class="markdown-body">
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
│
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
│ ├──
|
|
507
|
-
│
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
499
|
+
<h2>Backend</h2>
|
|
500
|
+
<p>The backend is a PHP REST API located in <code>src/backend/</code>, copied to the output directory automatically at build time.</p>
|
|
501
|
+
|
|
502
|
+
<h3>Structure</h3>
|
|
503
|
+
<pre><code>src/backend/
|
|
504
|
+
├── api/
|
|
505
|
+
│ ├── public/ # Endpoints accessible without an API key
|
|
506
|
+
│ └── protected/ # Endpoints requiring X-Api-Key header
|
|
507
|
+
├── database/
|
|
508
|
+
│ ├── Database.php
|
|
509
|
+
│ ├── models/
|
|
510
|
+
│ └── migrations/
|
|
511
|
+
├── config.php # Your local config — never commit this
|
|
512
|
+
└── config.example.php</code></pre>
|
|
513
|
+
|
|
514
|
+
<h3>Configuration</h3>
|
|
515
|
+
<p><code>config.php</code> works like a <code>.env</code> file — it holds secrets and environment settings that stay local and out of version control.</p>
|
|
516
|
+
<p>Copy <code>config.example.php</code> to <code>config.php</code> and fill in your values:</p>
|
|
517
|
+
|
|
518
|
+
<h4>config.php <small>(<code>src/backend/</code>)</small></h4>
|
|
519
|
+
<pre><code class="language-php">return [
|
|
520
|
+
// Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
|
|
521
|
+
'API_KEY' => 'default-key',
|
|
522
|
+
|
|
523
|
+
// If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
|
|
524
|
+
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
525
|
+
'ENDPOINT_KEYS' => [
|
|
526
|
+
'example-protected' => 'custom-key',
|
|
527
|
+
],
|
|
528
|
+
|
|
529
|
+
'DB_HOST' => '127.0.0.1',
|
|
530
|
+
'DB_NAME' => 'example_db',
|
|
531
|
+
'DB_USER' => 'root',
|
|
532
|
+
'DB_PASS' => '',
|
|
526
533
|
];</code></pre>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
534
|
+
|
|
535
|
+
<p><code>API_KEY</code> is the fallback key for all protected endpoints. Use <code>ENDPOINT_KEYS</code> to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.</p>
|
|
536
|
+
|
|
537
|
+
<h3>How routing works</h3>
|
|
538
|
+
<p>The file path inside <code>api/</code> maps directly to the URL. Extra URL segments become route parameters available as <code>$requestParams[]</code>.</p>
|
|
539
|
+
<p>Every endpoint file has access to:</p>
|
|
540
|
+
|
|
541
|
+
<table>
|
|
542
|
+
<thead>
|
|
543
|
+
<tr>
|
|
544
|
+
<th>Variable</th>
|
|
545
|
+
<th>Description</th>
|
|
546
|
+
</tr>
|
|
547
|
+
</thead>
|
|
548
|
+
<tbody>
|
|
549
|
+
<tr>
|
|
550
|
+
<td><code>$method</code></td>
|
|
551
|
+
<td>HTTP method (<code>GET</code>, <code>POST</code>, <code>PUT</code>, <code>PATCH</code>, <code>DELETE</code>)</td>
|
|
552
|
+
</tr>
|
|
553
|
+
<tr>
|
|
554
|
+
<td><code>$requestParams</code></td>
|
|
555
|
+
<td>Extra URL segments (e.g. <code>/api/posts/42</code> → <code>['42']</code>)</td>
|
|
556
|
+
</tr>
|
|
557
|
+
</tbody>
|
|
558
|
+
</table>
|
|
559
|
+
|
|
560
|
+
<h3>Creating a public endpoint</h3>
|
|
561
|
+
<p>Create a <code>.php</code> file anywhere inside <code>api/public/</code></p>
|
|
562
|
+
|
|
563
|
+
<h4>api/public/example.php</h4>
|
|
564
|
+
<pre><code class="language-php"><?php
|
|
543
565
|
declare(strict_types=1);
|
|
544
566
|
|
|
545
567
|
require_once CORE_PATH . '/modules/Response.php';
|
|
@@ -550,11 +572,13 @@ if ($method !== 'GET') {
|
|
|
550
572
|
|
|
551
573
|
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
552
574
|
|
|
553
|
-
Response::success(['id'
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
575
|
+
Response::success(['id' => $id]);</code></pre>
|
|
576
|
+
|
|
577
|
+
<h3>Creating a protected endpoint</h3>
|
|
578
|
+
<p>Create a <code>.php</code> file inside <code>api/protected/</code>. The API key check happens automatically before your file runs.</p>
|
|
579
|
+
|
|
580
|
+
<h4>api/protected/example.php</h4>
|
|
581
|
+
<pre><code class="language-php"><?php
|
|
558
582
|
declare(strict_types=1);
|
|
559
583
|
|
|
560
584
|
require_once CORE_PATH . '/modules/Response.php';
|
|
@@ -563,101 +587,40 @@ if ($method !== 'GET') {
|
|
|
563
587
|
Response::error('Method not allowed', 405);
|
|
564
588
|
}
|
|
565
589
|
|
|
566
|
-
Response::success(['visits'
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
590
|
+
Response::success(['visits' => 1024]);</code></pre>
|
|
591
|
+
|
|
592
|
+
<p>To assign a dedicated key, add it to <code>config.php</code>:</p>
|
|
593
|
+
<pre><code class="language-php">'ENDPOINT_KEYS' => [
|
|
594
|
+
'endpoint' => 'custom-key',
|
|
570
595
|
],</code></pre>
|
|
571
|
-
|
|
572
|
-
|
|
596
|
+
|
|
597
|
+
<h3>The Response helper</h3>
|
|
598
|
+
<pre><code class="language-php">Response::success($data, $code); // default 200
|
|
573
599
|
Response::error($message, $code, $details); // default 400
|
|
574
600
|
Response::noContent(); // 204</code></pre>
|
|
575
|
-
<h3>Handling multiple methods</h3>
|
|
576
|
-
<pre><code>$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
577
|
-
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
578
|
-
|
|
579
|
-
switch ($method) {
|
|
580
|
-
case 'GET':
|
|
581
|
-
Response::success(['id' => $id]);
|
|
582
|
-
break;
|
|
583
|
-
|
|
584
|
-
case 'POST':
|
|
585
|
-
if (empty($input['title'])) Response::error('Missing title', 400);
|
|
586
|
-
Response::success(['message' => 'Created'], 201);
|
|
587
|
-
break;
|
|
588
|
-
|
|
589
|
-
case 'DELETE':
|
|
590
|
-
if (!$id) Response::error('ID required', 400);
|
|
591
|
-
Response::success(['message' => 'Deleted']);
|
|
592
|
-
break;
|
|
593
|
-
|
|
594
|
-
default:
|
|
595
|
-
Response::error('Method not allowed', 405);
|
|
596
|
-
}</code></pre>
|
|
597
|
-
<h3>Using the database</h3>
|
|
598
|
-
<h4>database/models/Post.php</h4>
|
|
599
|
-
<pre><code><?php
|
|
600
|
-
declare(strict_types=1);
|
|
601
|
-
|
|
602
|
-
require_once __DIR__ . '/../Database.php';
|
|
603
|
-
|
|
604
|
-
class Post {
|
|
605
|
-
private PDO $db;
|
|
606
601
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
$post = new Post();
|
|
631
|
-
Response::success($post->getAll());</code></pre>
|
|
632
|
-
<p>Migrations live in <code>database/migrations/</code> as plain SQL files — run them manually against your database.</p>
|
|
633
|
-
<h3>Calling endpoints from the frontend</h3>
|
|
634
|
-
<pre><code>// Public
|
|
635
|
-
const res = await fetch('/api/posts/42');
|
|
636
|
-
|
|
637
|
-
// Protected
|
|
638
|
-
const res = await fetch('/api/admin/stats', {
|
|
639
|
-
headers: { 'X-Api-Key': 'secret-stats-key' }
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
// POST
|
|
643
|
-
const res = await fetch('/api/posts', {
|
|
644
|
-
method: 'POST',
|
|
645
|
-
headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'your-key' },
|
|
646
|
-
body: JSON.stringify({ title: 'Hello world' })
|
|
647
|
-
});</code></pre>
|
|
648
|
-
<h3>Pre-built endpoints</h3>
|
|
649
|
-
<table>
|
|
650
|
-
<thead>
|
|
651
|
-
<tr><th>Route</th><th>Auth</th><th>Methods</th><th>Description</th></tr>
|
|
652
|
-
</thead>
|
|
653
|
-
<tbody>
|
|
654
|
-
<tr><td><code>/api/example-public</code></td><td>No</td><td><code>GET</code></td><td>Smoke test for public routing</td></tr>
|
|
655
|
-
<tr><td><code>/api/subfolder/example-protected</code></td><td>Yes</td><td><code>GET</code></td><td>Smoke test for protected routing</td></tr>
|
|
656
|
-
<tr><td><code>/api/auth/register</code></td><td>No</td><td><code>POST</code></td><td>Register a new user</td></tr>
|
|
657
|
-
<tr><td><code>/api/auth/login</code></td><td>No</td><td><code>POST</code></td><td>Login and retrieve user data</td></tr>
|
|
658
|
-
<tr><td><code>/api/auth-system</code></td><td>Yes</td><td><code>GET POST PUT PATCH DELETE</code></td><td>Full CRUD on users</td></tr>
|
|
659
|
-
</tbody>
|
|
660
|
-
</table>
|
|
602
|
+
<h3>Pre-built endpoints</h3>
|
|
603
|
+
<table>
|
|
604
|
+
<thead>
|
|
605
|
+
<tr>
|
|
606
|
+
<th>Route</th>
|
|
607
|
+
<th>Method</th>
|
|
608
|
+
<th>Description</th>
|
|
609
|
+
</tr>
|
|
610
|
+
</thead>
|
|
611
|
+
<tbody>
|
|
612
|
+
<tr>
|
|
613
|
+
<td><code>/api/example-public</code></td>
|
|
614
|
+
<td><code>GET</code></td>
|
|
615
|
+
<td>Example endpoint that doesn't require any key</td>
|
|
616
|
+
</tr>
|
|
617
|
+
<tr>
|
|
618
|
+
<td><code>/api/example-protected</code></td>
|
|
619
|
+
<td><code>GET</code></td>
|
|
620
|
+
<td>Example endpoint that requires X-API-KEY</td>
|
|
621
|
+
</tr>
|
|
622
|
+
</tbody>
|
|
623
|
+
</table>
|
|
661
624
|
</div>
|
|
662
625
|
</div>
|
|
663
626
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
//===========================
|
|
2
2
|
// JAVASCRIPT MODULES IMPORTS
|
|
3
|
-
|
|
3
|
+
//===========================
|
|
4
4
|
|
|
5
5
|
// Call anywhere
|
|
6
6
|
import { showNotification } from '../modules/notification.js';
|
|
7
7
|
|
|
8
|
-
// Uncomment
|
|
8
|
+
// Uncomment of pre-existing modules
|
|
9
9
|
// import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
|
|
10
10
|
// import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
11
11
|
|
|
@@ -14,6 +14,8 @@ import { showNotification } from '../modules/notification.js';
|
|
|
14
14
|
//==========================
|
|
15
15
|
|
|
16
16
|
document.addEventListener("DOMContentLoaded", () => {
|
|
17
|
+
// initTextAreaAutoExpand();
|
|
18
|
+
// initNormalizePhoneNumber();
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
showNotification("Example notification", "success", 3000);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
//===========================
|
|
2
2
|
// JAVASCRIPT MODULES IMPORTS
|
|
3
|
-
|
|
3
|
+
//===========================
|
|
4
4
|
|
|
5
5
|
// Call anywhere
|
|
6
6
|
import { showNotification } from '../modules/notification.js';
|
|
7
7
|
|
|
8
|
-
// Uncomment
|
|
8
|
+
// Uncomment of pre-existing modules
|
|
9
9
|
// import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
|
|
10
10
|
// import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
11
11
|
|
|
@@ -14,6 +14,8 @@ import { showNotification } from '../modules/notification.js';
|
|
|
14
14
|
//==========================
|
|
15
15
|
|
|
16
16
|
document.addEventListener("DOMContentLoaded", () => {
|
|
17
|
+
// initTextAreaAutoExpand();
|
|
18
|
+
// initNormalizePhoneNumber();
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
showNotification("Example notification", "success", 3000);
|
package/src/frontend/llms.njk
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
// Getting root rules from _root.scss
|
|
6
6
|
@use 'root' as root;
|
|
7
7
|
|
|
8
|
+
// Frameworks imports
|
|
8
9
|
@import "../modules/frameworks/bootstrap";
|
|
9
10
|
// @import "../modules/frameworks/bulma";
|
|
10
11
|
// @import "../modules/frameworks/foundation";
|
|
11
12
|
// @import "../modules/frameworks/uikit";
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
// Modules imports
|
|
14
16
|
@import "typography";
|
|
15
17
|
|
|
16
18
|
@import "header";
|
|
@@ -21,6 +23,10 @@
|
|
|
21
23
|
|
|
22
24
|
@import "buttons";
|
|
23
25
|
|
|
26
|
+
//==========================
|
|
27
|
+
// Global css rules for the entire site
|
|
28
|
+
//==========================
|
|
29
|
+
|
|
24
30
|
*,
|
|
25
31
|
*::before,
|
|
26
32
|
*::after {
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
declare(strict_types=1);
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// 2. Richiamo il tuo modulo Response e il Modello
|
|
6
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
7
|
-
require_once __DIR__ . '/../../database/models/User.php';
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
// Your protected endpoint logic here. You can access route parameters in $requestParams array
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
$user = new User();
|
|
14
|
-
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
15
|
-
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
switch ($method) {
|
|
19
|
-
case 'GET':
|
|
20
|
-
$data = $id ? $user->getById($id) : $user->getAll();
|
|
21
|
-
if ($id && !$data) {
|
|
22
|
-
Response::error('User not found', 404);
|
|
23
|
-
}
|
|
24
|
-
// Sostituito con Response::success()
|
|
25
|
-
Response::success($data);
|
|
26
|
-
break;
|
|
27
|
-
|
|
28
|
-
case 'POST':
|
|
29
|
-
if (empty($input['nickname']) || empty($input['email'])) {
|
|
30
|
-
Response::error('Missing fields', 400);
|
|
31
|
-
}
|
|
32
|
-
if (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
|
|
33
|
-
Response::error('Invalid email', 400);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
$newId = $user->create($input['nickname'], $input['email']);
|
|
37
|
-
http_response_code(201);
|
|
38
|
-
Response::success(['message' => 'Created', 'id' => $newId]);
|
|
39
|
-
break;
|
|
40
|
-
|
|
41
|
-
case 'PUT':
|
|
42
|
-
case 'PATCH':
|
|
43
|
-
if (!$id) Response::error('ID required', 400);
|
|
44
|
-
if (!$user->update($id, $input)) {
|
|
45
|
-
Response::error('Not found or no changes', 404);
|
|
46
|
-
}
|
|
47
|
-
Response::success(['message' => 'Updated']);
|
|
48
|
-
break;
|
|
49
|
-
|
|
50
|
-
case 'DELETE':
|
|
51
|
-
if (!$id) Response::error('ID required', 400);
|
|
52
|
-
if (!$user->delete($id)) {
|
|
53
|
-
Response::error('Not found', 404);
|
|
54
|
-
}
|
|
55
|
-
Response::success(['message' => 'Deleted']);
|
|
56
|
-
break;
|
|
57
|
-
|
|
58
|
-
default:
|
|
59
|
-
Response::error('Method not allowed', 405);
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
} catch (PDOException $e) {
|
|
63
|
-
if ($e->getCode() === '23000') {
|
|
64
|
-
Response::error('Nickname or email already exists', 409);
|
|
65
|
-
}
|
|
66
|
-
Response::error('Database error', 500);
|
|
67
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
declare(strict_types=1);
|
|
3
|
-
|
|
4
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
5
|
-
require_once __DIR__ . '/../../../database/models/User.php';
|
|
6
|
-
|
|
7
|
-
if ($method !== 'POST') {
|
|
8
|
-
Response::error('Method not allowed', 405);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
// Your protected endpoint logic here. You can access route parameters in $requestParams array
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
16
|
-
|
|
17
|
-
$email = trim(filter_var($input['email'] ?? '', FILTER_SANITIZE_EMAIL));
|
|
18
|
-
$password = trim($input['password'] ?? '');
|
|
19
|
-
|
|
20
|
-
if (empty($email) || empty($password)) {
|
|
21
|
-
Response::error('Missing fields', 400);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
25
|
-
Response::error('Invalid email', 400);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
$user = new User();
|
|
29
|
-
$found = $user->findByEmail($email);
|
|
30
|
-
|
|
31
|
-
if (!$found || !password_verify($password, $found['password'])) {
|
|
32
|
-
Response::error('Invalid credentials', 401);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
unset($found['password']);
|
|
36
|
-
Response::success([
|
|
37
|
-
'user' => $found,
|
|
38
|
-
]);
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
declare(strict_types=1);
|
|
3
|
-
|
|
4
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
5
|
-
require_once __DIR__ . '/../../../database/models/User.php';
|
|
6
|
-
|
|
7
|
-
if ($method !== 'POST') {
|
|
8
|
-
Response::error('Method not allowed', 405);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
// Your protected endpoint logic here. You can access route parameters in $requestParams array
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
17
|
-
|
|
18
|
-
$nickname = htmlspecialchars(strip_tags(trim($input['nickname'] ?? '')));
|
|
19
|
-
$email = trim(filter_var($input['email'] ?? '', FILTER_SANITIZE_EMAIL));
|
|
20
|
-
$password = trim($input['password'] ?? '');
|
|
21
|
-
|
|
22
|
-
if (empty($nickname) || empty($email) || empty($password)) {
|
|
23
|
-
Response::error('Missing fields', 400);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
27
|
-
Response::error('Invalid email', 400);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (strlen($password) < 8) {
|
|
31
|
-
Response::error('Password must be at least 8 characters', 400);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
$user = new User();
|
|
36
|
-
$newId = $user->create($nickname, $email, $password);
|
|
37
|
-
http_response_code(201);
|
|
38
|
-
Response::success(['id' => $newId]);
|
|
39
|
-
} catch (PDOException $e) {
|
|
40
|
-
if ($e->getCode() === '23000') {
|
|
41
|
-
Response::error('Nickname or email already exists', 409);
|
|
42
|
-
}
|
|
43
|
-
Response::error('Database error', 500);
|
|
44
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
USE example_db;
|
|
2
|
-
|
|
3
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
4
|
-
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
5
|
-
nickname VARCHAR(50) NOT NULL UNIQUE,
|
|
6
|
-
email VARCHAR(255) NOT NULL UNIQUE,
|
|
7
|
-
password VARCHAR(255) NOT NULL,
|
|
8
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
9
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
declare(strict_types=1);
|
|
3
|
-
|
|
4
|
-
require_once __DIR__ . '/../Database.php';
|
|
5
|
-
|
|
6
|
-
class User {
|
|
7
|
-
private PDO $db;
|
|
8
|
-
|
|
9
|
-
public function __construct() {
|
|
10
|
-
$this->db = Database::getInstance();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public function getAll(): array {
|
|
14
|
-
return $this->db->query("SELECT id, nickname, email, created_at FROM users")->fetchAll();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
public function getById(int $id): ?array {
|
|
18
|
-
$stmt = $this->db->prepare("SELECT id, nickname, email, created_at FROM users WHERE id = :id");
|
|
19
|
-
$stmt->execute(['id' => $id]);
|
|
20
|
-
return $stmt->fetch() ?: null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public function findByEmail(string $email): ?array {
|
|
24
|
-
$stmt = $this->db->prepare("SELECT id, nickname, email, password, created_at FROM users WHERE email = :email");
|
|
25
|
-
$stmt->execute(['email' => filter_var(trim($email), FILTER_SANITIZE_EMAIL)]);
|
|
26
|
-
return $stmt->fetch() ?: null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public function create(string $nickname, string $email, string $password = ''): int {
|
|
30
|
-
$stmt = $this->db->prepare("INSERT INTO users (nickname, email, password) VALUES (:nickname, :email, :password)");
|
|
31
|
-
$stmt->execute([
|
|
32
|
-
'nickname' => htmlspecialchars(strip_tags(trim($nickname))),
|
|
33
|
-
'email' => filter_var(trim($email), FILTER_SANITIZE_EMAIL),
|
|
34
|
-
'password' => $password !== '' ? password_hash($password, PASSWORD_BCRYPT) : '',
|
|
35
|
-
]);
|
|
36
|
-
return (int)$this->db->lastInsertId();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
public function update(int $id, array $data): bool {
|
|
40
|
-
$fields = [];
|
|
41
|
-
$params = ['id' => $id];
|
|
42
|
-
|
|
43
|
-
if (isset($data['nickname'])) {
|
|
44
|
-
$fields[] = 'nickname = :nickname';
|
|
45
|
-
$params['nickname'] = htmlspecialchars(strip_tags($data['nickname']));
|
|
46
|
-
}
|
|
47
|
-
if (isset($data['email'])) {
|
|
48
|
-
$fields[] = 'email = :email';
|
|
49
|
-
$params['email'] = filter_var($data['email'], FILTER_SANITIZE_EMAIL);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (empty($fields)) return false;
|
|
53
|
-
|
|
54
|
-
$sql = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = :id";
|
|
55
|
-
return $this->db->prepare($sql)->execute($params);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public function delete(int $id): bool {
|
|
59
|
-
return $this->db->prepare("DELETE FROM users WHERE id = :id")->execute(['id' => $id]);
|
|
60
|
-
}
|
|
61
|
-
}
|
/package/src/backend/api/protected/{subfolder/example-protected.php → example-protected.php}
RENAMED
|
File without changes
|