@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.
- package/dist/cli/index.js +526 -12
- 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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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