create-sonicjs 3.0.0-beta.3 → 3.0.0-beta.5
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/package.json +1 -1
- package/src/cli.js +84 -98
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
+
import { randomBytes } from 'crypto'
|
|
6
7
|
import prompts from 'prompts'
|
|
7
8
|
import kleur from 'kleur'
|
|
8
9
|
import ora from 'ora'
|
|
@@ -50,7 +51,9 @@ const flags = {
|
|
|
50
51
|
databaseName: args.find(arg => arg.startsWith('--database='))?.split('=')[1],
|
|
51
52
|
bucketName: args.find(arg => arg.startsWith('--bucket='))?.split('=')[1],
|
|
52
53
|
skipExample: args.includes('--skip-example'),
|
|
53
|
-
includeExample: args.includes('--include-example')
|
|
54
|
+
includeExample: args.includes('--include-example'),
|
|
55
|
+
adminEmail: args.find(arg => arg.startsWith('--admin-email='))?.split('=')[1],
|
|
56
|
+
adminPassword: args.find(arg => arg.startsWith('--admin-password='))?.split('=')[1],
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
async function main() {
|
|
@@ -171,39 +174,40 @@ async function getProjectDetails(initialName) {
|
|
|
171
174
|
})
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
// Seed admin user
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
// Seed admin user (skip prompt if credentials provided via flags)
|
|
178
|
+
if (!flags.adminEmail || !flags.adminPassword) {
|
|
179
|
+
questions.push({
|
|
180
|
+
type: 'confirm',
|
|
181
|
+
name: 'seedAdmin',
|
|
182
|
+
message: 'Create admin user?',
|
|
183
|
+
initial: true
|
|
184
|
+
})
|
|
181
185
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
})
|
|
186
|
+
// Admin email (only if seeding)
|
|
187
|
+
questions.push({
|
|
188
|
+
type: (prev, values) => values.seedAdmin ? 'text' : null,
|
|
189
|
+
name: 'adminEmail',
|
|
190
|
+
message: 'Admin email:',
|
|
191
|
+
validate: (value) => {
|
|
192
|
+
if (!value) return 'Admin email is required'
|
|
193
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
194
|
+
if (!emailRegex.test(value)) return 'Please enter a valid email address'
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
})
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
199
|
+
// Admin password (only if seeding)
|
|
200
|
+
questions.push({
|
|
201
|
+
type: (prev, values) => values.seedAdmin ? 'password' : null,
|
|
202
|
+
name: 'adminPassword',
|
|
203
|
+
message: 'Admin password:',
|
|
204
|
+
validate: (value) => {
|
|
205
|
+
if (!value) return 'Admin password is required'
|
|
206
|
+
if (value.length < 8) return 'Password must be at least 8 characters'
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
207
211
|
|
|
208
212
|
// Create Cloudflare resources
|
|
209
213
|
if (!flags.skipCloudflare) {
|
|
@@ -236,9 +240,9 @@ async function getProjectDetails(initialName) {
|
|
|
236
240
|
template: flags.template || 'starter', // Always default to starter template
|
|
237
241
|
databaseName: flags.databaseName || answers.databaseName || `${initialName || answers.projectName}-db`,
|
|
238
242
|
bucketName: flags.bucketName || answers.bucketName || `${initialName || answers.projectName}-media`,
|
|
239
|
-
seedAdmin: answers.seedAdmin !== undefined ? answers.seedAdmin : true,
|
|
240
|
-
adminEmail: answers.adminEmail,
|
|
241
|
-
adminPassword: answers.adminPassword,
|
|
243
|
+
seedAdmin: (flags.adminEmail && flags.adminPassword) ? true : (answers.seedAdmin !== undefined ? answers.seedAdmin : true),
|
|
244
|
+
adminEmail: flags.adminEmail || answers.adminEmail,
|
|
245
|
+
adminPassword: flags.adminPassword || answers.adminPassword,
|
|
242
246
|
includeExample: true,
|
|
243
247
|
createResources: flags.skipCloudflare ? false : answers.createResources,
|
|
244
248
|
runMigrations: true, // Always run migrations automatically
|
|
@@ -405,7 +409,7 @@ async function copyTemplate(templateName, targetDir, options) {
|
|
|
405
409
|
|
|
406
410
|
// Add @sonicjs-cms/core dependency
|
|
407
411
|
packageJson.dependencies = {
|
|
408
|
-
'@sonicjs-cms/core': '^3.0.0-beta.
|
|
412
|
+
'@sonicjs-cms/core': '^3.0.0-beta.5',
|
|
409
413
|
...packageJson.dependencies
|
|
410
414
|
}
|
|
411
415
|
|
|
@@ -444,100 +448,88 @@ async function copyTemplate(templateName, targetDir, options) {
|
|
|
444
448
|
password: options.adminPassword
|
|
445
449
|
})
|
|
446
450
|
}
|
|
451
|
+
|
|
452
|
+
// Generate .dev.vars with a random BETTER_AUTH_SECRET for local dev
|
|
453
|
+
const devVarsPath = path.join(targetDir, '.dev.vars')
|
|
454
|
+
if (!fs.existsSync(devVarsPath)) {
|
|
455
|
+
const secret = randomBytes(32).toString('hex')
|
|
456
|
+
await fs.writeFile(devVarsPath, `BETTER_AUTH_SECRET="${secret}"\n`)
|
|
457
|
+
}
|
|
447
458
|
}
|
|
448
459
|
|
|
449
460
|
async function createAdminSeedScript(targetDir, { email, password }) {
|
|
450
|
-
const seedScriptContent = `import {
|
|
451
|
-
import { eq } from 'drizzle-orm'
|
|
452
|
-
import * as crypto from 'crypto'
|
|
461
|
+
const seedScriptContent = `import { bootstrapDocumentTypes, RbacService } from '@sonicjs-cms/core'
|
|
453
462
|
import { getPlatformProxy } from 'wrangler'
|
|
454
463
|
|
|
455
464
|
/**
|
|
456
465
|
* Seed script to create initial admin user
|
|
457
466
|
*
|
|
458
|
-
* Run this script after migrations:
|
|
459
|
-
* npm run db:migrate:local
|
|
460
|
-
* npm run seed
|
|
461
|
-
*
|
|
462
467
|
* Admin credentials:
|
|
463
468
|
* Email: ${email}
|
|
464
469
|
* Password: [as entered during setup]
|
|
465
470
|
*/
|
|
466
471
|
|
|
472
|
+
async function hashPassword(password) {
|
|
473
|
+
const iterations = 100000
|
|
474
|
+
const salt = new Uint8Array(16)
|
|
475
|
+
crypto.getRandomValues(salt)
|
|
476
|
+
const encoder = new TextEncoder()
|
|
477
|
+
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits'])
|
|
478
|
+
const hashBuffer = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' }, keyMaterial, 256)
|
|
479
|
+
const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
480
|
+
const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
481
|
+
return \`pbkdf2:\${iterations}:\${saltHex}:\${hashHex}\`
|
|
482
|
+
}
|
|
483
|
+
|
|
467
484
|
async function seed() {
|
|
468
|
-
// Get D1 database from Cloudflare environment using wrangler's getPlatformProxy
|
|
469
485
|
const { env, dispose } = await getPlatformProxy()
|
|
470
486
|
|
|
471
487
|
if (!env?.DB) {
|
|
472
|
-
console.error('❌ Error: DB binding not found')
|
|
473
|
-
console.error('')
|
|
474
|
-
console.error('Make sure you have:')
|
|
475
|
-
console.error('1. Created your D1 database: wrangler d1 create <database-name>')
|
|
476
|
-
console.error('2. Updated wrangler.toml with the database_id')
|
|
477
|
-
console.error('3. Run migrations: npm run db:migrate:local')
|
|
478
|
-
console.error('')
|
|
488
|
+
console.error('❌ Error: DB binding not found. Run migrations first: npm run db:migrate:local')
|
|
479
489
|
process.exit(1)
|
|
480
490
|
}
|
|
481
491
|
|
|
482
|
-
const db = createDb(env.DB)
|
|
483
|
-
|
|
484
492
|
try {
|
|
485
493
|
// Check if admin user already exists
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
.from(users)
|
|
489
|
-
.where(eq(users.email, '${email}'))
|
|
490
|
-
.get()
|
|
491
|
-
|
|
492
|
-
if (existingUser) {
|
|
494
|
+
const existing = await env.DB.prepare('SELECT id FROM auth_user WHERE email = ?').bind('${email}').first()
|
|
495
|
+
if (existing) {
|
|
493
496
|
console.log('✓ Admin user already exists')
|
|
494
|
-
|
|
495
|
-
console.log(\` Role: \${existingUser.role}\`)
|
|
497
|
+
await dispose()
|
|
496
498
|
return
|
|
497
499
|
}
|
|
498
500
|
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
isActive: true,
|
|
517
|
-
createdAt: now,
|
|
518
|
-
updatedAt: now
|
|
519
|
-
})
|
|
520
|
-
.run()
|
|
501
|
+
const passwordHash = await hashPassword('${password}')
|
|
502
|
+
const nowMs = Date.now()
|
|
503
|
+
const odid = \`admin-\${nowMs}-\${Math.random().toString(36).substr(2, 9)}\`
|
|
504
|
+
|
|
505
|
+
await env.DB.batch([
|
|
506
|
+
env.DB.prepare(
|
|
507
|
+
'INSERT INTO auth_user (id, email, first_name, last_name, role, is_active, created_at, updated_at, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
508
|
+
).bind(odid, '${email}', 'Admin', 'User', 'admin', 1, nowMs, nowMs, 'Admin User'),
|
|
509
|
+
env.DB.prepare(
|
|
510
|
+
'INSERT INTO auth_account (id, user_id, account_id, provider_id, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
511
|
+
).bind(crypto.randomUUID(), odid, odid, 'credential', passwordHash, nowMs, nowMs),
|
|
512
|
+
])
|
|
513
|
+
|
|
514
|
+
await bootstrapDocumentTypes(env.DB)
|
|
515
|
+
const rbac = new RbacService(env.DB)
|
|
516
|
+
await rbac.ensureSystemRbacSeed()
|
|
517
|
+
await rbac.addUserRoleByName(odid, 'admin')
|
|
521
518
|
|
|
522
519
|
console.log('✓ Admin user created successfully')
|
|
523
520
|
console.log(\` Email: ${email}\`)
|
|
524
521
|
console.log(\` Role: admin\`)
|
|
525
|
-
console.log('')
|
|
526
|
-
console.log('You can now login at: http://localhost:8787/auth/login')
|
|
527
522
|
} catch (error) {
|
|
528
523
|
console.error('❌ Error creating admin user:', error)
|
|
529
524
|
await dispose()
|
|
530
525
|
process.exit(1)
|
|
531
526
|
}
|
|
532
527
|
|
|
533
|
-
// Clean up the platform proxy
|
|
534
528
|
await dispose()
|
|
535
529
|
}
|
|
536
530
|
|
|
537
|
-
// Run seed
|
|
538
531
|
seed()
|
|
539
532
|
.then(() => {
|
|
540
|
-
console.log('')
|
|
541
533
|
console.log('✓ Seeding complete')
|
|
542
534
|
process.exit(0)
|
|
543
535
|
})
|
|
@@ -796,15 +788,9 @@ function printSuccessMessage(answers) {
|
|
|
796
788
|
console.log(kleur.bold('Visit:'))
|
|
797
789
|
console.log(kleur.cyan(' http://localhost:8787/admin'))
|
|
798
790
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (!databaseIdSet) {
|
|
803
|
-
console.log(kleur.cyan(` wrangler d1 create ${answers.databaseName}`))
|
|
804
|
-
console.log(kleur.dim(' # Copy database_id to wrangler.toml'))
|
|
805
|
-
}
|
|
806
|
-
console.log(kleur.cyan(` wrangler r2 bucket create ${answers.bucketName}`))
|
|
807
|
-
}
|
|
791
|
+
console.log()
|
|
792
|
+
console.log(kleur.bold('Deploy to Cloudflare (when ready):'))
|
|
793
|
+
console.log(kleur.cyan(' npm run deploy'))
|
|
808
794
|
|
|
809
795
|
console.log()
|
|
810
796
|
console.log(kleur.dim('Need help? Visit https://sonicjs.com'))
|