@actuate-media/cms-core 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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/actions/document-crud.test.js +1 -1
  3. package/dist/__tests__/actions/document-crud.test.js.map +1 -1
  4. package/dist/__tests__/scheduling/scheduling.test.js +1 -1
  5. package/dist/__tests__/scheduling/scheduling.test.js.map +1 -1
  6. package/dist/__tests__/security/access.test.js +1 -1
  7. package/dist/__tests__/security/access.test.js.map +1 -1
  8. package/dist/__tests__/security/reauth.test.js +1 -1
  9. package/dist/__tests__/security/reauth.test.js.map +1 -1
  10. package/dist/__tests__/security/sanitize.test.js +1 -1
  11. package/dist/__tests__/security/sanitize.test.js.map +1 -1
  12. package/dist/__tests__/webhooks/webhooks.test.js +2 -2
  13. package/dist/__tests__/webhooks/webhooks.test.js.map +1 -1
  14. package/dist/actions.js +4 -4
  15. package/dist/actions.js.map +1 -1
  16. package/dist/api/handler-factory.d.ts.map +1 -1
  17. package/dist/api/handler-factory.js +26 -7
  18. package/dist/api/handler-factory.js.map +1 -1
  19. package/dist/api/handlers.d.ts +1 -1
  20. package/dist/api/handlers.d.ts.map +1 -1
  21. package/dist/api/handlers.js +339 -75
  22. package/dist/api/handlers.js.map +1 -1
  23. package/dist/api/index.d.ts +3 -3
  24. package/dist/api/index.d.ts.map +1 -1
  25. package/dist/api/index.js +2 -2
  26. package/dist/api/index.js.map +1 -1
  27. package/dist/auth/index.d.ts +10 -10
  28. package/dist/auth/index.d.ts.map +1 -1
  29. package/dist/auth/index.js +8 -8
  30. package/dist/auth/index.js.map +1 -1
  31. package/dist/auth/oauth.d.ts +1 -1
  32. package/dist/auth/oauth.d.ts.map +1 -1
  33. package/dist/auth/oauth.js +1 -1
  34. package/dist/auth/oauth.js.map +1 -1
  35. package/dist/auth/password.d.ts +2 -2
  36. package/dist/auth/password.d.ts.map +1 -1
  37. package/dist/auth/password.js +1 -1
  38. package/dist/auth/password.js.map +1 -1
  39. package/dist/auth/providers/github.d.ts +1 -1
  40. package/dist/auth/providers/github.d.ts.map +1 -1
  41. package/dist/auth/providers/google.d.ts +1 -1
  42. package/dist/auth/providers/google.d.ts.map +1 -1
  43. package/dist/auth/providers/microsoft.d.ts +1 -1
  44. package/dist/auth/providers/microsoft.d.ts.map +1 -1
  45. package/dist/cache/index.d.ts +1 -1
  46. package/dist/cache/index.d.ts.map +1 -1
  47. package/dist/codegen/index.d.ts.map +1 -1
  48. package/dist/codegen/index.js +2 -2
  49. package/dist/codegen/index.js.map +1 -1
  50. package/dist/collections/index.d.ts +1 -1
  51. package/dist/collections/index.d.ts.map +1 -1
  52. package/dist/config/define.d.ts +8 -0
  53. package/dist/config/define.d.ts.map +1 -0
  54. package/dist/config/define.js +7 -0
  55. package/dist/config/define.js.map +1 -0
  56. package/dist/config/index.d.ts +3 -3
  57. package/dist/config/index.d.ts.map +1 -1
  58. package/dist/config/index.js +1 -1
  59. package/dist/config/index.js.map +1 -1
  60. package/dist/config/types.d.ts +25 -3
  61. package/dist/config/types.d.ts.map +1 -1
  62. package/dist/content/index.d.ts +7 -7
  63. package/dist/content/index.d.ts.map +1 -1
  64. package/dist/content/index.js +4 -4
  65. package/dist/content/index.js.map +1 -1
  66. package/dist/db/adapters/mysql.js +1 -1
  67. package/dist/db/adapters/mysql.js.map +1 -1
  68. package/dist/db/adapters/postgres.js +1 -1
  69. package/dist/db/adapters/postgres.js.map +1 -1
  70. package/dist/db/adapters/sqlite.js +1 -1
  71. package/dist/db/adapters/sqlite.js.map +1 -1
  72. package/dist/fields/index.d.ts +1 -1
  73. package/dist/fields/index.d.ts.map +1 -1
  74. package/dist/forms/index.d.ts +4 -4
  75. package/dist/forms/index.d.ts.map +1 -1
  76. package/dist/forms/index.js +2 -2
  77. package/dist/forms/index.js.map +1 -1
  78. package/dist/graphql/index.d.ts +1 -1
  79. package/dist/graphql/index.d.ts.map +1 -1
  80. package/dist/graphql/index.js +4 -4
  81. package/dist/graphql/index.js.map +1 -1
  82. package/dist/i18n/index.d.ts +1 -1
  83. package/dist/i18n/index.d.ts.map +1 -1
  84. package/dist/index.d.ts +72 -72
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +40 -40
  87. package/dist/index.js.map +1 -1
  88. package/dist/media/index.d.ts +2 -2
  89. package/dist/media/index.d.ts.map +1 -1
  90. package/dist/media/index.js +1 -1
  91. package/dist/media/index.js.map +1 -1
  92. package/dist/middleware.d.ts +10 -2
  93. package/dist/middleware.d.ts.map +1 -1
  94. package/dist/middleware.js +1 -1
  95. package/dist/middleware.js.map +1 -1
  96. package/dist/next/preview.js +1 -1
  97. package/dist/next/preview.js.map +1 -1
  98. package/dist/next.d.ts +2 -2
  99. package/dist/next.d.ts.map +1 -1
  100. package/dist/next.js +31 -1
  101. package/dist/next.js.map +1 -1
  102. package/dist/search/index.js +1 -1
  103. package/dist/search/index.js.map +1 -1
  104. package/dist/security/access.d.ts +1 -1
  105. package/dist/security/access.d.ts.map +1 -1
  106. package/dist/security/audit.js +2 -2
  107. package/dist/security/audit.js.map +1 -1
  108. package/dist/security/captcha.d.ts +32 -0
  109. package/dist/security/captcha.d.ts.map +1 -0
  110. package/dist/security/captcha.js +101 -0
  111. package/dist/security/captcha.js.map +1 -0
  112. package/dist/security/index.d.ts +32 -30
  113. package/dist/security/index.d.ts.map +1 -1
  114. package/dist/security/index.js +20 -19
  115. package/dist/security/index.js.map +1 -1
  116. package/dist/security/middleware.d.ts +2 -2
  117. package/dist/security/middleware.d.ts.map +1 -1
  118. package/dist/security/middleware.js +2 -2
  119. package/dist/security/middleware.js.map +1 -1
  120. package/dist/security/reauth.js +2 -2
  121. package/dist/security/reauth.js.map +1 -1
  122. package/dist/seo/index.d.ts +8 -8
  123. package/dist/seo/index.d.ts.map +1 -1
  124. package/dist/seo/index.js +4 -4
  125. package/dist/seo/index.js.map +1 -1
  126. package/dist/setup/index.js +1 -1
  127. package/dist/setup/index.js.map +1 -1
  128. package/dist/upgrade/index.d.ts +6 -6
  129. package/dist/upgrade/index.d.ts.map +1 -1
  130. package/dist/upgrade/index.js +3 -3
  131. package/dist/upgrade/index.js.map +1 -1
  132. package/dist/upgrade/upgrade-pr.d.ts +1 -1
  133. package/dist/upgrade/upgrade-pr.d.ts.map +1 -1
  134. package/dist/upgrade/upgrade-pr.js +107 -17
  135. package/dist/upgrade/upgrade-pr.js.map +1 -1
  136. package/dist/upgrade/version-check.d.ts +10 -2
  137. package/dist/upgrade/version-check.d.ts.map +1 -1
  138. package/dist/upgrade/version-check.js +57 -11
  139. package/dist/upgrade/version-check.js.map +1 -1
  140. package/dist/webhooks/index.js +2 -2
  141. package/dist/webhooks/index.js.map +1 -1
  142. package/dist/workflow/index.js +1 -1
  143. package/dist/workflow/index.js.map +1 -1
  144. package/package.json +21 -13
  145. package/prisma/cms-schema.prisma +237 -0
  146. package/prisma/migrations/0001_init/migration.sql +384 -0
  147. package/prisma/migrations/0002_folders/migration.sql +39 -0
  148. package/prisma/migrations/0003_search_and_webhooks/migration.sql +50 -0
  149. package/prisma/migrations/migration_lock.toml +3 -0
  150. package/prisma/schema.prisma +485 -0
  151. package/prisma/seed.ts +82 -0
