@beechcms/api 0.4.0-preview.11 → 0.4.0-preview.13
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/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/index-CewtCjom.css +1 -0
- package/assets/dashboard/assets/index-FQ6JhvRH.js +554 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +213 -213
- package/package.json +2 -2
- package/src/auth/constants.ts +10 -10
- package/src/auth/login.ts +91 -91
- package/src/auth/refresh.ts +127 -127
- package/src/factory.ts +347 -325
- package/src/features/content/constants.ts +10 -10
- package/src/features/content/handlers/create.ts +153 -163
- package/src/features/content/handlers/delete.ts +76 -85
- package/src/features/content/handlers/facets.ts +45 -45
- package/src/features/content/handlers/get.ts +116 -116
- package/src/features/content/handlers/list.ts +88 -88
- package/src/features/content/handlers/update.ts +207 -216
- package/src/features/content/index.ts +20 -20
- package/src/features/draft/draft.handler.ts +272 -272
- package/src/features/draft/index.ts +1 -1
- package/src/features/email/email.provider.ts +38 -38
- package/src/features/email/email.service.ts +80 -80
- package/src/features/email/email.types.ts +98 -98
- package/src/features/email/index.ts +28 -28
- package/src/features/email/providers/resend.ts +63 -63
- package/src/features/email/templates/password-changed.ts +59 -59
- package/src/features/email/templates/password-reset.ts +64 -64
- package/src/features/email/templates/shell.ts +92 -93
- package/src/features/notifications/index.ts +1 -1
- package/src/features/notifications/notifications.handler.ts +130 -88
- package/src/features/password-reset/index.ts +15 -15
- package/src/features/password-reset/request.ts +106 -88
- package/src/features/password-reset/reset.ts +128 -110
- package/src/features/rotate-field/index.ts +1 -1
- package/src/features/rotate-field/rotate-field.handler.ts +132 -82
- package/src/features/rotate-field/rotate-field.schema.ts +13 -9
- package/src/features/schema/schema.handler.ts +16 -16
- package/src/features/settings/settings.handler.ts +414 -267
- package/src/features/setup/index.ts +96 -59
- package/src/features/stats/index.ts +1 -1
- package/src/features/stats/stats.handler.ts +458 -395
- package/src/index.ts +24 -24
- package/src/media-utils.ts +78 -78
- package/src/middleware/repository.middleware.ts +24 -18
- package/src/middleware/storage.middleware.ts +21 -0
- package/src/middleware.ts +67 -67
- package/src/public/access-policy.ts +23 -23
- package/src/public/api-key-middleware.ts +53 -53
- package/src/public/index.ts +12 -12
- package/src/public/problem-details.ts +48 -42
- package/src/public/public-add.ts +166 -166
- package/src/public/public-edit.ts +158 -158
- package/src/public/public-errors.ts +15 -15
- package/src/public/public-read.ts +216 -216
- package/src/public/public-routes.ts +84 -31
- package/src/public/query-builder.ts +152 -152
- package/src/public/rate-limit-middleware.ts +42 -42
- package/src/public/response-builder.ts +26 -26
- package/src/public/sanitize.ts +65 -65
- package/src/public/slug-utils.ts +14 -14
- package/src/search-utils.ts +192 -192
- package/src/search.ts +72 -72
- package/src/shared/activity-logger.ts +79 -79
- package/src/shared/apply-policies.ts +63 -63
- package/src/shared/base.repository.d1.ts +28 -28
- package/src/shared/content-utils.ts +82 -108
- package/src/shared/content.repository.d1.ts +382 -382
- package/src/shared/fts-sync.ts +4 -4
- package/src/shared/idempotency.repository.d1.ts +56 -45
- package/src/shared/media.repository.d1.ts +64 -0
- package/src/shared/notification-service.ts +56 -56
- package/src/shared/query-utils.ts +137 -137
- package/src/shared/storage/factory.ts +40 -0
- package/src/shared/storage/r2-binding-bucket.ts +81 -0
- package/src/shared/storage/s3-bucket.ts +163 -0
- package/src/shared/storage-utils.ts +36 -36
- package/src/shared/system-stats.repository.d1.ts +44 -0
- package/src/types.ts +46 -41
- package/src/upload.ts +179 -331
- package/src/widget.ts +349 -349
- package/assets/dashboard/assets/index-9Ch2xWJr.js +0 -554
- package/assets/dashboard/assets/index-ye3325L9.css +0 -1
package/src/auth/refresh.ts
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import { SignJWT } from 'jose'
|
|
3
|
-
|
|
4
|
-
/** Secondi in un giorno (per calcolo scadenza) */
|
|
5
|
-
const SECONDS_PER_DAY = 24 * 60 * 60
|
|
6
|
-
|
|
7
|
-
export type JwtTokenOptions = {
|
|
8
|
-
issuer?: string
|
|
9
|
-
audience?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type RefreshTokenRecord = {
|
|
13
|
-
id: string
|
|
14
|
-
user_id: string
|
|
15
|
-
token_hash: string
|
|
16
|
-
expires_at: number
|
|
17
|
-
created_at: number
|
|
18
|
-
revoked_at: number | null
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Genera un refresh token sicuro (UUID v4) - usa Web Crypto API globale */
|
|
22
|
-
export function generateRefreshToken(): string {
|
|
23
|
-
return crypto.randomUUID()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Hash SHA-256 del refresh token per storage sicuro.
|
|
28
|
-
* Usa Web Crypto API (crypto.subtle.digest).
|
|
29
|
-
*/
|
|
30
|
-
export async function hashRefreshToken(token: string): Promise<string> {
|
|
31
|
-
const encoder = new TextEncoder()
|
|
32
|
-
const data = encoder.encode(token)
|
|
33
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
34
|
-
const hashBytes = Array.from(new Uint8Array(hashBuffer))
|
|
35
|
-
return hashBytes.map((byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Salva refresh token in DB (solo hash, mai in chiaro).
|
|
40
|
-
* @param expiresInDays - Giorni di validità (default 7)
|
|
41
|
-
*/
|
|
42
|
-
export async function saveRefreshToken(
|
|
43
|
-
db: D1Database,
|
|
44
|
-
userId: string,
|
|
45
|
-
token: string,
|
|
46
|
-
expiresInDays: number = 7
|
|
47
|
-
): Promise<void> {
|
|
48
|
-
const id = crypto.randomUUID()
|
|
49
|
-
const tokenHash = await hashRefreshToken(token)
|
|
50
|
-
const expiresAt = Math.floor(Date.now() / 1000) + expiresInDays * SECONDS_PER_DAY
|
|
51
|
-
|
|
52
|
-
await db.prepare(
|
|
53
|
-
`INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
|
|
54
|
-
VALUES (?, ?, ?, ?)`
|
|
55
|
-
).bind(id, userId, tokenHash, expiresAt).run()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Valida refresh token: verifica hash in DB, scadenza e che non sia revocato.
|
|
60
|
-
* @returns { valid, userId? } - userId presente solo se valido
|
|
61
|
-
*/
|
|
62
|
-
export async function validateRefreshToken(
|
|
63
|
-
db: D1Database,
|
|
64
|
-
token: string
|
|
65
|
-
): Promise<{ valid: boolean; userId?: string }> {
|
|
66
|
-
const tokenHash = await hashRefreshToken(token)
|
|
67
|
-
const now = Math.floor(Date.now() / 1000)
|
|
68
|
-
|
|
69
|
-
const row = await db.prepare(
|
|
70
|
-
`SELECT user_id, expires_at, revoked_at
|
|
71
|
-
FROM refresh_tokens
|
|
72
|
-
WHERE token_hash = ? LIMIT 1`
|
|
73
|
-
).bind(tokenHash).first<RefreshTokenRecord>()
|
|
74
|
-
|
|
75
|
-
if (!row) return { valid: false }
|
|
76
|
-
if (row.revoked_at !== null) return { valid: false }
|
|
77
|
-
if (row.expires_at < now) return { valid: false }
|
|
78
|
-
|
|
79
|
-
return { valid: true, userId: row.user_id }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Revoca un refresh token impostando revoked_at.
|
|
84
|
-
* Il token non potrà più essere usato per il refresh.
|
|
85
|
-
*/
|
|
86
|
-
export async function revokeRefreshToken(
|
|
87
|
-
db: D1Database,
|
|
88
|
-
token: string
|
|
89
|
-
): Promise<boolean> {
|
|
90
|
-
const tokenHash = await hashRefreshToken(token)
|
|
91
|
-
const now = Math.floor(Date.now() / 1000)
|
|
92
|
-
|
|
93
|
-
const result = await db.prepare(
|
|
94
|
-
`UPDATE refresh_tokens
|
|
95
|
-
SET revoked_at = ?
|
|
96
|
-
WHERE token_hash = ?
|
|
97
|
-
AND revoked_at IS NULL
|
|
98
|
-
AND expires_at >= ?`
|
|
99
|
-
).bind(now, tokenHash, now).run()
|
|
100
|
-
|
|
101
|
-
const changes = (result as unknown as { meta?: { changes?: number } })?.meta?.changes ?? 0
|
|
102
|
-
return changes > 0
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Genera JWT access token con scadenza breve (15 min).
|
|
107
|
-
* Payload: sub (userId), email, name (opzionale). Algoritmo HS256.
|
|
108
|
-
*/
|
|
109
|
-
export async function generateAccessToken(
|
|
110
|
-
userId: string,
|
|
111
|
-
email: string,
|
|
112
|
-
secret: string,
|
|
113
|
-
options: JwtTokenOptions = {},
|
|
114
|
-
name?: string
|
|
115
|
-
): Promise<string> {
|
|
116
|
-
const payload: Record<string, string> = { email }
|
|
117
|
-
if (name) payload.name = name
|
|
118
|
-
const secretBytes = new TextEncoder().encode(secret)
|
|
119
|
-
let jwt = new SignJWT(payload)
|
|
120
|
-
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
|
|
121
|
-
.setSubject(userId)
|
|
122
|
-
.setIssuedAt()
|
|
123
|
-
.setExpirationTime('15m')
|
|
124
|
-
if (options.issuer) jwt = jwt.setIssuer(options.issuer)
|
|
125
|
-
if (options.audience) jwt = jwt.setAudience(options.audience)
|
|
126
|
-
return jwt.sign(secretBytes)
|
|
127
|
-
}
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import { SignJWT } from 'jose'
|
|
3
|
+
|
|
4
|
+
/** Secondi in un giorno (per calcolo scadenza) */
|
|
5
|
+
const SECONDS_PER_DAY = 24 * 60 * 60
|
|
6
|
+
|
|
7
|
+
export type JwtTokenOptions = {
|
|
8
|
+
issuer?: string
|
|
9
|
+
audience?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RefreshTokenRecord = {
|
|
13
|
+
id: string
|
|
14
|
+
user_id: string
|
|
15
|
+
token_hash: string
|
|
16
|
+
expires_at: number
|
|
17
|
+
created_at: number
|
|
18
|
+
revoked_at: number | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Genera un refresh token sicuro (UUID v4) - usa Web Crypto API globale */
|
|
22
|
+
export function generateRefreshToken(): string {
|
|
23
|
+
return crypto.randomUUID()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hash SHA-256 del refresh token per storage sicuro.
|
|
28
|
+
* Usa Web Crypto API (crypto.subtle.digest).
|
|
29
|
+
*/
|
|
30
|
+
export async function hashRefreshToken(token: string): Promise<string> {
|
|
31
|
+
const encoder = new TextEncoder()
|
|
32
|
+
const data = encoder.encode(token)
|
|
33
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
34
|
+
const hashBytes = Array.from(new Uint8Array(hashBuffer))
|
|
35
|
+
return hashBytes.map((byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Salva refresh token in DB (solo hash, mai in chiaro).
|
|
40
|
+
* @param expiresInDays - Giorni di validità (default 7)
|
|
41
|
+
*/
|
|
42
|
+
export async function saveRefreshToken(
|
|
43
|
+
db: D1Database,
|
|
44
|
+
userId: string,
|
|
45
|
+
token: string,
|
|
46
|
+
expiresInDays: number = 7
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
const id = crypto.randomUUID()
|
|
49
|
+
const tokenHash = await hashRefreshToken(token)
|
|
50
|
+
const expiresAt = Math.floor(Date.now() / 1000) + expiresInDays * SECONDS_PER_DAY
|
|
51
|
+
|
|
52
|
+
await db.prepare(
|
|
53
|
+
`INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
|
|
54
|
+
VALUES (?, ?, ?, ?)`
|
|
55
|
+
).bind(id, userId, tokenHash, expiresAt).run()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Valida refresh token: verifica hash in DB, scadenza e che non sia revocato.
|
|
60
|
+
* @returns { valid, userId? } - userId presente solo se valido
|
|
61
|
+
*/
|
|
62
|
+
export async function validateRefreshToken(
|
|
63
|
+
db: D1Database,
|
|
64
|
+
token: string
|
|
65
|
+
): Promise<{ valid: boolean; userId?: string }> {
|
|
66
|
+
const tokenHash = await hashRefreshToken(token)
|
|
67
|
+
const now = Math.floor(Date.now() / 1000)
|
|
68
|
+
|
|
69
|
+
const row = await db.prepare(
|
|
70
|
+
`SELECT user_id, expires_at, revoked_at
|
|
71
|
+
FROM refresh_tokens
|
|
72
|
+
WHERE token_hash = ? LIMIT 1`
|
|
73
|
+
).bind(tokenHash).first<RefreshTokenRecord>()
|
|
74
|
+
|
|
75
|
+
if (!row) return { valid: false }
|
|
76
|
+
if (row.revoked_at !== null) return { valid: false }
|
|
77
|
+
if (row.expires_at < now) return { valid: false }
|
|
78
|
+
|
|
79
|
+
return { valid: true, userId: row.user_id }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Revoca un refresh token impostando revoked_at.
|
|
84
|
+
* Il token non potrà più essere usato per il refresh.
|
|
85
|
+
*/
|
|
86
|
+
export async function revokeRefreshToken(
|
|
87
|
+
db: D1Database,
|
|
88
|
+
token: string
|
|
89
|
+
): Promise<boolean> {
|
|
90
|
+
const tokenHash = await hashRefreshToken(token)
|
|
91
|
+
const now = Math.floor(Date.now() / 1000)
|
|
92
|
+
|
|
93
|
+
const result = await db.prepare(
|
|
94
|
+
`UPDATE refresh_tokens
|
|
95
|
+
SET revoked_at = ?
|
|
96
|
+
WHERE token_hash = ?
|
|
97
|
+
AND revoked_at IS NULL
|
|
98
|
+
AND expires_at >= ?`
|
|
99
|
+
).bind(now, tokenHash, now).run()
|
|
100
|
+
|
|
101
|
+
const changes = (result as unknown as { meta?: { changes?: number } })?.meta?.changes ?? 0
|
|
102
|
+
return changes > 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Genera JWT access token con scadenza breve (15 min).
|
|
107
|
+
* Payload: sub (userId), email, name (opzionale). Algoritmo HS256.
|
|
108
|
+
*/
|
|
109
|
+
export async function generateAccessToken(
|
|
110
|
+
userId: string,
|
|
111
|
+
email: string,
|
|
112
|
+
secret: string,
|
|
113
|
+
options: JwtTokenOptions = {},
|
|
114
|
+
name?: string
|
|
115
|
+
): Promise<string> {
|
|
116
|
+
const payload: Record<string, string> = { email }
|
|
117
|
+
if (name) payload.name = name
|
|
118
|
+
const secretBytes = new TextEncoder().encode(secret)
|
|
119
|
+
let jwt = new SignJWT(payload)
|
|
120
|
+
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
|
|
121
|
+
.setSubject(userId)
|
|
122
|
+
.setIssuedAt()
|
|
123
|
+
.setExpirationTime('15m')
|
|
124
|
+
if (options.issuer) jwt = jwt.setIssuer(options.issuer)
|
|
125
|
+
if (options.audience) jwt = jwt.setAudience(options.audience)
|
|
126
|
+
return jwt.sign(secretBytes)
|
|
127
|
+
}
|