@beechcms/api 0.4.0-preview.12 → 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.
Files changed (85) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/index-CewtCjom.css +1 -0
  4. package/assets/dashboard/assets/{index-CTSuGxlX.js → index-FQ6JhvRH.js} +99 -99
  5. package/assets/dashboard/beechLogoDark.svg +48 -48
  6. package/assets/dashboard/index.html +18 -18
  7. package/assets/dashboard/sol.svg +3 -3
  8. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  9. package/migrations/0000_v040_base.sql +213 -213
  10. package/package.json +2 -2
  11. package/src/auth/constants.ts +10 -10
  12. package/src/auth/login.ts +91 -91
  13. package/src/auth/refresh.ts +127 -127
  14. package/src/factory.ts +347 -325
  15. package/src/features/content/constants.ts +10 -10
  16. package/src/features/content/handlers/create.ts +153 -163
  17. package/src/features/content/handlers/delete.ts +76 -85
  18. package/src/features/content/handlers/facets.ts +45 -45
  19. package/src/features/content/handlers/get.ts +116 -116
  20. package/src/features/content/handlers/list.ts +88 -88
  21. package/src/features/content/handlers/update.ts +207 -216
  22. package/src/features/content/index.ts +20 -20
  23. package/src/features/draft/draft.handler.ts +272 -272
  24. package/src/features/draft/index.ts +1 -1
  25. package/src/features/email/email.provider.ts +38 -38
  26. package/src/features/email/email.service.ts +80 -80
  27. package/src/features/email/email.types.ts +98 -98
  28. package/src/features/email/index.ts +28 -28
  29. package/src/features/email/providers/resend.ts +63 -63
  30. package/src/features/email/templates/password-changed.ts +59 -59
  31. package/src/features/email/templates/password-reset.ts +64 -64
  32. package/src/features/email/templates/shell.ts +92 -93
  33. package/src/features/notifications/index.ts +1 -1
  34. package/src/features/notifications/notifications.handler.ts +130 -88
  35. package/src/features/password-reset/index.ts +15 -15
  36. package/src/features/password-reset/request.ts +106 -88
  37. package/src/features/password-reset/reset.ts +128 -110
  38. package/src/features/rotate-field/index.ts +1 -1
  39. package/src/features/rotate-field/rotate-field.handler.ts +132 -82
  40. package/src/features/rotate-field/rotate-field.schema.ts +13 -9
  41. package/src/features/schema/schema.handler.ts +16 -16
  42. package/src/features/settings/settings.handler.ts +414 -267
  43. package/src/features/setup/index.ts +96 -59
  44. package/src/features/stats/index.ts +1 -1
  45. package/src/features/stats/stats.handler.ts +458 -395
  46. package/src/index.ts +24 -24
  47. package/src/media-utils.ts +78 -78
  48. package/src/middleware/repository.middleware.ts +24 -18
  49. package/src/middleware/storage.middleware.ts +21 -0
  50. package/src/middleware.ts +67 -67
  51. package/src/public/access-policy.ts +23 -23
  52. package/src/public/api-key-middleware.ts +53 -53
  53. package/src/public/index.ts +12 -12
  54. package/src/public/problem-details.ts +48 -42
  55. package/src/public/public-add.ts +166 -166
  56. package/src/public/public-edit.ts +158 -158
  57. package/src/public/public-errors.ts +15 -15
  58. package/src/public/public-read.ts +216 -216
  59. package/src/public/public-routes.ts +84 -31
  60. package/src/public/query-builder.ts +152 -152
  61. package/src/public/rate-limit-middleware.ts +42 -42
  62. package/src/public/response-builder.ts +26 -26
  63. package/src/public/sanitize.ts +65 -65
  64. package/src/public/slug-utils.ts +14 -14
  65. package/src/search-utils.ts +192 -192
  66. package/src/search.ts +72 -72
  67. package/src/shared/activity-logger.ts +79 -79
  68. package/src/shared/apply-policies.ts +63 -63
  69. package/src/shared/base.repository.d1.ts +28 -28
  70. package/src/shared/content-utils.ts +82 -108
  71. package/src/shared/content.repository.d1.ts +382 -382
  72. package/src/shared/fts-sync.ts +4 -4
  73. package/src/shared/idempotency.repository.d1.ts +56 -45
  74. package/src/shared/media.repository.d1.ts +64 -0
  75. package/src/shared/notification-service.ts +56 -56
  76. package/src/shared/query-utils.ts +137 -137
  77. package/src/shared/storage/factory.ts +40 -0
  78. package/src/shared/storage/r2-binding-bucket.ts +81 -0
  79. package/src/shared/storage/s3-bucket.ts +163 -0
  80. package/src/shared/storage-utils.ts +36 -36
  81. package/src/shared/system-stats.repository.d1.ts +44 -0
  82. package/src/types.ts +46 -41
  83. package/src/upload.ts +179 -331
  84. package/src/widget.ts +349 -349
  85. package/assets/dashboard/assets/index-ye3325L9.css +0 -1