@@ -0,0 +1,485 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../generated"
4
+ previewFeatures = ["fullTextSearchPostgres"]
5
+ }
6
+
7
+ datasource db {
8
+ provider = "postgresql"
9
+ }
10
+
11
+ // ─── Enums ──────────────────────────────────────────────────────────
12
+
13
+ enum DocumentStatus {
14
+ DRAFT
15
+ PUBLISHED
16
+ ARCHIVED
17
+ SCHEDULED
18
+ SCHEDULED_UNPUBLISH
19
+ }
20
+
21
+ enum UserRole {
22
+ ADMIN
23
+ EDITOR
24
+ AUTHOR
25
+ CLIENT
26
+ }
27
+
28
+ enum ChangeType {
29
+ CREATE
30
+ UPDATE
31
+ PUBLISH
32
+ RESTORE
33
+ DELETE
34
+ }
35
+
36
+ enum AuditEvent {
37
+ login_success
38
+ login_failed
39
+ logout
40
+ password_change
41
+ password_reset_request
42
+ password_reset_complete
43
+ totp_enabled
44
+ totp_disabled
45
+ api_key_created
46
+ api_key_revoked
47
+ document_created
48
+ document_updated
49
+ document_published
50
+ document_deleted
51
+ media_uploaded
52
+ media_deleted
53
+ user_created
54
+ user_updated
55
+ user_deactivated
56
+ settings_changed
57
+ version_restored
58
+ export_requested
59
+ import_completed
60
+ }
61
+
62
+ enum NotificationType {
63
+ DOCUMENT_PUBLISHED
64
+ DOCUMENT_ASSIGNED
65
+ REVIEW_REQUESTED
66
+ REVIEW_APPROVED
67
+ COMMENT_ADDED
68
+ SYSTEM_ALERT
69
+ WORKFLOW_STEP
70
+ }
71
+
72
+ enum OrderStatus {
73
+ PENDING
74
+ CONFIRMED
75
+ PROCESSING
76
+ SHIPPED
77
+ DELIVERED
78
+ CANCELLED
79
+ REFUNDED
80
+ }
81
+
82
+ enum PaymentStatus {
83
+ PENDING
84
+ AUTHORIZED
85
+ CAPTURED
86
+ FAILED
87
+ REFUNDED
88
+ PARTIALLY_REFUNDED
89
+ }
90
+
91
+ enum WorkflowStage {
92
+ DRAFT
93
+ IN_REVIEW
94
+ APPROVED
95
+ PUBLISHED
96
+ ARCHIVED
97
+ }
98
+
99
+ enum DiscountType {
100
+ PERCENTAGE
101
+ FIXED_AMOUNT
102
+ FREE_SHIPPING
103
+ }
104
+
105
+ enum ProductType {
106
+ PHYSICAL
107
+ DIGITAL
108
+ SUBSCRIPTION
109
+ BUNDLE
110
+ }
111
+
112
+ // ─── Models ─────────────────────────────────────────────────────────
113
+
114
+ model User {
115
+ id String @id @default(cuid())
116
+ email String @unique
117
+ name String
118
+ avatarUrl String?
119
+ role UserRole @default(AUTHOR)
120
+ passwordHash String?
121
+ totpSecret String?
122
+ totpEnabled Boolean @default(false)
123
+ backupCodes Json?
124
+ authProvider String?
125
+ isActive Boolean @default(true)
126
+ isApproved Boolean @default(false)
127
+ emailVerified Boolean @default(false)
128
+ lastLoginAt DateTime?
129
+ createdAt DateTime @default(now())
130
+ updatedAt DateTime @updatedAt
131
+
132
+ sessions Session[]
133
+ oauthAccounts OAuthAccount[]
134
+ apiKeys ApiKey[]
135
+ documents Document[] @relation("CreatedDocuments")
136
+ updatedDocuments Document[] @relation("UpdatedDocuments")
137
+ versions Version[]
138
+ media Media[]
139
+ contentLocks ContentLock[]
140
+ notifications InAppNotification[]
141
+ contentTemplates ContentTemplate[]
142
+ workflowAssigned WorkflowState[]
143
+
144
+ @@map("actuate_users")
145
+ }
146
+
147
+ model OAuthAccount {
148
+ id String @id @default(cuid())
149
+ userId String
150
+ provider String
151
+ providerAccountId String
152
+ accessToken String?
153
+ refreshToken String?
154
+ expiresAt DateTime?
155
+ createdAt DateTime @default(now())
156
+
157
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
158
+
159
+ @@unique([provider, providerAccountId])
160
+ @@map("actuate_oauth_accounts")
161
+ }
162
+
163
+ model Session {
164
+ id String @id @default(cuid())
165
+ userId String
166
+ token String @unique
167
+ expiresAt DateTime
168
+ revokedAt DateTime?
169
+ ipAddress String?
170
+ userAgent String?
171
+ fingerprintHash String?
172
+ createdAt DateTime @default(now())
173
+
174
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
175
+
176
+ @@index([userId])
177
+ @@index([expiresAt])
178
+ @@map("actuate_sessions")
179
+ }
180
+
181
+ model ApiKey {
182
+ id String @id @default(cuid())
183
+ name String
184
+ keyHash String @unique
185
+ keyPrefix String
186
+ userId String
187
+ scopes Json
188
+ ipRestrictions Json?
189
+ expiresAt DateTime?
190
+ lastUsedAt DateTime?
191
+ revokedAt DateTime?
192
+ createdAt DateTime @default(now())
193
+
194
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
195
+
196
+ @@map("actuate_api_keys")
197
+ }
198
+
199
+ model AuditLog {
200
+ id String @id @default(cuid())
201
+ event String
202
+ userId String?
203
+ ipAddress String?
204
+ userAgent String?
205
+ details Json?
206
+ timestamp DateTime @default(now())
207
+
208
+ @@index([event])
209
+ @@index([userId])
210
+ @@index([timestamp])
211
+ @@map("actuate_audit_logs")
212
+ }
213
+
214
+ model Folder {
215
+ id String @id @default(cuid())
216
+ name String
217
+ scope String
218
+ parentId String?
219
+ position Int @default(0)
220
+ createdAt DateTime @default(now())
221
+ updatedAt DateTime @updatedAt
222
+
223
+ parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
224
+ children Folder[] @relation("FolderTree")
225
+
226
+ documents Document[]
227
+ media Media[]
228
+
229
+ @@index([scope])
230
+ @@index([parentId])
231
+ @@map("actuate_folders")
232
+ }
233
+
234
+ model Document {
235
+ id String @id @default(cuid())
236
+ collection String
237
+ title String?
238
+ slug String?
239
+ data Json
240
+ status DocumentStatus @default(DRAFT)
241
+ locale String?
242
+ localeGroupId String?
243
+ createdById String
244
+ updatedById String
245
+ publishedAt DateTime?
246
+ scheduledAt DateTime?
247
+ scheduledUnpublishAt DateTime?
248
+ deletedAt DateTime?
249
+ searchVector String?
250
+ plainText String?
251
+ contentHash String?
252
+ structuredData Json?
253
+ contentGraph Json?
254
+ siteId String?
255
+ templateId String?
256
+ workflowStage WorkflowStage @default(DRAFT)
257
+ reviewerId String?
258
+ reviewNote String?
259
+ folderId String?
260
+ createdAt DateTime @default(now())
261
+ updatedAt DateTime @updatedAt
262
+
263
+ createdBy User @relation("CreatedDocuments", fields: [createdById], references: [id])
264
+ updatedBy User @relation("UpdatedDocuments", fields: [updatedById], references: [id])
265
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
266
+ versions Version[]
267
+ mediaUsages MediaUsage[]
268
+ contentLocks ContentLock[]
269
+
270
+ @@unique([collection, slug])
271
+ @@index([collection])
272
+ @@index([status])
273
+ @@index([deletedAt])
274
+ @@index([publishedAt])
275
+ @@index([folderId])
276
+ @@index([scheduledAt])
277
+ @@index([localeGroupId])
278
+ @@index([createdById])
279
+ @@map("actuate_documents")
280
+ }
281
+
282
+ model Version {
283
+ id String @id @default(cuid())
284
+ documentId String
285
+ data Json
286
+ changedById String
287
+ changeType ChangeType
288
+ createdAt DateTime @default(now())
289
+
290
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
291
+ changedBy User @relation(fields: [changedById], references: [id])
292
+
293
+ @@index([documentId])
294
+ @@map("actuate_versions")
295
+ }
296
+
297
+ model Media {
298
+ id String @id @default(cuid())
299
+ filename String
300
+ storageKey String @unique
301
+ mimeType String
302
+ fileSize Int
303
+ width Int?
304
+ height Int?
305
+ altText String?
306
+ title String?
307
+ uploadedById String
308
+ focalPointX Float?
309
+ focalPointY Float?
310
+ blurHash String?
311
+ aiAltText String?
312
+ aiTags Json?
313
+ aiDescription String?
314
+ folderId String?
315
+ createdAt DateTime @default(now())
316
+ updatedAt DateTime @updatedAt
317
+
318
+ uploadedBy User @relation(fields: [uploadedById], references: [id])
319
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
320
+ mediaUsages MediaUsage[]
321
+
322
+ @@index([folderId])
323
+ @@map("actuate_media")
324
+ }
325
+
326
+ model MediaUsage {
327
+ id String @id @default(cuid())
328
+ mediaId String
329
+ documentId String
330
+ fieldName String
331
+ createdAt DateTime @default(now())
332
+
333
+ media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
334
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
335
+
336
+ @@unique([mediaId, documentId, fieldName])
337
+ @@index([documentId])
338
+ @@map("actuate_media_usages")
339
+ }
340
+
341
+ model ContentLock {
342
+ id String @id @default(cuid())
343
+ documentId String
344
+ userId String
345
+ lockedAt DateTime @default(now())
346
+ expiresAt DateTime
347
+
348
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
349
+ user User @relation(fields: [userId], references: [id])
350
+
351
+ @@map("actuate_content_locks")
352
+ }
353
+
354
+ model InAppNotification {
355
+ id String @id @default(cuid())
356
+ userId String
357
+ type String
358
+ title String
359
+ message String?
360
+ link String?
361
+ read Boolean @default(false)
362
+ createdAt DateTime @default(now())
363
+
364
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
365
+
366
+ @@map("actuate_notifications")
367
+ }
368
+
369
+ model ContentTemplate {
370
+ id String @id @default(cuid())
371
+ name String
372
+ collection String
373
+ templateData Json
374
+ createdById String
375
+ createdAt DateTime @default(now())
376
+ updatedAt DateTime @updatedAt
377
+
378
+ createdBy User @relation(fields: [createdById], references: [id])
379
+
380
+ @@map("actuate_content_templates")
381
+ }
382
+
383
+ model Site {
384
+ id String @id @default(cuid())
385
+ domain String @unique
386
+ name String
387
+ configOverrides Json?
388
+ isDefault Boolean @default(false)
389
+ createdAt DateTime @default(now())
390
+ updatedAt DateTime @updatedAt
391
+
392
+ @@map("actuate_sites")
393
+ }
394
+
395
+ model WorkflowState {
396
+ id String @id @default(cuid())
397
+ documentId String @unique
398
+ currentStep Int @default(0)
399
+ assignedToId String?
400
+ status String @default("pending")
401
+ definition Json
402
+ createdAt DateTime @default(now())
403
+ updatedAt DateTime @updatedAt
404
+
405
+ assignedTo User? @relation(fields: [assignedToId], references: [id])
406
+
407
+ @@map("actuate_workflow_states")
408
+ }
409
+
410
+ model Redirect {
411
+ id String @id @default(cuid())
412
+ source String
413
+ destination String
414
+ statusCode Int @default(301)
415
+ isRegex Boolean @default(false)
416
+ notes String?
417
+ createdAt DateTime @default(now())
418
+ updatedAt DateTime @updatedAt
419
+
420
+ @@unique([source])
421
+ @@map("actuate_redirects")
422
+ }
423
+
424
+ model FormSubmission {
425
+ id String @id @default(cuid())
426
+ formId String
427
+ data Json
428
+ attribution Json?
429
+ status String @default("new")
430
+ submittedAt DateTime @default(now())
431
+ createdAt DateTime @default(now())
432
+
433
+ @@index([formId])
434
+ @@index([status])
435
+ @@map("actuate_form_submissions")
436
+ }
437
+
438
+ model BackupRecord {
439
+ id String @id @default(cuid())
440
+ filename String
441
+ storageKey String
442
+ sizeBytes Int
443
+ type String
444
+ metadata Json?
445
+ createdAt DateTime @default(now())
446
+
447
+ @@map("actuate_backup_records")
448
+ }
449
+
450
+ model WebhookEndpoint {
451
+ id String @id @default(cuid())
452
+ url String
453
+ events Json
454
+ secret String
455
+ active Boolean @default(true)
456
+ name String?
457
+ createdAt DateTime @default(now())
458
+ updatedAt DateTime @updatedAt
459
+
460
+ deliveries WebhookDeliveryLog[]
461
+
462
+ @@map("actuate_webhook_endpoints")
463
+ }
464
+
465
+ model WebhookDeliveryLog {
466
+ id String @id @default(cuid())
467
+ endpointId String
468
+ event String
469
+ payload Json
470
+ status String @default("pending")
471
+ attempts Int @default(0)
472
+ maxAttempts Int @default(3)
473
+ lastAttemptAt DateTime?
474
+ nextRetryAt DateTime?
475
+ responseStatus Int?
476
+ responseBody String?
477
+ createdAt DateTime @default(now())
478
+
479
+ endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade)
480
+
481
+ @@index([endpointId])
482
+ @@index([status])
483
+ @@index([nextRetryAt])
484
+ @@map("actuate_webhook_deliveries")
485
+ }
package/prisma/seed.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Seed script for Actuate CMS.
3
+ *
4
+ * Reads credentials from environment variables (never hardcoded):
5
+ * CMS_ADMIN_EMAIL, CMS_ADMIN_PASSWORD, CMS_ADMIN_NAME
6
+ *
7
+ * Usage:
8
+ * cd packages/cms-core
9
+ * pnpm tsx prisma/seed.ts
10
+ *
11
+ * Requires DATABASE_URL + CMS_ADMIN_* vars in environment or .env file.
12
+ */
13
+
14
+ import 'dotenv/config';
15
+ import pg from 'pg';
16
+ import { PrismaPg } from '@prisma/adapter-pg';
17
+ import { PrismaClient } from '../generated/client.js';
18
+
19
+ async function main() {
20
+ const email = process.env.CMS_ADMIN_EMAIL;
21
+ const password = process.env.CMS_ADMIN_PASSWORD;
22
+ const name = process.env.CMS_ADMIN_NAME ?? 'Admin';
23
+
24
+ if (!email || !password) {
25
+ console.error('Set CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD in your .env file');
26
+ process.exit(1);
27
+ }
28
+
29
+ const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
30
+ const adapter = new PrismaPg(pool);
31
+ const prisma = new PrismaClient({ adapter });
32
+
33
+ try {
34
+ const existingCount = await prisma.user.count();
35
+ if (existingCount > 0) {
36
+ console.log(`Skipping seed — ${existingCount} user(s) already exist.`);
37
+ return;
38
+ }
39
+
40
+ const salt = crypto.getRandomValues(new Uint8Array(16));
41
+ const key = await crypto.subtle.importKey(
42
+ 'raw',
43
+ new TextEncoder().encode(password),
44
+ 'PBKDF2',
45
+ false,
46
+ ['deriveBits'],
47
+ );
48
+ const derived = await crypto.subtle.deriveBits(
49
+ { name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
50
+ key,
51
+ 256,
52
+ );
53
+ const saltHex = Buffer.from(salt).toString('hex');
54
+ const hashHex = Buffer.from(derived).toString('hex');
55
+ const passwordHash = `pbkdf2:100000:${saltHex}:${hashHex}`;
56
+
57
+ const user = await prisma.user.create({
58
+ data: {
59
+ email: email.toLowerCase().trim(),
60
+ name,
61
+ passwordHash,
62
+ role: 'ADMIN',
63
+ isActive: true,
64
+ isApproved: true,
65
+ emailVerified: true,
66
+ },
67
+ });
68
+
69
+ console.log(`Seeded admin user:`);
70
+ console.log(` Email: ${email}`);
71
+ console.log(` Role: ADMIN`);
72
+ console.log(` ID: ${user.id}`);
73
+ } finally {
74
+ await prisma.$disconnect();
75
+ await pool.end();
76
+ }
77
+ }
78
+
79
+ main().catch((err) => {
80
+ console.error('Seed failed:', err);
81
+ process.exit(1);
82
+ });