create-sks-admin 0.2.0 → 1.0.1
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/bin/cli.js +774 -140
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -4,10 +4,21 @@ import { execSync } from 'child_process'
|
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import { fileURLToPath } from 'url'
|
|
7
|
+
import { createHash } from 'crypto'
|
|
7
8
|
import prompts from 'prompts'
|
|
8
9
|
|
|
9
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
11
|
|
|
12
|
+
// License key validation (SHA256 hashed - original key not visible)
|
|
13
|
+
const VALID_KEY_HASHES = [
|
|
14
|
+
'c34e63049262e049b26ea19f39b4a9635e6bab0fa7754e7dbf1b66d6b53de680', // Primary key
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function validateLicenseKey(key) {
|
|
18
|
+
const hash = createHash('sha256').update(key.trim()).digest('hex')
|
|
19
|
+
return VALID_KEY_HASHES.includes(hash)
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
// Colors for terminal
|
|
12
23
|
const colors = {
|
|
13
24
|
green: (t) => `\x1b[32m${t}\x1b[0m`,
|
|
@@ -15,6 +26,7 @@ const colors = {
|
|
|
15
26
|
yellow: (t) => `\x1b[33m${t}\x1b[0m`,
|
|
16
27
|
red: (t) => `\x1b[31m${t}\x1b[0m`,
|
|
17
28
|
cyan: (t) => `\x1b[36m${t}\x1b[0m`,
|
|
29
|
+
magenta: (t) => `\x1b[35m${t}\x1b[0m`,
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
const log = {
|
|
@@ -25,13 +37,24 @@ const log = {
|
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
// Get project name from args
|
|
28
|
-
const projectName = process.argv[2]
|
|
40
|
+
const projectName = process.argv[2]
|
|
41
|
+
|
|
42
|
+
if (!projectName) {
|
|
43
|
+
console.log(`
|
|
44
|
+
${colors.cyan('Usage:')} npx create-sks-admin ${colors.yellow('<project-name>')}
|
|
45
|
+
|
|
46
|
+
${colors.cyan('Examples:')}
|
|
47
|
+
npx create-sks-admin my-admin-app
|
|
48
|
+
npx create-sks-admin . ${colors.blue('(current directory)')}
|
|
49
|
+
`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
29
52
|
|
|
30
53
|
console.log(`
|
|
31
|
-
|
|
32
|
-
║ ${colors.
|
|
33
|
-
║
|
|
34
|
-
|
|
54
|
+
${colors.cyan('╔═══════════════════════════════════════════════════╗')}
|
|
55
|
+
${colors.cyan('║')} ${colors.magenta('SKS Admin Panel')} - Production Ready ${colors.cyan('║')}
|
|
56
|
+
${colors.cyan('║')} ${colors.blue('Next.js 16 + Prisma + NextAuth + Shadcn')} ${colors.cyan('║')}
|
|
57
|
+
${colors.cyan('╚═══════════════════════════════════════════════════╝')}
|
|
35
58
|
`)
|
|
36
59
|
|
|
37
60
|
const projectPath = path.resolve(process.cwd(), projectName)
|
|
@@ -43,124 +66,470 @@ if (!isCurrentDir && fs.existsSync(projectPath)) {
|
|
|
43
66
|
process.exit(1)
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
// For current directory, check if it's empty
|
|
69
|
+
// For current directory, check if it's empty
|
|
47
70
|
if (isCurrentDir) {
|
|
48
71
|
const files = fs.readdirSync(projectPath).filter(f => !f.startsWith('.'))
|
|
49
72
|
if (files.length > 0) {
|
|
50
|
-
log.error('Current directory is not empty!
|
|
73
|
+
log.error('Current directory is not empty!')
|
|
51
74
|
process.exit(1)
|
|
52
75
|
}
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
// Ask for
|
|
56
|
-
console.log('')
|
|
57
|
-
console.log(colors.yellow('
|
|
58
|
-
console.log(colors.yellow('
|
|
59
|
-
console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
78
|
+
// Ask for license key
|
|
79
|
+
console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
80
|
+
console.log(colors.yellow(' License Verification'))
|
|
81
|
+
console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
60
82
|
console.log('')
|
|
61
83
|
|
|
62
|
-
const
|
|
84
|
+
const keyResponse = await prompts({
|
|
63
85
|
type: 'password',
|
|
64
|
-
name: '
|
|
65
|
-
message: 'Enter
|
|
86
|
+
name: 'licenseKey',
|
|
87
|
+
message: 'Enter license key (or press Enter for basic version):',
|
|
66
88
|
})
|
|
67
89
|
|
|
68
|
-
const
|
|
69
|
-
const
|
|
90
|
+
const licenseKey = keyResponse.licenseKey?.trim() || ''
|
|
91
|
+
const isPremium = licenseKey.length > 0 && validateLicenseKey(licenseKey)
|
|
70
92
|
|
|
71
|
-
if (
|
|
72
|
-
log.
|
|
73
|
-
|
|
74
|
-
log
|
|
93
|
+
if (licenseKey.length > 0 && !isPremium) {
|
|
94
|
+
log.error('Invalid license key!')
|
|
95
|
+
console.log(colors.yellow('Continuing with basic version...'))
|
|
96
|
+
console.log('')
|
|
97
|
+
} else if (isPremium) {
|
|
98
|
+
log.success('License verified! Premium features unlocked.')
|
|
99
|
+
console.log('')
|
|
75
100
|
}
|
|
76
101
|
|
|
77
|
-
|
|
102
|
+
// Database configuration (only for premium)
|
|
103
|
+
let dbConfig = null
|
|
104
|
+
if (isPremium) {
|
|
105
|
+
console.log(colors.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
106
|
+
console.log(colors.cyan(' Database Configuration'))
|
|
107
|
+
console.log(colors.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
108
|
+
console.log('')
|
|
109
|
+
|
|
110
|
+
dbConfig = await prompts([
|
|
111
|
+
{
|
|
112
|
+
type: 'select',
|
|
113
|
+
name: 'provider',
|
|
114
|
+
message: 'Database provider:',
|
|
115
|
+
choices: [
|
|
116
|
+
{ title: 'PostgreSQL (Recommended)', value: 'postgresql' },
|
|
117
|
+
{ title: 'MySQL', value: 'mysql' },
|
|
118
|
+
{ title: 'SQLite (Local dev)', value: 'sqlite' },
|
|
119
|
+
],
|
|
120
|
+
initial: 0,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: (prev) => prev !== 'sqlite' ? 'text' : null,
|
|
124
|
+
name: 'host',
|
|
125
|
+
message: 'Database host:',
|
|
126
|
+
initial: 'localhost',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: (prev, values) => values.provider !== 'sqlite' ? 'number' : null,
|
|
130
|
+
name: 'port',
|
|
131
|
+
message: 'Database port:',
|
|
132
|
+
initial: (prev, values) => values.provider === 'mysql' ? 3306 : 5432,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: (prev, values) => values.provider !== 'sqlite' ? 'text' : null,
|
|
136
|
+
name: 'database',
|
|
137
|
+
message: 'Database name:',
|
|
138
|
+
initial: 'admin_db',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: (prev, values) => values.provider !== 'sqlite' ? 'text' : null,
|
|
142
|
+
name: 'username',
|
|
143
|
+
message: 'Database username:',
|
|
144
|
+
initial: 'postgres',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: (prev, values) => values.provider !== 'sqlite' ? 'password' : null,
|
|
148
|
+
name: 'password',
|
|
149
|
+
message: 'Database password:',
|
|
150
|
+
},
|
|
151
|
+
])
|
|
152
|
+
console.log('')
|
|
153
|
+
}
|
|
78
154
|
|
|
79
155
|
try {
|
|
80
156
|
// Step 1: Create Next.js app
|
|
81
|
-
log.info('Creating Next.js
|
|
157
|
+
log.info('Creating Next.js application...')
|
|
82
158
|
execSync(
|
|
83
159
|
`npx create-next-app@latest ${projectName} --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*" --turbopack`,
|
|
84
160
|
{ stdio: 'inherit' }
|
|
85
161
|
)
|
|
86
162
|
log.success('Next.js app created')
|
|
87
163
|
|
|
88
|
-
//
|
|
164
|
+
// Navigate to project
|
|
89
165
|
process.chdir(projectPath)
|
|
90
166
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'
|
|
96
|
-
|
|
97
|
-
//npm.pkg.github.com/:_authToken=${authToken}
|
|
98
|
-
`
|
|
167
|
+
if (isPremium) {
|
|
168
|
+
// Step 2: Install dependencies
|
|
169
|
+
log.info('Installing premium dependencies...')
|
|
170
|
+
execSync(
|
|
171
|
+
'npm install prisma @prisma/client next-auth@beta @auth/prisma-adapter bcryptjs zod react-hook-form @hookform/resolvers sonner',
|
|
172
|
+
{ stdio: 'inherit' }
|
|
99
173
|
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'
|
|
103
|
-
`@sks:registry=https://npm.pkg.github.com
|
|
104
|
-
# Add your token here to install premium packages:
|
|
105
|
-
# //npm.pkg.github.com/:_authToken=YOUR_TOKEN
|
|
106
|
-
`
|
|
174
|
+
execSync(
|
|
175
|
+
'npm install -D @types/bcryptjs',
|
|
176
|
+
{ stdio: 'inherit' }
|
|
107
177
|
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
log.success('Dependencies installed')
|
|
179
|
+
|
|
180
|
+
// Step 3: Install Shadcn components
|
|
181
|
+
log.info('Installing Shadcn UI components...')
|
|
182
|
+
execSync('npx shadcn@latest init -d', { stdio: 'inherit' })
|
|
183
|
+
execSync(
|
|
184
|
+
'npx shadcn@latest add button input label card form sonner dialog dropdown-menu avatar badge table tabs separator alert alert-dialog checkbox select textarea switch skeleton sheet sidebar tooltip',
|
|
185
|
+
{ stdio: 'inherit' }
|
|
186
|
+
)
|
|
187
|
+
log.success('Shadcn components installed')
|
|
188
|
+
|
|
189
|
+
// Step 4: Initialize Prisma
|
|
190
|
+
log.info('Setting up Prisma...')
|
|
191
|
+
execSync(`npx prisma init --datasource-provider ${dbConfig.provider}`, { stdio: 'inherit' })
|
|
192
|
+
log.success('Prisma initialized')
|
|
193
|
+
|
|
194
|
+
// Step 5: Create .env file
|
|
195
|
+
log.info('Creating environment files...')
|
|
196
|
+
let databaseUrl = ''
|
|
197
|
+
if (dbConfig.provider === 'sqlite') {
|
|
198
|
+
databaseUrl = 'file:./dev.db'
|
|
199
|
+
} else if (dbConfig.provider === 'postgresql') {
|
|
200
|
+
databaseUrl = `postgresql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}?schema=public`
|
|
201
|
+
} else if (dbConfig.provider === 'mysql') {
|
|
202
|
+
databaseUrl = `mysql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`
|
|
122
203
|
}
|
|
123
|
-
|
|
124
|
-
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync('.env', `# Database
|
|
206
|
+
DATABASE_URL="${databaseUrl}"
|
|
207
|
+
|
|
208
|
+
# NextAuth
|
|
209
|
+
AUTH_SECRET="${createHash('sha256').update(Date.now().toString()).digest('hex').slice(0, 32)}"
|
|
210
|
+
AUTH_TRUST_HOST=true
|
|
211
|
+
|
|
212
|
+
# App
|
|
213
|
+
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
214
|
+
`)
|
|
215
|
+
|
|
216
|
+
fs.writeFileSync('.env.example', `# Database
|
|
217
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/admin_db?schema=public"
|
|
218
|
+
|
|
219
|
+
# NextAuth
|
|
220
|
+
AUTH_SECRET="your-secret-key-here"
|
|
221
|
+
AUTH_TRUST_HOST=true
|
|
222
|
+
|
|
223
|
+
# App
|
|
224
|
+
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
225
|
+
`)
|
|
226
|
+
log.success('Environment files created')
|
|
227
|
+
|
|
228
|
+
// Step 6: Create Prisma schema
|
|
229
|
+
log.info('Creating database schema...')
|
|
230
|
+
fs.writeFileSync('prisma/schema.prisma', `generator client {
|
|
231
|
+
provider = "prisma-client-js"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
datasource db {
|
|
235
|
+
provider = "${dbConfig.provider}"
|
|
236
|
+
url = env("DATABASE_URL")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
enum Role {
|
|
240
|
+
USER
|
|
241
|
+
ADMIN
|
|
242
|
+
SUPER_ADMIN
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
model User {
|
|
246
|
+
id String @id @default(cuid())
|
|
247
|
+
name String?
|
|
248
|
+
email String @unique
|
|
249
|
+
emailVerified DateTime?
|
|
250
|
+
image String?
|
|
251
|
+
password String?
|
|
252
|
+
role Role @default(USER)
|
|
253
|
+
createdAt DateTime @default(now())
|
|
254
|
+
updatedAt DateTime @updatedAt
|
|
255
|
+
accounts Account[]
|
|
256
|
+
sessions Session[]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
model Account {
|
|
260
|
+
userId String
|
|
261
|
+
type String
|
|
262
|
+
provider String
|
|
263
|
+
providerAccountId String
|
|
264
|
+
refresh_token String?
|
|
265
|
+
access_token String?
|
|
266
|
+
expires_at Int?
|
|
267
|
+
token_type String?
|
|
268
|
+
scope String?
|
|
269
|
+
id_token String?
|
|
270
|
+
session_state String?
|
|
271
|
+
createdAt DateTime @default(now())
|
|
272
|
+
updatedAt DateTime @updatedAt
|
|
273
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
274
|
+
|
|
275
|
+
@@id([provider, providerAccountId])
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
model Session {
|
|
279
|
+
sessionToken String @unique
|
|
280
|
+
userId String
|
|
281
|
+
expires DateTime
|
|
282
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
283
|
+
createdAt DateTime @default(now())
|
|
284
|
+
updatedAt DateTime @updatedAt
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
model VerificationToken {
|
|
288
|
+
identifier String
|
|
289
|
+
token String
|
|
290
|
+
expires DateTime
|
|
291
|
+
|
|
292
|
+
@@id([identifier, token])
|
|
293
|
+
}
|
|
294
|
+
`)
|
|
295
|
+
log.success('Database schema created')
|
|
296
|
+
|
|
297
|
+
// Step 7: Create lib files
|
|
298
|
+
log.info('Creating library files...')
|
|
299
|
+
fs.mkdirSync('lib', { recursive: true })
|
|
300
|
+
|
|
301
|
+
// lib/db.ts
|
|
302
|
+
fs.writeFileSync('lib/db.ts', `import { PrismaClient } from '@prisma/client'
|
|
303
|
+
|
|
304
|
+
const globalForPrisma = globalThis as unknown as {
|
|
305
|
+
prisma: PrismaClient | undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
|
309
|
+
|
|
310
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
311
|
+
|
|
312
|
+
export default prisma
|
|
313
|
+
`)
|
|
314
|
+
|
|
315
|
+
// lib/utils.ts (update existing)
|
|
316
|
+
const utilsContent = fs.readFileSync('lib/utils.ts', 'utf-8')
|
|
317
|
+
fs.writeFileSync('lib/utils.ts', utilsContent + `
|
|
318
|
+
|
|
319
|
+
export function formatDate(date: Date | string) {
|
|
320
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
321
|
+
month: 'short',
|
|
322
|
+
day: 'numeric',
|
|
323
|
+
year: 'numeric',
|
|
324
|
+
}).format(new Date(date))
|
|
325
|
+
}
|
|
326
|
+
`)
|
|
327
|
+
|
|
328
|
+
// lib/dal.ts - Data Access Layer
|
|
329
|
+
fs.writeFileSync('lib/dal.ts', `import { auth } from '@/auth'
|
|
330
|
+
import { redirect } from 'next/navigation'
|
|
331
|
+
import { cache } from 'react'
|
|
332
|
+
|
|
333
|
+
export const verifySession = cache(async () => {
|
|
334
|
+
const session = await auth()
|
|
335
|
+
|
|
336
|
+
if (!session?.user) {
|
|
337
|
+
redirect('/admin/login')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return session.user
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
export const verifyAdminSession = cache(async () => {
|
|
344
|
+
const session = await auth()
|
|
345
|
+
|
|
346
|
+
if (!session?.user) {
|
|
347
|
+
redirect('/admin/login')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const role = session.user.role
|
|
351
|
+
if (role !== 'ADMIN' && role !== 'SUPER_ADMIN') {
|
|
352
|
+
redirect('/unauthorized')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return session.user
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
export const getOptionalSession = cache(async () => {
|
|
359
|
+
const session = await auth()
|
|
360
|
+
return session?.user ?? null
|
|
361
|
+
})
|
|
362
|
+
`)
|
|
363
|
+
|
|
364
|
+
// lib/api/response.ts
|
|
365
|
+
fs.mkdirSync('lib/api', { recursive: true })
|
|
366
|
+
fs.writeFileSync('lib/api/response.ts', `import { NextResponse } from 'next/server'
|
|
367
|
+
|
|
368
|
+
interface ApiSuccessResponse<T> {
|
|
369
|
+
success: true
|
|
370
|
+
data: T
|
|
371
|
+
meta: {
|
|
372
|
+
timestamp: string
|
|
125
373
|
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface ApiErrorResponse {
|
|
377
|
+
success: false
|
|
378
|
+
error: {
|
|
379
|
+
code: string
|
|
380
|
+
message: string
|
|
381
|
+
details?: unknown
|
|
382
|
+
}
|
|
383
|
+
meta: {
|
|
384
|
+
timestamp: string
|
|
385
|
+
}
|
|
386
|
+
}
|
|
126
387
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// import { createDAL } from '@sks/admin-core/auth'
|
|
137
|
-
// import { auth } from '@/auth'
|
|
138
|
-
|
|
139
|
-
// export const {
|
|
140
|
-
// verifySession,
|
|
141
|
-
// verifyAdminSession,
|
|
142
|
-
// redirectIfAuthenticated,
|
|
143
|
-
// } = createDAL({
|
|
144
|
-
// auth,
|
|
145
|
-
// loginPath: '/admin/login',
|
|
146
|
-
// adminRoles: ['ADMIN'],
|
|
147
|
-
// })
|
|
148
|
-
|
|
149
|
-
// Placeholder exports for now
|
|
150
|
-
export const verifySession = async () => ({ userId: '1', email: 'test@test.com', role: 'ADMIN' })
|
|
151
|
-
export const verifyAdminSession = verifySession
|
|
152
|
-
`
|
|
388
|
+
export function apiSuccess<T>(data: T, status = 200): NextResponse<ApiSuccessResponse<T>> {
|
|
389
|
+
return NextResponse.json(
|
|
390
|
+
{
|
|
391
|
+
success: true,
|
|
392
|
+
data,
|
|
393
|
+
meta: { timestamp: new Date().toISOString() },
|
|
394
|
+
},
|
|
395
|
+
{ status }
|
|
153
396
|
)
|
|
397
|
+
}
|
|
154
398
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
399
|
+
export function apiError(
|
|
400
|
+
code: string,
|
|
401
|
+
message: string,
|
|
402
|
+
details?: unknown,
|
|
403
|
+
status = 500
|
|
404
|
+
): NextResponse<ApiErrorResponse> {
|
|
405
|
+
return NextResponse.json(
|
|
406
|
+
{
|
|
407
|
+
success: false,
|
|
408
|
+
error: { code, message, details },
|
|
409
|
+
meta: { timestamp: new Date().toISOString() },
|
|
410
|
+
},
|
|
411
|
+
{ status }
|
|
412
|
+
)
|
|
413
|
+
}
|
|
161
414
|
|
|
162
|
-
|
|
163
|
-
|
|
415
|
+
export const ErrorCodes = {
|
|
416
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
417
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
418
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
419
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
420
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
421
|
+
} as const
|
|
422
|
+
`)
|
|
423
|
+
|
|
424
|
+
// Step 8: Create auth.ts
|
|
425
|
+
log.info('Setting up authentication...')
|
|
426
|
+
fs.writeFileSync('auth.ts', `import NextAuth from 'next-auth'
|
|
427
|
+
import { PrismaAdapter } from '@auth/prisma-adapter'
|
|
428
|
+
import Credentials from 'next-auth/providers/credentials'
|
|
429
|
+
import bcrypt from 'bcryptjs'
|
|
430
|
+
import prisma from '@/lib/db'
|
|
431
|
+
|
|
432
|
+
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
433
|
+
adapter: PrismaAdapter(prisma),
|
|
434
|
+
session: { strategy: 'jwt' },
|
|
435
|
+
pages: {
|
|
436
|
+
signIn: '/admin/login',
|
|
437
|
+
},
|
|
438
|
+
providers: [
|
|
439
|
+
Credentials({
|
|
440
|
+
name: 'credentials',
|
|
441
|
+
credentials: {
|
|
442
|
+
email: { label: 'Email', type: 'email' },
|
|
443
|
+
password: { label: 'Password', type: 'password' },
|
|
444
|
+
},
|
|
445
|
+
async authorize(credentials) {
|
|
446
|
+
if (!credentials?.email || !credentials?.password) {
|
|
447
|
+
return null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const user = await prisma.user.findUnique({
|
|
451
|
+
where: { email: credentials.email as string },
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
if (!user || !user.password) {
|
|
455
|
+
return null
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const passwordMatch = await bcrypt.compare(
|
|
459
|
+
credentials.password as string,
|
|
460
|
+
user.password
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if (!passwordMatch) {
|
|
464
|
+
return null
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
id: user.id,
|
|
469
|
+
email: user.email,
|
|
470
|
+
name: user.name,
|
|
471
|
+
role: user.role,
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
}),
|
|
475
|
+
],
|
|
476
|
+
callbacks: {
|
|
477
|
+
async jwt({ token, user }) {
|
|
478
|
+
if (user) {
|
|
479
|
+
token.role = user.role
|
|
480
|
+
}
|
|
481
|
+
return token
|
|
482
|
+
},
|
|
483
|
+
async session({ session, token }) {
|
|
484
|
+
if (session.user) {
|
|
485
|
+
session.user.id = token.sub!
|
|
486
|
+
session.user.role = token.role as string
|
|
487
|
+
}
|
|
488
|
+
return session
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
`)
|
|
493
|
+
|
|
494
|
+
// types/next-auth.d.ts
|
|
495
|
+
fs.mkdirSync('types', { recursive: true })
|
|
496
|
+
fs.writeFileSync('types/next-auth.d.ts', `import { DefaultSession } from 'next-auth'
|
|
497
|
+
|
|
498
|
+
declare module 'next-auth' {
|
|
499
|
+
interface Session {
|
|
500
|
+
user: {
|
|
501
|
+
id: string
|
|
502
|
+
role: string
|
|
503
|
+
} & DefaultSession['user']
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
interface User {
|
|
507
|
+
role: string
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
declare module 'next-auth/jwt' {
|
|
512
|
+
interface JWT {
|
|
513
|
+
role?: string
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
`)
|
|
517
|
+
|
|
518
|
+
// Step 9: Create API route for auth
|
|
519
|
+
fs.mkdirSync('app/api/auth/[...nextauth]', { recursive: true })
|
|
520
|
+
fs.writeFileSync('app/api/auth/[...nextauth]/route.ts', `import { handlers } from '@/auth'
|
|
521
|
+
|
|
522
|
+
export const { GET, POST } = handlers
|
|
523
|
+
`)
|
|
524
|
+
|
|
525
|
+
// Step 10: Create admin pages
|
|
526
|
+
log.info('Creating admin pages...')
|
|
527
|
+
|
|
528
|
+
// Admin layout
|
|
529
|
+
fs.mkdirSync('app/admin', { recursive: true })
|
|
530
|
+
fs.writeFileSync('app/admin/layout.tsx', `import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
|
531
|
+
import { AppSidebar } from '@/components/app-sidebar'
|
|
532
|
+
import { Toaster } from '@/components/ui/sonner'
|
|
164
533
|
|
|
165
534
|
export default function AdminLayout({
|
|
166
535
|
children,
|
|
@@ -168,76 +537,341 @@ export default function AdminLayout({
|
|
|
168
537
|
children: React.ReactNode
|
|
169
538
|
}) {
|
|
170
539
|
return (
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
</
|
|
180
|
-
</aside>
|
|
181
|
-
<main className="ml-64 p-8">
|
|
182
|
-
{children}
|
|
540
|
+
<SidebarProvider>
|
|
541
|
+
<AppSidebar />
|
|
542
|
+
<main className="flex-1">
|
|
543
|
+
<header className="flex h-14 items-center gap-4 border-b bg-background px-6">
|
|
544
|
+
<SidebarTrigger />
|
|
545
|
+
</header>
|
|
546
|
+
<div className="p-6">
|
|
547
|
+
{children}
|
|
548
|
+
</div>
|
|
183
549
|
</main>
|
|
550
|
+
<Toaster />
|
|
551
|
+
</SidebarProvider>
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
`)
|
|
555
|
+
|
|
556
|
+
// Admin dashboard
|
|
557
|
+
fs.writeFileSync('app/admin/page.tsx', `import { verifyAdminSession } from '@/lib/dal'
|
|
558
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
559
|
+
|
|
560
|
+
export default async function AdminDashboard() {
|
|
561
|
+
const user = await verifyAdminSession()
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<div className="space-y-6">
|
|
565
|
+
<div>
|
|
566
|
+
<h1 className="text-3xl font-bold">Dashboard</h1>
|
|
567
|
+
<p className="text-muted-foreground">Welcome back, {user.name || user.email}</p>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
571
|
+
<Card>
|
|
572
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
573
|
+
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
|
574
|
+
</CardHeader>
|
|
575
|
+
<CardContent>
|
|
576
|
+
<div className="text-2xl font-bold">1,234</div>
|
|
577
|
+
<p className="text-xs text-muted-foreground">+12% from last month</p>
|
|
578
|
+
</CardContent>
|
|
579
|
+
</Card>
|
|
580
|
+
<Card>
|
|
581
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
582
|
+
<CardTitle className="text-sm font-medium">Revenue</CardTitle>
|
|
583
|
+
</CardHeader>
|
|
584
|
+
<CardContent>
|
|
585
|
+
<div className="text-2xl font-bold">$12,345</div>
|
|
586
|
+
<p className="text-xs text-muted-foreground">+8% from last month</p>
|
|
587
|
+
</CardContent>
|
|
588
|
+
</Card>
|
|
589
|
+
<Card>
|
|
590
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
591
|
+
<CardTitle className="text-sm font-medium">Active Sessions</CardTitle>
|
|
592
|
+
</CardHeader>
|
|
593
|
+
<CardContent>
|
|
594
|
+
<div className="text-2xl font-bold">573</div>
|
|
595
|
+
<p className="text-xs text-muted-foreground">+2% from last hour</p>
|
|
596
|
+
</CardContent>
|
|
597
|
+
</Card>
|
|
598
|
+
<Card>
|
|
599
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
600
|
+
<CardTitle className="text-sm font-medium">Conversion Rate</CardTitle>
|
|
601
|
+
</CardHeader>
|
|
602
|
+
<CardContent>
|
|
603
|
+
<div className="text-2xl font-bold">3.2%</div>
|
|
604
|
+
<p className="text-xs text-muted-foreground">+0.5% from last week</p>
|
|
605
|
+
</CardContent>
|
|
606
|
+
</Card>
|
|
607
|
+
</div>
|
|
184
608
|
</div>
|
|
185
609
|
)
|
|
186
610
|
}
|
|
187
|
-
`
|
|
611
|
+
`)
|
|
612
|
+
|
|
613
|
+
// Login page
|
|
614
|
+
fs.mkdirSync('app/admin/login', { recursive: true })
|
|
615
|
+
fs.writeFileSync('app/admin/login/page.tsx', `'use client'
|
|
616
|
+
|
|
617
|
+
import { useState } from 'react'
|
|
618
|
+
import { signIn } from 'next-auth/react'
|
|
619
|
+
import { useRouter } from 'next/navigation'
|
|
620
|
+
import { Button } from '@/components/ui/button'
|
|
621
|
+
import { Input } from '@/components/ui/input'
|
|
622
|
+
import { Label } from '@/components/ui/label'
|
|
623
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
624
|
+
|
|
625
|
+
export default function LoginPage() {
|
|
626
|
+
const [email, setEmail] = useState('')
|
|
627
|
+
const [password, setPassword] = useState('')
|
|
628
|
+
const [error, setError] = useState('')
|
|
629
|
+
const [loading, setLoading] = useState(false)
|
|
630
|
+
const router = useRouter()
|
|
631
|
+
|
|
632
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
633
|
+
e.preventDefault()
|
|
634
|
+
setLoading(true)
|
|
635
|
+
setError('')
|
|
636
|
+
|
|
637
|
+
const result = await signIn('credentials', {
|
|
638
|
+
email,
|
|
639
|
+
password,
|
|
640
|
+
redirect: false,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
if (result?.error) {
|
|
644
|
+
setError('Invalid email or password')
|
|
645
|
+
setLoading(false)
|
|
646
|
+
} else {
|
|
647
|
+
router.push('/admin')
|
|
648
|
+
router.refresh()
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return (
|
|
653
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
|
654
|
+
<Card className="w-full max-w-md">
|
|
655
|
+
<CardHeader className="text-center">
|
|
656
|
+
<CardTitle className="text-2xl">Admin Login</CardTitle>
|
|
657
|
+
<CardDescription>Enter your credentials to continue</CardDescription>
|
|
658
|
+
</CardHeader>
|
|
659
|
+
<CardContent>
|
|
660
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
661
|
+
{error && (
|
|
662
|
+
<div className="p-3 text-sm text-red-500 bg-red-50 rounded-md">
|
|
663
|
+
{error}
|
|
664
|
+
</div>
|
|
665
|
+
)}
|
|
666
|
+
<div className="space-y-2">
|
|
667
|
+
<Label htmlFor="email">Email</Label>
|
|
668
|
+
<Input
|
|
669
|
+
id="email"
|
|
670
|
+
type="email"
|
|
671
|
+
placeholder="admin@example.com"
|
|
672
|
+
value={email}
|
|
673
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
674
|
+
required
|
|
675
|
+
/>
|
|
676
|
+
</div>
|
|
677
|
+
<div className="space-y-2">
|
|
678
|
+
<Label htmlFor="password">Password</Label>
|
|
679
|
+
<Input
|
|
680
|
+
id="password"
|
|
681
|
+
type="password"
|
|
682
|
+
value={password}
|
|
683
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
684
|
+
required
|
|
685
|
+
/>
|
|
686
|
+
</div>
|
|
687
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
688
|
+
{loading ? 'Signing in...' : 'Sign In'}
|
|
689
|
+
</Button>
|
|
690
|
+
</form>
|
|
691
|
+
</CardContent>
|
|
692
|
+
</Card>
|
|
693
|
+
</div>
|
|
188
694
|
)
|
|
695
|
+
}
|
|
696
|
+
`)
|
|
189
697
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'app
|
|
193
|
-
|
|
698
|
+
// Sidebar component
|
|
699
|
+
fs.mkdirSync('components', { recursive: true })
|
|
700
|
+
fs.writeFileSync('components/app-sidebar.tsx', `'use client'
|
|
701
|
+
|
|
702
|
+
import {
|
|
703
|
+
Sidebar,
|
|
704
|
+
SidebarContent,
|
|
705
|
+
SidebarFooter,
|
|
706
|
+
SidebarGroup,
|
|
707
|
+
SidebarGroupContent,
|
|
708
|
+
SidebarGroupLabel,
|
|
709
|
+
SidebarHeader,
|
|
710
|
+
SidebarMenu,
|
|
711
|
+
SidebarMenuButton,
|
|
712
|
+
SidebarMenuItem,
|
|
713
|
+
} from '@/components/ui/sidebar'
|
|
714
|
+
import { Home, Users, Settings, LogOut } from 'lucide-react'
|
|
715
|
+
import { signOut } from 'next-auth/react'
|
|
716
|
+
import Link from 'next/link'
|
|
717
|
+
|
|
718
|
+
const menuItems = [
|
|
719
|
+
{ title: 'Dashboard', icon: Home, href: '/admin' },
|
|
720
|
+
{ title: 'Users', icon: Users, href: '/admin/users' },
|
|
721
|
+
{ title: 'Settings', icon: Settings, href: '/admin/settings' },
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
export function AppSidebar() {
|
|
194
725
|
return (
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
726
|
+
<Sidebar>
|
|
727
|
+
<SidebarHeader className="border-b px-6 py-4">
|
|
728
|
+
<h1 className="text-xl font-bold">Admin Panel</h1>
|
|
729
|
+
</SidebarHeader>
|
|
730
|
+
<SidebarContent>
|
|
731
|
+
<SidebarGroup>
|
|
732
|
+
<SidebarGroupLabel>Menu</SidebarGroupLabel>
|
|
733
|
+
<SidebarGroupContent>
|
|
734
|
+
<SidebarMenu>
|
|
735
|
+
{menuItems.map((item) => (
|
|
736
|
+
<SidebarMenuItem key={item.title}>
|
|
737
|
+
<SidebarMenuButton asChild>
|
|
738
|
+
<Link href={item.href}>
|
|
739
|
+
<item.icon className="h-4 w-4" />
|
|
740
|
+
<span>{item.title}</span>
|
|
741
|
+
</Link>
|
|
742
|
+
</SidebarMenuButton>
|
|
743
|
+
</SidebarMenuItem>
|
|
744
|
+
))}
|
|
745
|
+
</SidebarMenu>
|
|
746
|
+
</SidebarGroupContent>
|
|
747
|
+
</SidebarGroup>
|
|
748
|
+
</SidebarContent>
|
|
749
|
+
<SidebarFooter className="border-t p-4">
|
|
750
|
+
<SidebarMenu>
|
|
751
|
+
<SidebarMenuItem>
|
|
752
|
+
<SidebarMenuButton onClick={() => signOut({ callbackUrl: '/admin/login' })}>
|
|
753
|
+
<LogOut className="h-4 w-4" />
|
|
754
|
+
<span>Logout</span>
|
|
755
|
+
</SidebarMenuButton>
|
|
756
|
+
</SidebarMenuItem>
|
|
757
|
+
</SidebarMenu>
|
|
758
|
+
</SidebarFooter>
|
|
759
|
+
</Sidebar>
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
`)
|
|
763
|
+
|
|
764
|
+
// Install lucide-react
|
|
765
|
+
execSync('npm install lucide-react', { stdio: 'inherit' })
|
|
766
|
+
|
|
767
|
+
// Create seed script
|
|
768
|
+
fs.mkdirSync('prisma', { recursive: true })
|
|
769
|
+
fs.writeFileSync('prisma/seed.ts', `import { PrismaClient } from '@prisma/client'
|
|
770
|
+
import bcrypt from 'bcryptjs'
|
|
771
|
+
|
|
772
|
+
const prisma = new PrismaClient()
|
|
773
|
+
|
|
774
|
+
async function main() {
|
|
775
|
+
const hashedPassword = await bcrypt.hash('admin123', 10)
|
|
776
|
+
|
|
777
|
+
const admin = await prisma.user.upsert({
|
|
778
|
+
where: { email: 'admin@example.com' },
|
|
779
|
+
update: {},
|
|
780
|
+
create: {
|
|
781
|
+
email: 'admin@example.com',
|
|
782
|
+
name: 'Admin User',
|
|
783
|
+
password: hashedPassword,
|
|
784
|
+
role: 'ADMIN',
|
|
785
|
+
},
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
console.log('Created admin user:', admin.email)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
main()
|
|
792
|
+
.catch((e) => {
|
|
793
|
+
console.error(e)
|
|
794
|
+
process.exit(1)
|
|
795
|
+
})
|
|
796
|
+
.finally(async () => {
|
|
797
|
+
await prisma.$disconnect()
|
|
798
|
+
})
|
|
799
|
+
`)
|
|
800
|
+
|
|
801
|
+
// Update package.json with prisma seed
|
|
802
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
|
803
|
+
packageJson.prisma = { seed: 'npx tsx prisma/seed.ts' }
|
|
804
|
+
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2))
|
|
805
|
+
|
|
806
|
+
execSync('npm install -D tsx', { stdio: 'inherit' })
|
|
807
|
+
|
|
808
|
+
log.success('Admin pages created')
|
|
809
|
+
} else {
|
|
810
|
+
// Basic version - just create simple admin page
|
|
811
|
+
log.info('Creating basic admin template...')
|
|
812
|
+
fs.mkdirSync('app/admin', { recursive: true })
|
|
813
|
+
fs.writeFileSync('app/admin/page.tsx', `export default function AdminPage() {
|
|
814
|
+
return (
|
|
815
|
+
<div className="min-h-screen bg-gray-100 p-8">
|
|
816
|
+
<div className="max-w-4xl mx-auto">
|
|
817
|
+
<h1 className="text-3xl font-bold mb-4">Admin Panel</h1>
|
|
818
|
+
<p className="text-gray-600 mb-8">Basic version - upgrade to premium for full features.</p>
|
|
819
|
+
|
|
820
|
+
<div className="grid grid-cols-3 gap-4">
|
|
821
|
+
<div className="bg-white p-6 rounded-lg shadow">
|
|
822
|
+
<h2 className="text-gray-500 text-sm">Total Users</h2>
|
|
823
|
+
<p className="text-3xl font-bold">--</p>
|
|
824
|
+
</div>
|
|
825
|
+
<div className="bg-white p-6 rounded-lg shadow">
|
|
826
|
+
<h2 className="text-gray-500 text-sm">Revenue</h2>
|
|
827
|
+
<p className="text-3xl font-bold">--</p>
|
|
828
|
+
</div>
|
|
829
|
+
<div className="bg-white p-6 rounded-lg shadow">
|
|
830
|
+
<h2 className="text-gray-500 text-sm">Orders</h2>
|
|
831
|
+
<p className="text-3xl font-bold">--</p>
|
|
832
|
+
</div>
|
|
205
833
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<p className="text-
|
|
834
|
+
|
|
835
|
+
<div className="mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
836
|
+
<p className="text-yellow-800">
|
|
837
|
+
<strong>Upgrade to Premium</strong> to unlock: Prisma ORM, NextAuth, Shadcn UI,
|
|
838
|
+
Admin Dashboard, User Management, and more!
|
|
839
|
+
</p>
|
|
209
840
|
</div>
|
|
210
841
|
</div>
|
|
211
842
|
</div>
|
|
212
843
|
)
|
|
213
844
|
}
|
|
214
|
-
`
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
log.success('Boilerplate files created')
|
|
845
|
+
`)
|
|
846
|
+
log.success('Basic template created')
|
|
847
|
+
}
|
|
218
848
|
|
|
219
|
-
//
|
|
849
|
+
// Final message
|
|
220
850
|
console.log('')
|
|
221
|
-
console.log(colors.green('
|
|
851
|
+
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
222
852
|
console.log(colors.green(' ✓ Project created successfully!'))
|
|
223
|
-
console.log(colors.green('
|
|
224
|
-
console.log('')
|
|
225
|
-
|
|
226
|
-
console.log(colors.blue('Next steps:'))
|
|
227
|
-
console.log(` cd ${isCurrentDir ? '.' : projectName}`)
|
|
228
|
-
console.log(' npm run dev')
|
|
229
|
-
console.log('')
|
|
230
|
-
console.log(colors.blue('Admin panel:'))
|
|
231
|
-
console.log(' http://localhost:3000/admin')
|
|
853
|
+
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
232
854
|
console.log('')
|
|
233
855
|
|
|
234
|
-
if (
|
|
235
|
-
console.log(colors.
|
|
856
|
+
if (isPremium) {
|
|
857
|
+
console.log(colors.cyan('Next steps:'))
|
|
858
|
+
console.log(` 1. cd ${isCurrentDir ? '.' : projectName}`)
|
|
859
|
+
console.log(' 2. npx prisma db push # Create database tables')
|
|
860
|
+
console.log(' 3. npx prisma db seed # Create admin user')
|
|
861
|
+
console.log(' 4. npm run dev # Start development server')
|
|
862
|
+
console.log('')
|
|
863
|
+
console.log(colors.cyan('Default admin credentials:'))
|
|
864
|
+
console.log(' Email: admin@example.com')
|
|
865
|
+
console.log(' Password: admin123')
|
|
866
|
+
console.log('')
|
|
867
|
+
console.log(colors.cyan('Admin panel:'))
|
|
868
|
+
console.log(' http://localhost:3000/admin')
|
|
236
869
|
} else {
|
|
237
|
-
console.log(colors.
|
|
238
|
-
console.log(
|
|
239
|
-
console.log('
|
|
240
|
-
console.log('
|
|
870
|
+
console.log(colors.blue('Next steps:'))
|
|
871
|
+
console.log(` cd ${isCurrentDir ? '.' : projectName}`)
|
|
872
|
+
console.log(' npm run dev')
|
|
873
|
+
console.log('')
|
|
874
|
+
console.log(colors.yellow('Get premium license for full features!'))
|
|
241
875
|
}
|
|
242
876
|
console.log('')
|
|
243
877
|
|