@@ -1,213 +1,213 @@
1
- -- =============================================================================
2
- -- Beech CMS v0.4.0 — Base Schema
3
- --
4
- -- Single migration for fresh installations. Creates all system tables.
5
- --
6
- -- Content tables (content_{slug}, fts_{slug}, content_{slug}_drafts) are
7
- -- NOT defined here — they are generated at deploy time by: beech seed:load
8
- -- FTS indexes text + richtext branches (policies.search !== false).
9
- --
10
- -- Usage:
11
- -- wrangler d1 execute beech-db --file=migrations/0000_v040_base.sql (remote)
12
- -- wrangler d1 execute beech-db --local --file=migrations/0000_v040_base.sql (local)
13
- --
14
- -- After this migration, run:
15
- -- beech seed:load → creates content_{slug} tables + FTS + triggers
16
- -- POST /auth/setup → creates first admin user
17
- -- =============================================================================
18
-
19
-
20
- -- =============================================================================
21
- -- 1. USERS
22
- -- Dashboard users (admins + editors). Passwords stored as bcrypt hashes.
23
- -- Profile fields (name, avatar_url, notification_prefs) merged in from
24
- -- the start — no incremental ALTER TABLE needed.
25
- -- =============================================================================
26
-
27
- CREATE TABLE IF NOT EXISTS users (
28
- id TEXT NOT NULL PRIMARY KEY,
29
- email TEXT NOT NULL UNIQUE,
30
- password_hash TEXT NOT NULL,
31
- role TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('admin', 'editor')),
32
- name TEXT,
33
- avatar_url TEXT,
34
- notification_prefs TEXT NOT NULL DEFAULT '{}',
35
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
36
- );
37
-
38
-
39
- -- =============================================================================
40
- -- 2. REFRESH TOKENS
41
- -- JWT refresh token rotation. Stored as SHA-256 hashes, never plaintext.
42
- -- revoked_at IS NULL = active token.
43
- -- =============================================================================
44
-
45
- CREATE TABLE IF NOT EXISTS refresh_tokens (
46
- id TEXT NOT NULL PRIMARY KEY,
47
- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
48
- token_hash TEXT NOT NULL,
49
- expires_at INTEGER NOT NULL,
50
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
51
- revoked_at INTEGER DEFAULT NULL
52
- );
53
-
54
- CREATE INDEX IF NOT EXISTS idx_refresh_user ON refresh_tokens(user_id);
55
- CREATE INDEX IF NOT EXISTS idx_refresh_hash ON refresh_tokens(token_hash);
56
- CREATE INDEX IF NOT EXISTS idx_refresh_expires ON refresh_tokens(expires_at);
57
-
58
-
59
- -- =============================================================================
60
- -- 3. PASSWORD RESET TOKENS
61
- -- Single-use, 30-minute TTL, stored as SHA-256 hashes.
62
- -- used_at set on redemption to prevent replay attacks.
63
- -- =============================================================================
64
-
65
- CREATE TABLE IF NOT EXISTS password_reset_tokens (
66
- id TEXT NOT NULL PRIMARY KEY,
67
- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
68
- token_hash TEXT NOT NULL,
69
- expires_at INTEGER NOT NULL,
70
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
71
- used_at INTEGER DEFAULT NULL
72
- );
73
-
74
- CREATE INDEX IF NOT EXISTS idx_prt_hash ON password_reset_tokens(token_hash);
75
- CREATE INDEX IF NOT EXISTS idx_prt_user ON password_reset_tokens(user_id);
76
-
77
-
78
- -- =============================================================================
79
- -- 4. PUBLIC API — IDEMPOTENCY KEYS
80
- -- Deduplicates POST requests from public consumers (e.g. contact forms).
81
- -- expires_at-based pruning keeps the table small.
82
- -- =============================================================================
83
-
84
- CREATE TABLE IF NOT EXISTS public_idempotency_keys (
85
- idempotency_key TEXT NOT NULL PRIMARY KEY,
86
- request_fingerprint TEXT NOT NULL,
87
- response_status INTEGER NOT NULL,
88
- response_body TEXT NOT NULL,
89
- created_at INTEGER NOT NULL,
90
- expires_at INTEGER NOT NULL
91
- );
92
-
93
- CREATE INDEX IF NOT EXISTS idx_idempotency_expires ON public_idempotency_keys(expires_at);
94
-
95
-
96
- -- =============================================================================
97
- -- 5. ANALYTICS
98
- -- Daily metric counters per seed and global (seed = '' sentinel).
99
- -- Metrics: 'requests' | 'visitors' | 'bandwidth_mb'
100
- -- =============================================================================
101
-
102
- CREATE TABLE IF NOT EXISTS analytics (
103
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
104
- day_ts INTEGER NOT NULL,
105
- metric TEXT NOT NULL,
106
- seed TEXT NOT NULL DEFAULT '',
107
- value INTEGER NOT NULL DEFAULT 0,
108
- UNIQUE(day_ts, metric, seed)
109
- );
110
-
111
- CREATE INDEX IF NOT EXISTS idx_analytics_day ON analytics(day_ts);
112
- CREATE INDEX IF NOT EXISTS idx_analytics_seed ON analytics(seed, day_ts);
113
-
114
-
115
- -- =============================================================================
116
- -- 6. SYSTEM STATS
117
- -- Persistent key-value store for system-level counters.
118
- -- 'total_storage_bytes' is kept for dashboard widgets;
119
- -- SUM(size_bytes) FROM media_objects is the authoritative source.
120
- -- =============================================================================
121
-
122
- CREATE TABLE IF NOT EXISTS system_stats (
123
- id TEXT NOT NULL PRIMARY KEY,
124
- value TEXT NOT NULL
125
- );
126
-
127
- INSERT OR IGNORE INTO system_stats (id, value) VALUES ('total_storage_bytes', '0');
128
-
129
-
130
- -- =============================================================================
131
- -- 7. ACTIVITY LOGS
132
- -- Append-only audit log. No FK on user_id — deleted users are retained
133
- -- for compliance. user_name denormalized to survive user deletions.
134
- -- Actions: 'create' | 'update' | 'delete' | 'upload'
135
- -- Entity types: 'content' | 'media'
136
- -- =============================================================================
137
-
138
- CREATE TABLE IF NOT EXISTS activity_logs (
139
- id TEXT NOT NULL PRIMARY KEY,
140
- user_id TEXT NOT NULL,
141
- user_email TEXT NOT NULL,
142
- user_name TEXT,
143
- action TEXT NOT NULL,
144
- entity_type TEXT NOT NULL,
145
- entity_id TEXT NOT NULL,
146
- entity_slug TEXT,
147
- details TEXT,
148
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
149
- );
150
-
151
- CREATE INDEX IF NOT EXISTS idx_activity_user ON activity_logs(user_id);
152
- CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_logs(created_at);
153
-
154
-
155
- -- =============================================================================
156
- -- 8. NOTIFICATIONS
157
- -- In-app notification inbox for system events (e.g. new form submission).
158
- -- is_read: 0 = unread, 1 = read.
159
- -- =============================================================================
160
-
161
- CREATE TABLE IF NOT EXISTS notifications (
162
- id TEXT NOT NULL PRIMARY KEY,
163
- title TEXT NOT NULL,
164
- message TEXT NOT NULL,
165
- type TEXT NOT NULL DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error')),
166
- is_read INTEGER NOT NULL DEFAULT 0 CHECK (is_read IN (0, 1)),
167
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
168
- );
169
-
170
- CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at);
171
- CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(is_read);
172
-
173
-
174
- -- =============================================================================
175
- -- 9. MEDIA OBJECTS
176
- -- Tracks every file uploaded to R2. Enables media library UI, orphan
177
- -- detection, and storage reporting. No FK on uploaded_by for D1 portability.
178
- -- =============================================================================
179
-
180
- CREATE TABLE IF NOT EXISTS media_objects (
181
- key TEXT NOT NULL PRIMARY KEY,
182
- filename TEXT NOT NULL,
183
- mime_type TEXT NOT NULL,
184
- size_bytes INTEGER NOT NULL,
185
- uploaded_by TEXT NOT NULL DEFAULT '',
186
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
187
- );
188
-
189
- CREATE INDEX IF NOT EXISTS idx_media_user ON media_objects(uploaded_by);
190
- CREATE INDEX IF NOT EXISTS idx_media_created ON media_objects(created_at DESC);
191
-
192
-
193
- -- =============================================================================
194
- -- 10. CONTENT EVENT LOG
195
- -- Lightweight event log for activity feed and dashboard stats.
196
- -- Does NOT store field data — only event metadata (who did what, when).
197
- -- Replaces the legacy content_entries table as cross-seed event source.
198
- -- The actual content lives in content_{slug} tables (created by seed:load).
199
- -- =============================================================================
200
-
201
- CREATE TABLE IF NOT EXISTS content_event_log (
202
- id TEXT NOT NULL PRIMARY KEY,
203
- schema_slug TEXT NOT NULL,
204
- entry_id TEXT NOT NULL,
205
- action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
206
- user_id TEXT,
207
- details TEXT,
208
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
209
- );
210
-
211
- CREATE INDEX IF NOT EXISTS idx_event_log_schema_slug ON content_event_log(schema_slug);
212
- CREATE INDEX IF NOT EXISTS idx_event_log_created_at ON content_event_log(created_at DESC);
213
- CREATE INDEX IF NOT EXISTS idx_event_log_entry_id ON content_event_log(entry_id);
1
+ -- =============================================================================
2
+ -- Beech CMS v0.4.0 — Base Schema
3
+ --
4
+ -- Single migration for fresh installations. Creates all system tables.
5
+ --
6
+ -- Content tables (content_{slug}, fts_{slug}, content_{slug}_drafts) are
7
+ -- NOT defined here — they are generated at deploy time by: beech seed:load
8
+ -- FTS indexes text + richtext branches (policies.search !== false).
9
+ --
10
+ -- Usage:
11
+ -- wrangler d1 execute beech-db --file=migrations/0000_v040_base.sql (remote)
12
+ -- wrangler d1 execute beech-db --local --file=migrations/0000_v040_base.sql (local)
13
+ --
14
+ -- After this migration, run:
15
+ -- beech seed:load → creates content_{slug} tables + FTS + triggers
16
+ -- POST /auth/setup → creates first admin user
17
+ -- =============================================================================
18
+
19
+
20
+ -- =============================================================================
21
+ -- 1. USERS
22
+ -- Dashboard users (admins + editors). Passwords stored as bcrypt hashes.
23
+ -- Profile fields (name, avatar_url, notification_prefs) merged in from
24
+ -- the start — no incremental ALTER TABLE needed.
25
+ -- =============================================================================
26
+
27
+ CREATE TABLE IF NOT EXISTS users (
28
+ id TEXT NOT NULL PRIMARY KEY,
29
+ email TEXT NOT NULL UNIQUE,
30
+ password_hash TEXT NOT NULL,
31
+ role TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('admin', 'editor')),
32
+ name TEXT,
33
+ avatar_url TEXT,
34
+ notification_prefs TEXT NOT NULL DEFAULT '{}',
35
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
36
+ );
37
+
38
+
39
+ -- =============================================================================
40
+ -- 2. REFRESH TOKENS
41
+ -- JWT refresh token rotation. Stored as SHA-256 hashes, never plaintext.
42
+ -- revoked_at IS NULL = active token.
43
+ -- =============================================================================
44
+
45
+ CREATE TABLE IF NOT EXISTS refresh_tokens (
46
+ id TEXT NOT NULL PRIMARY KEY,
47
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
48
+ token_hash TEXT NOT NULL,
49
+ expires_at INTEGER NOT NULL,
50
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
51
+ revoked_at INTEGER DEFAULT NULL
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_refresh_user ON refresh_tokens(user_id);
55
+ CREATE INDEX IF NOT EXISTS idx_refresh_hash ON refresh_tokens(token_hash);
56
+ CREATE INDEX IF NOT EXISTS idx_refresh_expires ON refresh_tokens(expires_at);
57
+
58
+
59
+ -- =============================================================================
60
+ -- 3. PASSWORD RESET TOKENS
61
+ -- Single-use, 30-minute TTL, stored as SHA-256 hashes.
62
+ -- used_at set on redemption to prevent replay attacks.
63
+ -- =============================================================================
64
+
65
+ CREATE TABLE IF NOT EXISTS password_reset_tokens (
66
+ id TEXT NOT NULL PRIMARY KEY,
67
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
68
+ token_hash TEXT NOT NULL,
69
+ expires_at INTEGER NOT NULL,
70
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
71
+ used_at INTEGER DEFAULT NULL
72
+ );
73
+
74
+ CREATE INDEX IF NOT EXISTS idx_prt_hash ON password_reset_tokens(token_hash);
75
+ CREATE INDEX IF NOT EXISTS idx_prt_user ON password_reset_tokens(user_id);
76
+
77
+
78
+ -- =============================================================================
79
+ -- 4. PUBLIC API — IDEMPOTENCY KEYS
80
+ -- Deduplicates POST requests from public consumers (e.g. contact forms).
81
+ -- expires_at-based pruning keeps the table small.
82
+ -- =============================================================================
83
+
84
+ CREATE TABLE IF NOT EXISTS public_idempotency_keys (
85
+ idempotency_key TEXT NOT NULL PRIMARY KEY,
86
+ request_fingerprint TEXT NOT NULL,
87
+ response_status INTEGER NOT NULL,
88
+ response_body TEXT NOT NULL,
89
+ created_at INTEGER NOT NULL,
90
+ expires_at INTEGER NOT NULL
91
+ );
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_idempotency_expires ON public_idempotency_keys(expires_at);
94
+
95
+
96
+ -- =============================================================================
97
+ -- 5. ANALYTICS
98
+ -- Daily metric counters per seed and global (seed = '' sentinel).
99
+ -- Metrics: 'requests' | 'visitors' | 'bandwidth_mb'
100
+ -- =============================================================================
101
+
102
+ CREATE TABLE IF NOT EXISTS analytics (
103
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
104
+ day_ts INTEGER NOT NULL,
105
+ metric TEXT NOT NULL,
106
+ seed TEXT NOT NULL DEFAULT '',
107
+ value INTEGER NOT NULL DEFAULT 0,
108
+ UNIQUE(day_ts, metric, seed)
109
+ );
110
+
111
+ CREATE INDEX IF NOT EXISTS idx_analytics_day ON analytics(day_ts);
112
+ CREATE INDEX IF NOT EXISTS idx_analytics_seed ON analytics(seed, day_ts);
113
+
114
+
115
+ -- =============================================================================
116
+ -- 6. SYSTEM STATS
117
+ -- Persistent key-value store for system-level counters.
118
+ -- 'total_storage_bytes' is kept for dashboard widgets;
119
+ -- SUM(size_bytes) FROM media_objects is the authoritative source.
120
+ -- =============================================================================
121
+
122
+ CREATE TABLE IF NOT EXISTS system_stats (
123
+ id TEXT NOT NULL PRIMARY KEY,
124
+ value TEXT NOT NULL
125
+ );
126
+
127
+ INSERT OR IGNORE INTO system_stats (id, value) VALUES ('total_storage_bytes', '0');
128
+
129
+
130
+ -- =============================================================================
131
+ -- 7. ACTIVITY LOGS
132
+ -- Append-only audit log. No FK on user_id — deleted users are retained
133
+ -- for compliance. user_name denormalized to survive user deletions.
134
+ -- Actions: 'create' | 'update' | 'delete' | 'upload'
135
+ -- Entity types: 'content' | 'media'
136
+ -- =============================================================================
137
+
138
+ CREATE TABLE IF NOT EXISTS activity_logs (
139
+ id TEXT NOT NULL PRIMARY KEY,
140
+ user_id TEXT NOT NULL,
141
+ user_email TEXT NOT NULL,
142
+ user_name TEXT,
143
+ action TEXT NOT NULL,
144
+ entity_type TEXT NOT NULL,
145
+ entity_id TEXT NOT NULL,
146
+ entity_slug TEXT,
147
+ details TEXT,
148
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
149
+ );
150
+
151
+ CREATE INDEX IF NOT EXISTS idx_activity_user ON activity_logs(user_id);
152
+ CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_logs(created_at);
153
+
154
+
155
+ -- =============================================================================
156
+ -- 8. NOTIFICATIONS
157
+ -- In-app notification inbox for system events (e.g. new form submission).
158
+ -- is_read: 0 = unread, 1 = read.
159
+ -- =============================================================================
160
+
161
+ CREATE TABLE IF NOT EXISTS notifications (
162
+ id TEXT NOT NULL PRIMARY KEY,
163
+ title TEXT NOT NULL,
164
+ message TEXT NOT NULL,
165
+ type TEXT NOT NULL DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error')),
166
+ is_read INTEGER NOT NULL DEFAULT 0 CHECK (is_read IN (0, 1)),
167
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
168
+ );
169
+
170
+ CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at);
171
+ CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(is_read);
172
+
173
+
174
+ -- =============================================================================
175
+ -- 9. MEDIA OBJECTS
176
+ -- Tracks every file uploaded to R2. Enables media library UI, orphan
177
+ -- detection, and storage reporting. No FK on uploaded_by for D1 portability.
178
+ -- =============================================================================
179
+
180
+ CREATE TABLE IF NOT EXISTS media_objects (
181
+ key TEXT NOT NULL PRIMARY KEY,
182
+ filename TEXT NOT NULL,
183
+ mime_type TEXT NOT NULL,
184
+ size_bytes INTEGER NOT NULL,
185
+ uploaded_by TEXT NOT NULL DEFAULT '',
186
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
187
+ );
188
+
189
+ CREATE INDEX IF NOT EXISTS idx_media_user ON media_objects(uploaded_by);
190
+ CREATE INDEX IF NOT EXISTS idx_media_created ON media_objects(created_at DESC);
191
+
192
+
193
+ -- =============================================================================
194
+ -- 10. CONTENT EVENT LOG
195
+ -- Lightweight event log for activity feed and dashboard stats.
196
+ -- Does NOT store field data — only event metadata (who did what, when).
197
+ -- Replaces the legacy content_entries table as cross-seed event source.
198
+ -- The actual content lives in content_{slug} tables (created by seed:load).
199
+ -- =============================================================================
200
+
201
+ CREATE TABLE IF NOT EXISTS content_event_log (
202
+ id TEXT NOT NULL PRIMARY KEY,
203
+ schema_slug TEXT NOT NULL,
204
+ entry_id TEXT NOT NULL,
205
+ action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
206
+ user_id TEXT,
207
+ details TEXT,
208
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
209
+ );
210
+
211
+ CREATE INDEX IF NOT EXISTS idx_event_log_schema_slug ON content_event_log(schema_slug);
212
+ CREATE INDEX IF NOT EXISTS idx_event_log_created_at ON content_event_log(created_at DESC);
213
+ CREATE INDEX IF NOT EXISTS idx_event_log_entry_id ON content_event_log(entry_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/api",
3
- "version": "0.4.0-preview.12",
3
+ "version": "0.4.0-preview.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/factory.ts"
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@aws-sdk/client-s3": "^3.995.0",
25
- "@beechcms/core": "^0.4.0-preview.12",
25
+ "@beechcms/core": "^0.4.0-preview.13",
26
26
  "bcryptjs": "^2.4.3",
27
27
  "hono": "^4.11.9",
28
28
  "jose": "^6.1.3"
@@ -1,10 +1,10 @@
1
- /** Messaggi di errore API auth - usati da handler e test */
2
- export const AUTH_ERRORS = {
3
- INVALID_REQUEST: 'Invalid request',
4
- INVALID_CREDENTIALS: 'Invalid credentials',
5
- DATABASE_ERROR: 'Database error',
6
- /** Messaggio generico per errori 500 - non rivela dettagli del sistema */
7
- GENERIC_ERROR: 'An error occurred',
8
- /** Rate limit superato */
9
- RATE_LIMIT_EXCEEDED: 'Too many requests',
10
- } as const
1
+ /** Messaggi di errore API auth - usati da handler e test */
2
+ export const AUTH_ERRORS = {
3
+ INVALID_REQUEST: 'Invalid request',
4
+ INVALID_CREDENTIALS: 'Invalid credentials',
5
+ DATABASE_ERROR: 'Database error',
6
+ /** Messaggio generico per errori 500 - non rivela dettagli del sistema */
7
+ GENERIC_ERROR: 'An error occurred',
8
+ /** Rate limit superato */
9
+ RATE_LIMIT_EXCEEDED: 'Too many requests',
10
+ } as const
package/src/auth/login.ts CHANGED
@@ -1,91 +1,91 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import bcrypt from 'bcryptjs'
3
-
4
- export type LoginCredentials = {
5
- email: string
6
- password: string
7
- }
8
-
9
- export type UserRecord = {
10
- id: string
11
- email: string
12
- password_hash: string
13
- }
14
-
15
- /** Regex per validare formato email (deve contenere @ e punto dopo @) */
16
- const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
17
-
18
- /** Lunghezza minima password (caratteri) */
19
- const MIN_PASSWORD_LENGTH = 8
20
-
21
- /** Lunghezza massima password (caratteri) - limite ragionevole per evitare DoS */
22
- const MAX_PASSWORD_LENGTH = 128
23
-
24
- /**
25
- * Hash bcrypt dummy valido.
26
- * Usato quando l'utente non esiste per evitare timing attack
27
- * (stesso tempo di risposta che con password errata).
28
- */
29
- export const DUMMY_PASSWORD_HASH =
30
- '$2a$10$SbkRFOafACxVM2ahxerVDu3tSkCXWm29b62WdB.4WGG02Qjsfzni6'
31
-
32
- /**
33
- * Estrae e valida email e password dal body della richiesta.
34
- * @param body - Body grezzo (tipicamente da req.json())
35
- * @returns Oggetto {email, password} se valido, null altrimenti
36
- */
37
- export function parseLoginBody(body: unknown): LoginCredentials | null {
38
- if (body === null || typeof body !== 'object') return null
39
- const obj = body as Record<string, unknown>
40
- const email = obj.email
41
- const password = obj.password
42
- if (typeof email !== 'string' || typeof password !== 'string') return null
43
- if (!email.trim() || !password) return null
44
- return { email: email.trim(), password }
45
- }
46
-
47
- /**
48
- * Verifica che email e password rispettino i formati richiesti.
49
- * @param email - Email da validare (deve contenere @ e punto dopo @)
50
- * @param password - Password (8-128 caratteri)
51
- * @returns true se valido
52
- */
53
- export function validateLoginInput(email: string, password: string): boolean {
54
- return (
55
- EMAIL_REGEX.test(email) &&
56
- password.length >= MIN_PASSWORD_LENGTH &&
57
- password.length <= MAX_PASSWORD_LENGTH
58
- )
59
- }
60
-
61
- /**
62
- * Cerca un utente nel database D1 per email.
63
- * @param db - Istanza D1Database
64
- * @param email - Email dell'utente
65
- * @returns UserRecord se trovato, null altrimenti
66
- */
67
- export async function findUserByEmail(
68
- db: D1Database,
69
- email: string
70
- ): Promise<UserRecord | null> {
71
- // IMPORTANTE: non interpolare mai direttamente l'email (o altri input utente)
72
- // dentro la stringa SQL, usa sempre il placeholder "?" con .bind(...) per evitare SQL injection.
73
- const stmt = db.prepare(
74
- 'SELECT id, email, password_hash FROM users WHERE email = ? LIMIT 1'
75
- )
76
- const row = await stmt.bind(email).first<UserRecord>()
77
- return row
78
- }
79
-
80
- /**
81
- * Verifica che la password in chiaro corrisponda all'hash bcrypt salvato.
82
- * @param plainPassword - Password in chiaro
83
- * @param hash - Hash bcrypt salvato nel DB
84
- * @returns true se la password è corretta
85
- */
86
- export async function verifyPassword(
87
- plainPassword: string,
88
- hash: string
89
- ): Promise<boolean> {
90
- return bcrypt.compare(plainPassword, hash)
91
- }
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import bcrypt from 'bcryptjs'
3
+
4
+ export type LoginCredentials = {
5
+ email: string
6
+ password: string
7
+ }
8
+
9
+ export type UserRecord = {
10
+ id: string
11
+ email: string
12
+ password_hash: string
13
+ }
14
+
15
+ /** Regex per validare formato email (deve contenere @ e punto dopo @) */
16
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
17
+
18
+ /** Lunghezza minima password (caratteri) */
19
+ const MIN_PASSWORD_LENGTH = 8
20
+
21
+ /** Lunghezza massima password (caratteri) - limite ragionevole per evitare DoS */
22
+ const MAX_PASSWORD_LENGTH = 128
23
+
24
+ /**
25
+ * Hash bcrypt dummy valido.
26
+ * Usato quando l'utente non esiste per evitare timing attack
27
+ * (stesso tempo di risposta che con password errata).
28
+ */
29
+ export const DUMMY_PASSWORD_HASH =
30
+ '$2a$10$SbkRFOafACxVM2ahxerVDu3tSkCXWm29b62WdB.4WGG02Qjsfzni6'
31
+
32
+ /**
33
+ * Estrae e valida email e password dal body della richiesta.
34
+ * @param body - Body grezzo (tipicamente da req.json())
35
+ * @returns Oggetto {email, password} se valido, null altrimenti
36
+ */
37
+ export function parseLoginBody(body: unknown): LoginCredentials | null {
38
+ if (body === null || typeof body !== 'object') return null
39
+ const obj = body as Record<string, unknown>
40
+ const email = obj.email
41
+ const password = obj.password
42
+ if (typeof email !== 'string' || typeof password !== 'string') return null
43
+ if (!email.trim() || !password) return null
44
+ return { email: email.trim(), password }
45
+ }
46
+
47
+ /**
48
+ * Verifica che email e password rispettino i formati richiesti.
49
+ * @param email - Email da validare (deve contenere @ e punto dopo @)
50
+ * @param password - Password (8-128 caratteri)
51
+ * @returns true se valido
52
+ */
53
+ export function validateLoginInput(email: string, password: string): boolean {
54
+ return (
55
+ EMAIL_REGEX.test(email) &&
56
+ password.length >= MIN_PASSWORD_LENGTH &&
57
+ password.length <= MAX_PASSWORD_LENGTH
58
+ )
59
+ }
60
+
61
+ /**
62
+ * Cerca un utente nel database D1 per email.
63
+ * @param db - Istanza D1Database
64
+ * @param email - Email dell'utente
65
+ * @returns UserRecord se trovato, null altrimenti
66
+ */
67
+ export async function findUserByEmail(
68
+ db: D1Database,
69
+ email: string
70
+ ): Promise<UserRecord | null> {
71
+ // IMPORTANTE: non interpolare mai direttamente l'email (o altri input utente)
72
+ // dentro la stringa SQL, usa sempre il placeholder "?" con .bind(...) per evitare SQL injection.
73
+ const stmt = db.prepare(
74
+ 'SELECT id, email, password_hash FROM users WHERE email = ? LIMIT 1'
75
+ )
76
+ const row = await stmt.bind(email).first<UserRecord>()
77
+ return row
78
+ }
79
+
80
+ /**
81
+ * Verifica che la password in chiaro corrisponda all'hash bcrypt salvato.
82
+ * @param plainPassword - Password in chiaro
83
+ * @param hash - Hash bcrypt salvato nel DB
84
+ * @returns true se la password è corretta
85
+ */
86
+ export async function verifyPassword(
87
+ plainPassword: string,
88
+ hash: string
89
+ ): Promise<boolean> {
90
+ return bcrypt.compare(plainPassword, hash)
91
+ }