@materializecms/db 0.0.0-stage → 0.1.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 +24 -0
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/package.json +28 -4
- package/src/activity.d.ts +38 -0
- package/src/activity.js +52 -0
- package/src/auth.d.ts +154 -0
- package/src/auth.js +337 -0
- package/src/comments.d.ts +26 -0
- package/src/comments.js +25 -0
- package/src/content.d.ts +115 -0
- package/src/content.js +314 -0
- package/src/database.d.ts +19 -0
- package/src/database.js +35 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -0
- package/src/media.d.ts +36 -0
- package/src/media.js +47 -0
- package/src/settings.d.ts +7 -0
- package/src/settings.js +21 -0
- package/src/submissions.d.ts +27 -0
- package/src/submissions.js +28 -0
- package/src/tables.d.ts +1855 -0
- package/src/tables.js +316 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Novità di Materialize
|
|
2
|
+
|
|
3
|
+
Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Prossima versione»: `npm run bump` le assegna alla nuova versione.
|
|
4
|
+
|
|
5
|
+
## Prossima versione
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — 2026-10-31
|
|
8
|
+
|
|
9
|
+
La prima versione pubblica.
|
|
10
|
+
|
|
11
|
+
- **Editor visuale**: pagine disegnate su una tela, con sezioni, colonne, testi, immagini, video, icone e forme; componenti riutilizzabili con varianti; righelli, guide, allineamenti e misure; modalità Contenuti per chi modifica solo i testi.
|
|
12
|
+
- **Animazioni ed effetti**: animazioni allo scorrimento e con linee temporali, stati al passaggio del mouse, sfondi WebGL e oggetti 3D.
|
|
13
|
+
- **Contenuti**: raccolte come Progetti, Diario e Team, con pagine modello; azioni rapide e selezione multipla negli elenchi e nei Media.
|
|
14
|
+
- **Siti in più lingue**: traduzioni collegate tra loro, anche con l’AI, e un selettore di lingua per il sito.
|
|
15
|
+
- **SEO**: titoli e descrizioni, sitemap, controlli sulla pagina e reindirizzamenti automatici quando cambia un indirizzo.
|
|
16
|
+
- **Moduli di contatto** con invio delle email dal tuo server di posta.
|
|
17
|
+
- **Assistente AI e server MCP**: testi, traduzioni e modifiche alle pagine chiedendole a parole.
|
|
18
|
+
- **Importazione da WordPress**: pagine, articoli e immagini.
|
|
19
|
+
- **Statistiche e cookie**: Google Analytics, Tag Manager, Meta Pixel, Plausible, Matomo e codice personalizzato, con il banner per il consenso.
|
|
20
|
+
- **Installazione su VPS con un comando**: Node.js, HTTPS con Caddy, servizio che riparte da solo, firewall e link per finire nel browser.
|
|
21
|
+
- **Immagine Docker** con `compose.yaml` pronto per l'HTTPS: il sito sta in un volume, quindi gli aggiornamenti dal pannello restano anche ricreando il container.
|
|
22
|
+
- **Installazione nel browser**: controllo del server, nome e lingua del sito, account amministratore, passo per passo.
|
|
23
|
+
- **Aggiornamenti con un clic** da Impostazioni → Aggiornamenti, con backup prima e ritorno automatico alla versione precedente se la nuova non parte; le correzioni possono installarsi da sole.
|
|
24
|
+
- **Sicurezza**: verifica in due passaggi, pausa dopo i tentativi di accesso falliti, registro delle attività, backup giornalieri del database e dei media.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cristiano Casazza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @materializecms/db
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Il database di Materialize: contenuti con bozze, pubblicazione e revisioni, utenti e sessioni (SQLite o libSQL).
|
|
4
|
+
|
|
5
|
+
Lo usa il server di Materialize: di solito non serve importarlo direttamente. Funziona con un file SQLite locale o con un database libSQL ospitato.
|
|
6
|
+
|
|
7
|
+
## Materialize
|
|
8
|
+
|
|
9
|
+
CMS open source con editor visuale: le pagine si disegnano sulla tela, il sito è fatto di file statici veloci. Per un nuovo sito (serve Node 24):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm create materialize@latest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Documentazione e codice: https://github.com/materializecms/materialize · Licenza MIT.
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@materializecms/db",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Il database di Materialize: contenuti con bozze, pubblicazione e revisioni, utenti e sessioni (SQLite o libSQL).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/materializecms/materialize.git",
|
|
10
|
+
"directory": "packages/db"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/materializecms/materialize#readme",
|
|
13
|
+
"bugs": "https://github.com/materializecms/materialize/issues",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.d.ts",
|
|
17
|
+
"default": "./src/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@libsql/client": "0.18.0",
|
|
22
|
+
"drizzle-orm": "0.45.4"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Db } from './database.ts';
|
|
2
|
+
export interface ActivityEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
at: Date;
|
|
5
|
+
userId: string | null;
|
|
6
|
+
userName: string;
|
|
7
|
+
/** A dotted code: "login.failed", "document.published", "user.removed"… */
|
|
8
|
+
action: string;
|
|
9
|
+
target: string;
|
|
10
|
+
detail: Record<string, unknown> | null;
|
|
11
|
+
ip: string | null;
|
|
12
|
+
/** Position in the log, to continue a list from (entries of the same millisecond included). */
|
|
13
|
+
seq: number;
|
|
14
|
+
}
|
|
15
|
+
/** The activity log: written by the server as things happen, read by administrators. */
|
|
16
|
+
export declare function createActivityStore(db: Db): {
|
|
17
|
+
record(entry: Omit<ActivityEntry, 'id' | 'at' | 'seq'>): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Newest first. `prefixes` keeps the actions of some groups ("login.", "user."…); `before`
|
|
20
|
+
* continues a list from the `seq` of its last entry.
|
|
21
|
+
*/
|
|
22
|
+
list(options?: {
|
|
23
|
+
prefixes?: string[];
|
|
24
|
+
before?: number;
|
|
25
|
+
limit?: number;
|
|
26
|
+
}): Promise<ActivityEntry[]>;
|
|
27
|
+
/** How many times an action happened since a moment: failed sign-ins in the last day… */
|
|
28
|
+
countSince(action: string, since: Date): Promise<number>;
|
|
29
|
+
/** Emails and addresses paused right now after failed sign-ins. */
|
|
30
|
+
lockedKeys(): Promise<string[]>;
|
|
31
|
+
/** Access tokens, with whose they are and when last used: for the security overview. */
|
|
32
|
+
tokens(): Promise<{
|
|
33
|
+
userId: string;
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
lastUsedAt: Date | null;
|
|
36
|
+
}[]>;
|
|
37
|
+
};
|
|
38
|
+
export type ActivityStore = ReturnType<typeof createActivityStore>;
|
package/src/activity.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { and, count, desc, eq, getTableColumns, gt, like, lt, or, sql } from 'drizzle-orm';
|
|
3
|
+
import { activity, apiTokens, loginAttempts } from './tables.js';
|
|
4
|
+
/** Entries older than this are deleted as new ones come in. */
|
|
5
|
+
const KEEP_MS = 365 * 24 * 60 * 60_000;
|
|
6
|
+
/** The activity log: written by the server as things happen, read by administrators. */
|
|
7
|
+
export function createActivityStore(db) {
|
|
8
|
+
let writes = 0;
|
|
9
|
+
return {
|
|
10
|
+
async record(entry) {
|
|
11
|
+
await db.insert(activity).values({ id: randomUUID(), at: new Date(), ...entry });
|
|
12
|
+
// Now and then, the oldest entries go.
|
|
13
|
+
if (++writes % 500 === 1)
|
|
14
|
+
await db.delete(activity).where(lt(activity.at, new Date(Date.now() - KEEP_MS)));
|
|
15
|
+
},
|
|
16
|
+
/**
|
|
17
|
+
* Newest first. `prefixes` keeps the actions of some groups ("login.", "user."…); `before`
|
|
18
|
+
* continues a list from the `seq` of its last entry.
|
|
19
|
+
*/
|
|
20
|
+
async list(options = {}) {
|
|
21
|
+
const rows = await db
|
|
22
|
+
.select({ ...getTableColumns(activity), seq: sql `rowid` })
|
|
23
|
+
.from(activity)
|
|
24
|
+
.where(and(options.before ? sql `rowid < ${options.before}` : undefined, options.prefixes?.length ? or(...options.prefixes.map((p) => like(activity.action, `${p}%`))) : undefined))
|
|
25
|
+
.orderBy(desc(sql `rowid`))
|
|
26
|
+
.limit(Math.min(options.limit ?? 50, 200));
|
|
27
|
+
return rows.map((r) => ({ ...r, detail: r.detail ?? null, ip: r.ip ?? null, userId: r.userId ?? null }));
|
|
28
|
+
},
|
|
29
|
+
/** How many times an action happened since a moment: failed sign-ins in the last day… */
|
|
30
|
+
async countSince(action, since) {
|
|
31
|
+
const [row] = await db
|
|
32
|
+
.select({ n: count() })
|
|
33
|
+
.from(activity)
|
|
34
|
+
.where(and(eq(activity.action, action), gt(activity.at, since)));
|
|
35
|
+
return row?.n ?? 0;
|
|
36
|
+
},
|
|
37
|
+
/** Emails and addresses paused right now after failed sign-ins. */
|
|
38
|
+
async lockedKeys() {
|
|
39
|
+
const rows = await db
|
|
40
|
+
.select({ key: loginAttempts.key })
|
|
41
|
+
.from(loginAttempts)
|
|
42
|
+
.where(gt(loginAttempts.lockedUntil, new Date()));
|
|
43
|
+
return rows.map((r) => r.key);
|
|
44
|
+
},
|
|
45
|
+
/** Access tokens, with whose they are and when last used: for the security overview. */
|
|
46
|
+
async tokens() {
|
|
47
|
+
return db
|
|
48
|
+
.select({ userId: apiTokens.userId, createdAt: apiTokens.createdAt, lastUsedAt: apiTokens.lastUsedAt })
|
|
49
|
+
.from(apiTokens);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/src/auth.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { Db } from './database.ts';
|
|
2
|
+
export type Role = 'admin' | 'editor';
|
|
3
|
+
export interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
role: Role;
|
|
8
|
+
/** Signs in with a code from an authenticator app too. */
|
|
9
|
+
twoFactor: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** A signed-in device, as listed in Profilo → Sicurezza. */
|
|
12
|
+
export interface DeviceSession {
|
|
13
|
+
/** The hash of the session token: safe to show, useless to sign in. */
|
|
14
|
+
id: string;
|
|
15
|
+
userAgent: string | null;
|
|
16
|
+
ip: string | null;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
lastSeenAt: Date;
|
|
19
|
+
}
|
|
20
|
+
export interface Member extends User {
|
|
21
|
+
createdAt: Date;
|
|
22
|
+
/** Most recent sign-in, if any. */
|
|
23
|
+
lastSeenAt: Date | null;
|
|
24
|
+
}
|
|
25
|
+
export interface Invite {
|
|
26
|
+
id: string;
|
|
27
|
+
kind: 'invite' | 'reset';
|
|
28
|
+
email: string;
|
|
29
|
+
name: string;
|
|
30
|
+
role: Role;
|
|
31
|
+
userId: string | null;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
expiresAt: Date;
|
|
34
|
+
}
|
|
35
|
+
/** A personal access token as listed: the secret itself is shown only once, when created. */
|
|
36
|
+
export interface ApiToken {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
/** The last characters of the secret, to tell tokens apart. */
|
|
40
|
+
hint: string;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
lastUsedAt: Date | null;
|
|
43
|
+
}
|
|
44
|
+
/** Prefix of every personal access token, so they are easy to recognise (and to scan for if leaked). */
|
|
45
|
+
export declare const TOKEN_PREFIX = "mzp_";
|
|
46
|
+
/** Raised when an email already belongs to someone. */
|
|
47
|
+
export declare class EmailTakenError extends Error {
|
|
48
|
+
constructor(email: string);
|
|
49
|
+
}
|
|
50
|
+
export declare function createAuthStore(db: Db): {
|
|
51
|
+
countUsers(): Promise<number>;
|
|
52
|
+
createUser(input: {
|
|
53
|
+
email: string;
|
|
54
|
+
name: string;
|
|
55
|
+
role: Role;
|
|
56
|
+
passwordHash: string;
|
|
57
|
+
}): Promise<User>;
|
|
58
|
+
findForLogin(email: string): Promise<(User & {
|
|
59
|
+
passwordHash: string;
|
|
60
|
+
}) | null>;
|
|
61
|
+
/** Returns the raw token for the cookie; only its hash is stored. */
|
|
62
|
+
createSession(userId: string, ttlMs: number, device?: {
|
|
63
|
+
userAgent?: string | null;
|
|
64
|
+
ip?: string | null;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
token: string;
|
|
67
|
+
expiresAt: Date;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Who a session belongs to. It ends when it expires or when it has not been used for
|
|
71
|
+
* `idleMs`; use is noted at most every few minutes.
|
|
72
|
+
*/
|
|
73
|
+
userForSession(token: string, idleMs?: number): Promise<User | null>;
|
|
74
|
+
/** The id under which a session is listed. */
|
|
75
|
+
sessionId: (token: string) => string;
|
|
76
|
+
listSessions(userId: string): Promise<DeviceSession[]>;
|
|
77
|
+
/** Signs one device out; only its owner can. */
|
|
78
|
+
endSession(userId: string, id: string): Promise<boolean>;
|
|
79
|
+
/** Signs every device out, except the session given. */
|
|
80
|
+
endOtherSessions(userId: string, keepToken?: string): Promise<number>;
|
|
81
|
+
twoFactorOf(userId: string): Promise<{
|
|
82
|
+
secret: string | null;
|
|
83
|
+
pending: string | null;
|
|
84
|
+
lastStep: number | null;
|
|
85
|
+
recoveryLeft: number;
|
|
86
|
+
} | null>;
|
|
87
|
+
/** A new secret during setup, not in use until a first code confirms it. */
|
|
88
|
+
setPendingSecret(userId: string, secret: string | null): Promise<void>;
|
|
89
|
+
/** The pending secret becomes the one in use, with fresh recovery codes (stored as hashes). */
|
|
90
|
+
enableTwoFactor(userId: string, step: number, recoveryCodes: string[]): Promise<void>;
|
|
91
|
+
disableTwoFactor(userId: string): Promise<void>;
|
|
92
|
+
/** Notes the time step of an accepted code, so the same code cannot be used again. */
|
|
93
|
+
useStep(userId: string, step: number): Promise<void>;
|
|
94
|
+
setRecoveryCodes(userId: string, codes: string[]): Promise<void>;
|
|
95
|
+
/** A recovery code works once: true when it was valid (and is now spent). */
|
|
96
|
+
useRecoveryCode(userId: string, code: string): Promise<boolean>;
|
|
97
|
+
/** Until when a key (an email, an address) is locked, if it is. */
|
|
98
|
+
lockedUntil(key: string): Promise<Date | null>;
|
|
99
|
+
/**
|
|
100
|
+
* Counts a failure. From `threshold` failures on, the key is locked for a time that doubles
|
|
101
|
+
* with each further failure (1, 2, 4… minutes, at most an hour). Failures are forgotten
|
|
102
|
+
* after a day without any.
|
|
103
|
+
*/
|
|
104
|
+
recordFailure(key: string, threshold: number): Promise<{
|
|
105
|
+
failures: number;
|
|
106
|
+
lockedUntil: Date | null;
|
|
107
|
+
}>;
|
|
108
|
+
clearFailures(key: string): Promise<void>;
|
|
109
|
+
/** A new personal access token for someone: returns the secret, which is never stored. */
|
|
110
|
+
createToken(userId: string, name: string): Promise<{
|
|
111
|
+
token: string;
|
|
112
|
+
record: ApiToken;
|
|
113
|
+
}>;
|
|
114
|
+
listTokens(userId: string): Promise<ApiToken[]>;
|
|
115
|
+
/** Only the owner can revoke a token. */
|
|
116
|
+
revokeToken(id: string, userId: string): Promise<boolean>;
|
|
117
|
+
/** Who a token belongs to; notes when it was last used (at most once a minute). */
|
|
118
|
+
userForToken(token: string): Promise<User | null>;
|
|
119
|
+
deleteSession(token: string): Promise<void>;
|
|
120
|
+
purgeExpiredSessions(): Promise<void>;
|
|
121
|
+
listUsers(): Promise<Member[]>;
|
|
122
|
+
getUser(id: string): Promise<User | null>;
|
|
123
|
+
getPasswordHash(id: string): Promise<string | null>;
|
|
124
|
+
countAdmins(): Promise<number>;
|
|
125
|
+
/** Changes name, role or email; an email someone else already uses raises EmailTakenError. */
|
|
126
|
+
updateUser(id: string, input: {
|
|
127
|
+
name?: string;
|
|
128
|
+
role?: Role;
|
|
129
|
+
email?: string;
|
|
130
|
+
}): Promise<User | null>;
|
|
131
|
+
deleteUser(id: string): Promise<boolean>;
|
|
132
|
+
/** Sets a new password and signs the user out everywhere, except the session given. */
|
|
133
|
+
setPassword(id: string, passwordHash: string, keepSession?: string): Promise<void>;
|
|
134
|
+
emailTaken(email: string): Promise<boolean>;
|
|
135
|
+
/** Returns the raw token for the link; only its hash is stored. Replaces earlier links of the same kind. */
|
|
136
|
+
createInvite(input: {
|
|
137
|
+
kind: Invite['kind'];
|
|
138
|
+
email: string;
|
|
139
|
+
name?: string;
|
|
140
|
+
role: Role;
|
|
141
|
+
userId?: string;
|
|
142
|
+
createdBy?: string;
|
|
143
|
+
ttlMs: number;
|
|
144
|
+
}): Promise<{
|
|
145
|
+
token: string;
|
|
146
|
+
invite: Invite;
|
|
147
|
+
}>;
|
|
148
|
+
pendingInvites(): Promise<Invite[]>;
|
|
149
|
+
findInvite(token: string): Promise<Invite | null>;
|
|
150
|
+
revokeInvite(id: string): Promise<boolean>;
|
|
151
|
+
/** A link works once. */
|
|
152
|
+
consumeInvite(token: string): Promise<void>;
|
|
153
|
+
};
|
|
154
|
+
export type AuthStore = ReturnType<typeof createAuthStore>;
|