@lucifer91299/create-portal-app 1.1.26 → 1.1.28

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 (2) hide show
  1. package/dist/cli/index.js +526 -12
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -232,16 +232,57 @@ export function Providers({ children }: { children: ReactNode }) {
232
232
  }
233
233
  function genLoginPage(o) {
234
234
  const isAnimated = o.loginStyle !== "simple";
235
- const component = isAnimated ? "LoginPage" : "LoginPageSimple";
236
- const credField = isAnimated ? "identifier" : "email";
237
- const poweredByProp = isAnimated ? ` poweredBy={{
238
- logoSrc: "/brand/powered-by-logo.svg",
239
- text: "Powered by",
240
- href: "#",
241
- }}` : "";
235
+ if (!isAnimated) {
236
+ return `'use client'
237
+
238
+ import { LoginPageSimple } from '@lucifer91299/ui'
239
+ import React, { useState } from 'react'
240
+ import { useRouter } from 'next/navigation'
241
+
242
+ export default function Login() {
243
+ const [isLoading, setIsLoading] = useState(false)
244
+ const [error, setError] = useState<string | null>(null)
245
+ const router = useRouter()
246
+
247
+ const handleSubmit = async (creds: { email: string; password: string }) => {
248
+ setError(null)
249
+ setIsLoading(true)
250
+ try {
251
+ const res = await fetch('/api/auth/login', {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ email: creds.email, password: creds.password }),
255
+ credentials: 'include',
256
+ })
257
+ const data = await res.json()
258
+ if (!res.ok) { setError(data.error ?? 'Invalid credentials'); return }
259
+ router.replace('/dashboard')
260
+ } catch {
261
+ setError('Login failed. Please try again.')
262
+ } finally {
263
+ setIsLoading(false)
264
+ }
265
+ }
266
+
267
+ return (
268
+ <LoginPageSimple
269
+ projectName="${o.projectName}"
270
+ projectSubtitle="Sign in to your account"
271
+ logoSrc="/brand/logo.svg"
272
+ onSubmit={handleSubmit}
273
+ isLoading={isLoading}
274
+ error={error}
275
+ registerLinks={[
276
+ { label: 'Create account', href: '/register' },
277
+ ]}
278
+ />
279
+ )
280
+ }
281
+ `;
282
+ }
242
283
  return `'use client'
243
284
 
244
- import { ${component} } from '@lucifer91299/ui'
285
+ import { LoginPage } from '@lucifer91299/ui'
245
286
  import React, { useState } from 'react'
246
287
  import { useRouter } from 'next/navigation'
247
288
 
@@ -250,14 +291,15 @@ export default function Login() {
250
291
  const [error, setError] = useState<string | null>(null)
251
292
  const router = useRouter()
252
293
 
253
- const handleSubmit = async (creds: { ${credField}: string; password: string }) => {
294
+ // \u2500\u2500 Submit handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
295
+ const handleSubmit = async (creds: { identifier: string; password: string }) => {
254
296
  setError(null)
255
297
  setIsLoading(true)
256
298
  try {
257
299
  const res = await fetch('/api/auth/login', {
258
300
  method: 'POST',
259
301
  headers: { 'Content-Type': 'application/json' },
260
- body: JSON.stringify({ email: creds.${credField}, password: creds.password }),
302
+ body: JSON.stringify({ email: creds.identifier, password: creds.password }),
261
303
  credentials: 'include',
262
304
  })
263
305
  const data = await res.json()
@@ -271,19 +313,485 @@ export default function Login() {
271
313
  }
272
314
 
273
315
  return (
274
- <${component}
316
+ <LoginPage
317
+ // \u2500\u2500 Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
275
318
  projectName="${o.projectName}"
276
319
  projectSubtitle="Sign in to continue"
277
320
  logoSrc="/brand/logo.svg"
321
+ logoAlt="${o.projectName} logo"
322
+ logoSize={56}
323
+
324
+ // \u2500\u2500 Auth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
325
+ onSubmit={handleSubmit}
326
+ isLoading={isLoading}
327
+ error={error}
328
+
329
+ // \u2500\u2500 Field labels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
330
+ identifierLabel="Email address"
331
+ identifierType="email"
332
+ identifierPlaceholder="you@example.com"
333
+ passwordPlaceholder="Enter your password"
334
+
335
+ // \u2500\u2500 Forgot password \u2014 links to /forgot-password \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
336
+ onForgotPassword={() => router.push('/forgot-password')}
337
+ forgotPasswordLabel="Forgot Password?"
338
+
339
+ // \u2500\u2500 Registration links (set isOpen: false to lock a link) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
340
+ registrationLinks={[
341
+ { label: 'Create account', href: '/register', isOpen: true },
342
+ ]}
343
+
344
+ // \u2500\u2500 Social connect links \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
345
+ // socialLinks={[
346
+ // { label: 'WhatsApp', href: '#', brand: '#25D366', icon: <WhatsAppIcon /> },
347
+ // { label: 'Facebook', href: '#', brand: '#1877F2', icon: <FacebookIcon /> },
348
+ // ]}
349
+ // socialLinksLabel="Connect with us"
350
+
351
+ // \u2500\u2500 Powered-by badge (bottom-right corner) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
352
+ poweredBy={{
353
+ logoSrc: "/brand/powered-by-logo.svg",
354
+ text: "Powered by",
355
+ href: "#",
356
+ }}
357
+
358
+ // \u2500\u2500 Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
359
+ // backgroundGradient="linear-gradient(150deg, #f7f6f3 0%, #f1efe9 55%, #f5f2ed 100%)"
360
+ // ambientColors={[[255,153,51],[19,136,8],[0,0,128]]}
361
+ />
362
+ )
363
+ }
364
+ `;
365
+ }
366
+ function genForgotPasswordPage(o) {
367
+ return `'use client'
368
+
369
+ import { ForgotPasswordPage } from '@lucifer91299/ui'
370
+ import React, { useState } from 'react'
371
+ import { useRouter } from 'next/navigation'
372
+
373
+ export default function ForgotPassword() {
374
+ const [isLoading, setIsLoading] = useState(false)
375
+ const [error, setError] = useState<string | null>(null)
376
+ const [isSuccess, setIsSuccess] = useState(false)
377
+ const router = useRouter()
378
+
379
+ const handleSubmit = async (email: string) => {
380
+ setError(null)
381
+ setIsLoading(true)
382
+ try {
383
+ const res = await fetch('/api/auth/forgot-password', {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify({ email }),
387
+ })
388
+ const data = await res.json()
389
+ if (!res.ok) { setError(data.error ?? 'Could not send reset link'); return }
390
+ setIsSuccess(true)
391
+ } catch {
392
+ setError('Something went wrong. Please try again.')
393
+ } finally {
394
+ setIsLoading(false)
395
+ }
396
+ }
397
+
398
+ return (
399
+ <ForgotPasswordPage
400
+ // \u2500\u2500 Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
401
+ projectName="${o.projectName}"
402
+ projectSubtitle="Reset your password"
403
+ logoSrc="/brand/logo.svg"
404
+ logoAlt="${o.projectName} logo"
405
+ logoSize={56}
406
+
407
+ // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
278
408
  onSubmit={handleSubmit}
279
409
  isLoading={isLoading}
280
410
  error={error}
281
- ${poweredByProp}
411
+ isSuccess={isSuccess}
412
+
413
+ // \u2500\u2500 Success screen text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
414
+ successMessage="Check your email"
415
+ successSubMessage="We've sent a password reset link to your email address. Follow the link to create a new password."
416
+
417
+ // \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
418
+ onBackToLogin={() => router.push('/login')}
419
+ backToLoginHref="/login"
420
+
421
+ // \u2500\u2500 Powered-by badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
422
+ poweredBy={{
423
+ logoSrc: "/brand/powered-by-logo.svg",
424
+ text: "Powered by",
425
+ href: "#",
426
+ }}
427
+
428
+ // \u2500\u2500 Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
429
+ // backgroundGradient="linear-gradient(150deg, #f7f6f3 0%, #f1efe9 55%, #f5f2ed 100%)"
430
+ // ambientColors={[[255,153,51],[19,136,8],[0,0,128]]}
431
+ // socialLinks={[...]}
282
432
  />
283
433
  )
284
434
  }
285
435
  `;
286
436
  }
