@materializecms/server 0.1.0 → 0.2.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/CHANGELOG.md +8 -0
- package/package.json +4 -4
- package/src/app.js +55 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Pros
|
|
|
4
4
|
|
|
5
5
|
## Prossima versione
|
|
6
6
|
|
|
7
|
+
## 0.2.0 — 2026-11-01
|
|
8
|
+
|
|
9
|
+
- **Password dimenticata?** nella pagina di accesso: un link per email, valido un'ora. Senza email configurata, il comando `materialize reset-password` stampa il link sul server, anche per l'unico amministratore.
|
|
10
|
+
- **Cronologia per le pagine sulla tela**: ogni versione si guarda su desktop, tablet e mobile prima di ripristinarla.
|
|
11
|
+
- **Esporta la pagina in PDF o PNG** dall'editor, per mandarla al cliente: desktop, tablet e mobile, ognuno come un'unica immagine lunga.
|
|
12
|
+
- **Guida per chi usa Materialize**, in `docs/guida`: installazione, editor, contenuti, lingue, SEO, persone, aggiornamenti, backup e problemi comuni.
|
|
13
|
+
- **Sicurezza**: i link degli inviti e delle nuove password puntano sempre all'indirizzo pubblico del sito, mai a quello dichiarato da chi fa la richiesta. Consigliato aggiornare.
|
|
14
|
+
|
|
7
15
|
## 0.1.0 — 2026-10-31
|
|
8
16
|
|
|
9
17
|
La prima versione pubblica.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@materializecms/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Il server di Materialize: API, accessi, media, backup, aggiornamenti dal pannello e server MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@anthropic-ai/sdk": "1.3.0",
|
|
22
22
|
"@hono/node-server": "2.1.5",
|
|
23
|
-
"@materializecms/admin": "0.
|
|
24
|
-
"@materializecms/core": "0.
|
|
25
|
-
"@materializecms/db": "0.
|
|
23
|
+
"@materializecms/admin": "0.2.0",
|
|
24
|
+
"@materializecms/core": "0.2.0",
|
|
25
|
+
"@materializecms/db": "0.2.0",
|
|
26
26
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
27
27
|
"hono": "4.13.14",
|
|
28
28
|
"htmlparser2": "12.0.0",
|
package/src/app.js
CHANGED
|
@@ -52,6 +52,11 @@ const CHALLENGE_TTL = 5 * 60 * 1000;
|
|
|
52
52
|
const dummyHash = hashPassword(randomBytes(16).toString('hex'));
|
|
53
53
|
const INVITE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
54
54
|
const RESET_TTL = 24 * 60 * 60 * 1000;
|
|
55
|
+
/** A link asked for from the sign-in page: shorter, since nobody vouched for the request. */
|
|
56
|
+
const FORGOT_TTL = 60 * 60 * 1000;
|
|
57
|
+
/** Links asked for per email, and per address, before a pause: no one is flooded with emails. */
|
|
58
|
+
const FORGOT_THRESHOLD = 3;
|
|
59
|
+
const FORGOT_IP_THRESHOLD = 10;
|
|
55
60
|
/** Where people open their one-time link: the admin's welcome screen. */
|
|
56
61
|
const linkPath = (token) => `/admin/_invite/${token}`;
|
|
57
62
|
/** The custom code blocks in a document: it runs on the public site, so only admins may change it. */
|
|
@@ -643,6 +648,40 @@ export function createApp(base) {
|
|
|
643
648
|
const { passwordHash: _, ...user } = found;
|
|
644
649
|
return signIn(c, user);
|
|
645
650
|
});
|
|
651
|
+
/** For the sign-in page: whether "Password dimenticata?" can send an email, or someone has to help. */
|
|
652
|
+
app.get('/api/auth/recovery', async (c) => c.json({ mail: isConfigured(await settings.get('mail')) }));
|
|
653
|
+
/**
|
|
654
|
+
* A forgotten password: a one-time link by email, valid an hour. The answer is the same whether
|
|
655
|
+
* the email has an account or not, so nobody can learn who is registered; requests per email and
|
|
656
|
+
* per address are limited, so nobody can flood an inbox.
|
|
657
|
+
*/
|
|
658
|
+
app.post('/api/auth/forgot', async (c) => {
|
|
659
|
+
const input = await body(c, z.object({ email: z.email() }));
|
|
660
|
+
const email = input.email.trim().toLowerCase();
|
|
661
|
+
const ip = clientIp(c);
|
|
662
|
+
await checkLocks(`forgot:${email}`, `forgot-ip:${ip}`);
|
|
663
|
+
await auth.recordFailure(`forgot:${email}`, FORGOT_THRESHOLD);
|
|
664
|
+
await auth.recordFailure(`forgot-ip:${ip}`, FORGOT_IP_THRESHOLD);
|
|
665
|
+
const mail = isConfigured(await settings.get('mail'));
|
|
666
|
+
const found = mail ? await auth.findForLogin(email) : null;
|
|
667
|
+
if (found) {
|
|
668
|
+
const { token } = await auth.createInvite({
|
|
669
|
+
kind: 'reset',
|
|
670
|
+
email: found.email,
|
|
671
|
+
name: found.name,
|
|
672
|
+
role: found.role,
|
|
673
|
+
userId: found.id,
|
|
674
|
+
ttlMs: FORGOT_TTL,
|
|
675
|
+
});
|
|
676
|
+
await mailLink(c, found.email, 'Nuova password', [
|
|
677
|
+
`Qualcuno ha chiesto una nuova password per il tuo account su ${ctx.config().site.name}.`,
|
|
678
|
+
'Se non sei stato tu, ignora questa email: la password resta quella di prima.',
|
|
679
|
+
'Per sceglierne una nuova, apri questo link entro un’ora:',
|
|
680
|
+
], linkPath(token));
|
|
681
|
+
await record(c, 'password.forgot', found.email, null, { id: found.id, name: found.name });
|
|
682
|
+
}
|
|
683
|
+
return c.json({ ok: true, mail });
|
|
684
|
+
});
|
|
646
685
|
/** The second step: a code from the app, or a recovery code. */
|
|
647
686
|
app.post('/api/auth/login/verify', async (c) => {
|
|
648
687
|
const input = await body(c, z.object({ ticket: z.string().min(1).max(100), code: z.string().trim().min(6).max(20) }));
|
|
@@ -770,7 +809,7 @@ export function createApp(base) {
|
|
|
770
809
|
app.get('/api/security', requireUser, requireAdmin, async (c) => c.json({ requireTwoFactor: await twoFactorRequired() }));
|
|
771
810
|
/** Groups of the activity log, by the start of their action codes. */
|
|
772
811
|
const ACTIVITY_GROUPS = {
|
|
773
|
-
access: ['login.', 'logout', 'session.', 'twofactor.', 'token.', 'security.'],
|
|
812
|
+
access: ['login.', 'logout', 'session.', 'twofactor.', 'token.', 'security.', 'password.'],
|
|
774
813
|
people: ['user.'],
|
|
775
814
|
content: ['document.', 'media.', 'message.'],
|
|
776
815
|
settings: ['settings.', 'backup.', 'update.'],
|
|
@@ -1448,12 +1487,26 @@ export function createApp(base) {
|
|
|
1448
1487
|
return false;
|
|
1449
1488
|
}
|
|
1450
1489
|
};
|
|
1490
|
+
/**
|
|
1491
|
+
* The address links in emails point to: the site's own (MATERIALIZE_SITE_URL, site.url) when it
|
|
1492
|
+
* is known. The request's Host header is used only without it, as in local development: anyone
|
|
1493
|
+
* can send a request with a false Host, and a link to their server would carry the token there.
|
|
1494
|
+
*/
|
|
1495
|
+
const publicOrigin = (c) => {
|
|
1496
|
+
const site = ctx.config().site.url;
|
|
1497
|
+
try {
|
|
1498
|
+
if (site)
|
|
1499
|
+
return new URL(site).origin;
|
|
1500
|
+
}
|
|
1501
|
+
catch { }
|
|
1502
|
+
return new URL(c.req.url).origin;
|
|
1503
|
+
};
|
|
1451
1504
|
const mailLink = async (c, to, subject, lines, path) => {
|
|
1452
1505
|
const mail = await settings.get('mail');
|
|
1453
1506
|
if (!isConfigured(mail))
|
|
1454
1507
|
return false;
|
|
1455
1508
|
try {
|
|
1456
|
-
const url = new URL(path,
|
|
1509
|
+
const url = new URL(path, publicOrigin(c)).toString();
|
|
1457
1510
|
await sendMail(mail, { to, subject, text: `${lines.join('\n')}\n\n${url}\n` });
|
|
1458
1511
|
return true;
|
|
1459
1512
|
}
|