437
+ function genResetPasswordPage(o) {
438
+ return `'use client'
439
+
440
+ import { ResetPasswordPage } from '@lucifer91299/ui'
441
+ import React, { useState, useEffect, Suspense } from 'react'
442
+ import { useRouter, useSearchParams } from 'next/navigation'
443
+
444
+ function ResetPasswordContent() {
445
+ const router = useRouter()
446
+ const searchParams = useSearchParams()
447
+ const token = searchParams.get('token') ?? ''
448
+
449
+ const [isValidating, setIsValidating] = useState(true)
450
+ const [isValid, setIsValid] = useState(false)
451
+ const [isLoading, setIsLoading] = useState(false)
452
+ const [error, setError] = useState<string | null>(null)
453
+ const [isSuccess, setIsSuccess] = useState(false)
454
+
455
+ // Validate token on mount
456
+ useEffect(() => {
457
+ if (!token) { setIsValidating(false); return }
458
+ fetch(\`/api/auth/reset-password/validate?token=\${encodeURIComponent(token)}\`)
459
+ .then(r => r.json())
460
+ .then(d => setIsValid(d.valid === true))
461
+ .catch(() => setIsValid(false))
462
+ .finally(() => setIsValidating(false))
463
+ }, [token])
464
+
465
+ const handleSubmit = async (password: string) => {
466
+ setError(null)
467
+ setIsLoading(true)
468
+ try {
469
+ const res = await fetch('/api/auth/reset-password', {
470
+ method: 'POST',
471
+ headers: { 'Content-Type': 'application/json' },
472
+ body: JSON.stringify({ token, password }),
473
+ })
474
+ const data = await res.json()
475
+ if (!res.ok) { setError(data.error ?? 'Could not reset password'); return }
476
+ setIsSuccess(true)
477
+ } catch {
478
+ setError('Something went wrong. Please try again.')
479
+ } finally {
480
+ setIsLoading(false)
481
+ }
482
+ }
483
+
484
+ return (
485
+ <ResetPasswordPage
486
+ // \u2500\u2500 Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
487
+ projectName="${o.projectName}"
488
+ projectSubtitle="Set a new password"
489
+ logoSrc="/brand/logo.svg"
490
+ logoAlt="${o.projectName} logo"
491
+ logoSize={56}
492
+
493
+ // \u2500\u2500 Token validation state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
494
+ isValidating={isValidating}
495
+ isValid={isValid}
496
+
497
+ // \u2500\u2500 Form state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
498
+ onSubmit={handleSubmit}
499
+ isLoading={isLoading}
500
+ error={error}
501
+ isSuccess={isSuccess}
502
+
503
+ // \u2500\u2500 Password rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
504
+ minPasswordLength={6}
505
+
506
+ // \u2500\u2500 Custom messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
507
+ validatingMessage="Checking reset link\u2026"
508
+ invalidMessage="Reset link expired"
509
+ invalidSubMessage="This reset link is invalid, expired, or has already been used. Please request a new one."
510
+ successMessage="Password updated"
511
+ successSubMessage="Your password has been changed. You can now sign in with your new password."
512
+
513
+ // \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
514
+ onBackToLogin={() => router.push('/login')}
515
+ backToLoginHref="/login"
516
+
517
+ // \u2500\u2500 Powered-by badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
518
+ poweredBy={{
519
+ logoSrc: "/brand/powered-by-logo.svg",
520
+ text: "Powered by",
521
+ href: "#",
522
+ }}
523
+
524
+ // \u2500\u2500 Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
525
+ // backgroundGradient="linear-gradient(150deg, #f7f6f3 0%, #f1efe9 55%, #f5f2ed 100%)"
526
+ // ambientColors={[[255,153,51],[19,136,8],[0,0,128]]}
527
+ />
528
+ )
529
+ }
530
+
531
+ export default function ResetPassword() {
532
+ return (
533
+ <Suspense>
534
+ <ResetPasswordContent />
535
+ </Suspense>
536
+ )
537
+ }
538
+ `;
539
+ }
540
+ function genRegisterPage(o) {
541
+ return `'use client'
542
+
543
+ import { RegisterPage } from '@lucifer91299/ui'
544
+ import { Input, Select, PhoneInput } from '@lucifer91299/ui'
545
+ import React, { useState } from 'react'
546
+ import { useRouter } from 'next/navigation'
547
+
548
+ const STEPS = [
549
+ { label: 'Account', description: 'Email and password' },
550
+ { label: 'Profile', description: 'Personal details' },
551
+ { label: 'Review', description: 'Confirm and submit' },
552
+ ]
553
+
554
+ export default function Register() {
555
+ const router = useRouter()
556
+ const [currentStep, setCurrentStep] = useState(0)
557
+ const [isLoading, setIsLoading] = useState(false)
558
+ const [error, setError] = useState<string | null>(null)
559
+
560
+ // Form state
561
+ const [email, setEmail] = useState('')
562
+ const [password, setPassword] = useState('')
563
+ const [name, setName] = useState('')
564
+ const [phone, setPhone] = useState('')
565
+ const [role, setRole] = useState('')
566
+
567
+ const handleNext = () => {
568
+ setError(null)
569
+ // Add your step validation here
570
+ if (currentStep === 0 && !email) { setError('Email is required'); return }
571
+ if (currentStep === 0 && !password) { setError('Password is required'); return }
572
+ if (currentStep === 1 && !name) { setError('Name is required'); return }
573
+ setCurrentStep(s => s + 1)
574
+ }
575
+
576
+ const handleSubmit = async () => {
577
+ setError(null)
578
+ setIsLoading(true)
579
+ try {
580
+ const res = await fetch('/api/auth/register', {
581
+ method: 'POST',
582
+ headers: { 'Content-Type': 'application/json' },
583
+ body: JSON.stringify({ email, password, name, phone, role }),
584
+ })
585
+ const data = await res.json()
586
+ if (!res.ok) { setError(data.error ?? 'Registration failed'); return }
587
+ router.replace('/login')
588
+ } catch {
589
+ setError('Something went wrong. Please try again.')
590
+ } finally {
591
+ setIsLoading(false)
592
+ }
593
+ }
594
+
595
+ return (
596
+ <RegisterPage
597
+ // \u2500\u2500 Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
598
+ projectName="${o.projectName}"
599
+ projectSubtitle="Create your account"
600
+ logoSrc="/brand/logo.svg"
601
+ logoAlt="${o.projectName} logo"
602
+ logoSize={40}
603
+
604
+ // \u2500\u2500 Steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
605
+ steps={STEPS}
606
+ currentStep={currentStep}
607
+
608
+ // \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
609
+ onNext={handleNext}
610
+ onBack={() => { setError(null); setCurrentStep(s => s - 1) }}
611
+ onSubmit={handleSubmit}
612
+ nextLabel="Continue"
613
+ backLabel="Back"
614
+ submitLabel="Create Account"
615
+ isLoading={isLoading}
616
+ error={error}
617
+
618
+ // \u2500\u2500 Login link \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
619
+ onLoginLink={() => router.push('/login')}
620
+ loginLabel="Already have an account? Sign in"
621
+
622
+ // \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
623
+ maxWidth="max-w-2xl"
624
+
625
+ // \u2500\u2500 Powered-by badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
626
+ poweredBy={{
627
+ logoSrc: "/brand/powered-by-logo.svg",
628
+ text: "Powered by",
629
+ href: "#",
630
+ }}
631
+
632
+ // \u2500\u2500 Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
633
+ // backgroundGradient="linear-gradient(150deg, #f7f6f3 0%, #f1efe9 55%, #f5f2ed 100%)"
634
+ // ambientColors={[[255,153,51],[19,136,8],[0,0,128]]}
635
+ // socialLinks={[...]}
636
+ >
637
+ {/* \u2500\u2500 Step 1: Account \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
638
+ {currentStep === 0 && (
639
+ <>
640
+ <Input
641
+ label="Email address"
642
+ type="email"
643
+ value={email}
644
+ onChange={e => setEmail(e.target.value)}
645
+ placeholder="you@example.com"
646
+ autoComplete="email"
647
+ />
648
+ <Input
649
+ label="Password"
650
+ type="password"
651
+ value={password}
652
+ onChange={e => setPassword(e.target.value)}
653
+ placeholder="Minimum 6 characters"
654
+ autoComplete="new-password"
655
+ />
656
+ </>
657
+ )}
658
+
659
+ {/* \u2500\u2500 Step 2: Profile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
660
+ {currentStep === 1 && (
661
+ <>
662
+ <Input
663
+ label="Full name"
664
+ value={name}
665
+ onChange={e => setName(e.target.value)}
666
+ placeholder="Priya Mehta"
667
+ />
668
+ <PhoneInput
669
+ label="Phone number"
670
+ value={phone}
671
+ onChange={setPhone}
672
+ />
673
+ <Select
674
+ label="Role"
675
+ value={role}
676
+ onChange={setRole}
677
+ options={[
678
+ { value: 'member', label: 'Member' },
679
+ { value: 'manager', label: 'Manager' },
680
+ ]}
681
+ />
682
+ </>
683
+ )}
684
+
685
+ {/* \u2500\u2500 Step 3: Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
686
+ {currentStep === 2 && (
687
+ <div className="space-y-3">
688
+ <div className="bg-surface-secondary rounded-xl p-4 text-sm space-y-2">
689
+ <div className="flex justify-between">
690
+ <span className="text-label-tertiary">Email</span>
691
+ <span className="font-medium text-label-primary">{email}</span>
692
+ </div>
693
+ <div className="flex justify-between">
694
+ <span className="text-label-tertiary">Name</span>
695
+ <span className="font-medium text-label-primary">{name || '\u2014'}</span>
696
+ </div>
697
+ <div className="flex justify-between">
698
+ <span className="text-label-tertiary">Phone</span>
699
+ <span className="font-medium text-label-primary">{phone || '\u2014'}</span>
700
+ </div>
701
+ <div className="flex justify-between">
702
+ <span className="text-label-tertiary">Role</span>
703
+ <span className="font-medium text-label-primary">{role || '\u2014'}</span>
704
+ </div>
705
+ </div>
706
+ <p className="text-xs text-label-tertiary text-center">
707
+ Review your details above and click Create Account to proceed.
708
+ </p>
709
+ </div>
710
+ )}
711
+ </RegisterPage>
712
+ )
713
+ }
714
+ `;
715
+ }
716
+ function genForgotPasswordRoute() {
717
+ return `import { NextResponse } from 'next/server'
718
+
719
+ export async function POST(request: Request) {
720
+ const { email } = (await request.json()) as { email?: string }
721
+
722
+ if (!email) {
723
+ return NextResponse.json({ error: 'Email is required' }, { status: 400 })
724
+ }
725
+
726
+ // TODO: implement your forgot-password logic here.
727
+ // 1. Look up the user by email.
728
+ // 2. Generate a secure reset token and store it (with expiry).
729
+ // 3. Send a reset email containing: /reset-password?token=<token>
730
+ //
731
+ // Example with a backend API:
732
+ // const res = await fetch(\`\${process.env.API_URL}/auth/forgot-password\`, {
733
+ // method: 'POST',
734
+ // headers: { 'Content-Type': 'application/json' },
735
+ // body: JSON.stringify({ email }),
736
+ // })
737
+ // if (!res.ok) return NextResponse.json({ error: 'User not found' }, { status: 404 })
738
+
739
+ // Always return 200 to avoid leaking whether the email exists.
740
+ return NextResponse.json({ ok: true })
741
+ }
742
+ `;
743
+ }
744
+ function genResetPasswordRoutes() {
745
+ return `import { NextResponse } from 'next/server'
746
+
747
+ export async function POST(request: Request) {
748
+ const { token, password } = (await request.json()) as { token?: string; password?: string }
749
+
750
+ if (!token || !password) {
751
+ return NextResponse.json({ error: 'Token and password are required' }, { status: 400 })
752
+ }
753
+
754
+ // TODO: implement your reset-password logic here.
755
+ // 1. Look up the token in your store and check it hasn't expired.
756
+ // 2. Hash the new password and update the user record.
757
+ // 3. Invalidate / delete the token.
758
+ //
759
+ // Example with a backend API:
760
+ // const res = await fetch(\`\${process.env.API_URL}/auth/reset-password\`, {
761
+ // method: 'POST',
762
+ // headers: { 'Content-Type': 'application/json' },
763
+ // body: JSON.stringify({ token, password }),
764
+ // })
765
+ // if (!res.ok) return NextResponse.json({ error: 'Invalid or expired token' }, { status: 400 })
766
+
767
+ return NextResponse.json({ ok: true })
768
+ }
769
+ `;
770
+ }
771
+ function genResetPasswordValidateRoute() {
772
+ return `import { NextResponse } from 'next/server'
773
+
774
+ export async function GET(request: Request) {
775
+ const { searchParams } = new URL(request.url)
776
+ const token = searchParams.get('token')
777
+
778
+ if (!token) {
779
+ return NextResponse.json({ valid: false })
780
+ }
781
+
782
+ // TODO: validate the reset token.
783
+ // Check that it exists in your store and hasn't expired.
784
+ //
785
+ // Example with a backend API:
786
+ // const res = await fetch(\`\${process.env.API_URL}/auth/reset-password/validate?token=\${token}\`)
787
+ // const data = await res.json()
788
+ // return NextResponse.json({ valid: res.ok && data.valid })
789
+
790
+ // Demo: treat any non-empty token as valid
791
+ return NextResponse.json({ valid: token.length > 0 })
792
+ }
793
+ `;
794
+ }
287
795
  function genLoginRoute(o) {
288
796
  const cookieName = o.jwtCookieName ?? "access_token";
289
797
  return `import { SignJWT } from 'jose'
@@ -3086,6 +3594,9 @@ function scaffold(opts) {
3086
3594
  w(f("src/app/layout.tsx"), genRootLayout(opts));
3087
3595
  w(f("src/app/page.tsx"), genRootPage());
3088
3596
  w(f("src/app/login/page.tsx"), genLoginPage(opts));
3597
+ w(f("src/app/forgot-password/page.tsx"), genForgotPasswordPage(opts));
3598
+ w(f("src/app/reset-password/page.tsx"), genResetPasswordPage(opts));
3599
+ w(f("src/app/register/page.tsx"), genRegisterPage(opts));
3089
3600
  w(f("src/app/dashboard/layout.tsx"), genDashboardLayout(opts));
3090
3601
  w(f("src/app/dashboard/page.tsx"), genDashboardHomePage(opts));
3091
3602
  w(f("src/app/dashboard/users/page.tsx"), genUsersPage(opts));
@@ -3097,6 +3608,9 @@ function scaffold(opts) {
3097
3608
  w(f("src/app/api/auth/user/route.ts"), genUserRoute(opts));
3098
3609
  w(f("src/app/api/auth/session/route.ts"), genSessionRoute(opts));
3099
3610
  w(f("src/app/api/auth/logout/route.ts"), genLogoutRoute(opts));
3611
+ w(f("src/app/api/auth/forgot-password/route.ts"), genForgotPasswordRoute());
3612
+ w(f("src/app/api/auth/reset-password/route.ts"), genResetPasswordRoutes());
3613
+ w(f("src/app/api/auth/reset-password/validate/route.ts"), genResetPasswordValidateRoute());
3100
3614
  w(f("src/providers/index.tsx"), genProviders(opts));
3101
3615
  w(f("src/lib/api.ts"), genApiClient(opts));
3102
3616
  w(f("src/components/layout/nav-config.tsx"), genNavConfig(opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucifer91299/create-portal-app",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "Scaffold a Next.js authenticated portal with full design system in one command",
5
5
  "license": "MIT",
6
6
  "author": "Aakash Kanojiya",