create-nevela 0.2.1 → 0.4.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/README.md +17 -6
- package/index.mjs +89 -17
- package/package.json +2 -1
- package/project.mjs +9 -4
- package/selfupdate.mjs +13 -3
- package/template/laravel/README.md +1 -1
- package/template/laravel/composer.json +2 -1
- package/template/laravel/config/nevela.php +66 -1
- package/template/laravel/database/migrations/2026_10_05_000000_create_nevela_auth_tables.php +66 -0
- package/template/laravel/src/Auth/Account.php +140 -0
- package/template/laravel/src/Auth/AuthMail.php +77 -0
- package/template/laravel/src/Auth/Challenges.php +135 -0
- package/template/laravel/src/Auth/Passkeys.php +206 -0
- package/template/laravel/src/Auth/Totp.php +106 -0
- package/template/laravel/src/Auth/Web.php +75 -0
- package/template/laravel/src/Console/GenerateCommand.php +1 -0
- package/template/laravel/src/Console/StatusCommand.php +2 -2
- package/template/laravel/src/Console/UpdateCommand.php +67 -9
- package/template/laravel/src/Generator/ResourceGenerator.php +63 -2
- package/template/laravel/src/Http/Auth/AccountController.php +252 -0
- package/template/laravel/src/Http/Auth/Answers.php +33 -0
- package/template/laravel/src/Http/Auth/SecurityController.php +188 -0
- package/template/laravel/src/Http/Auth/SignInController.php +302 -0
- package/template/laravel/src/Http/TokenController.php +19 -37
- package/template/laravel/src/Http/UploadController.php +24 -12
- package/template/laravel/src/Models/Passkey.php +38 -0
- package/template/laravel/src/Models/TwoFactor.php +64 -0
- package/template/laravel/src/Nevela.php +1 -1
- package/template/laravel/src/NevelaServiceProvider.php +72 -4
- package/template/laravel/src/Support/Descriptor.php +3 -3
- package/template/laravel/src/Support/Field.php +29 -2
- package/template/web/app/api/[...path]/route.ts +5 -1
- package/template/web/app/api/auth/[...action]/route.ts +196 -0
- package/template/web/app/auth-actions.ts +4 -43
- package/template/web/app/dashboard/account/layout.tsx +29 -0
- package/template/web/app/dashboard/account/page.tsx +19 -0
- package/template/web/app/dashboard/account/password/page.tsx +10 -0
- package/template/web/app/dashboard/account/security/page.tsx +19 -0
- package/template/web/app/dashboard/account/sessions/page.tsx +9 -0
- package/template/web/app/dashboard/layout.tsx +2 -1
- package/template/web/app/forgot-password/page.tsx +22 -0
- package/template/web/app/reset-password/page.tsx +31 -0
- package/template/web/app/sign-in/page.tsx +34 -4
- package/template/web/app/sign-up/page.tsx +43 -0
- package/template/web/app/two-factor/page.tsx +34 -0
- package/template/web/app/verify-email/page.tsx +33 -0
- package/template/web/components/account/account-nav.tsx +39 -0
- package/template/web/components/account/sections.tsx +573 -0
- package/template/web/components/auth/otp-input.tsx +95 -0
- package/template/web/components/auth/password-field.tsx +116 -0
- package/template/web/components/auth/password-forms.tsx +76 -0
- package/template/web/components/auth/provider-icons.tsx +39 -0
- package/template/web/components/auth/sign-in-flow.tsx +307 -0
- package/template/web/components/auth/sign-up-form.tsx +93 -0
- package/template/web/components/auth/social-buttons.tsx +56 -0
- package/template/web/components/auth/two-factor-form.tsx +144 -0
- package/template/web/components/auth/use-resend.ts +44 -0
- package/template/web/components/auth/verify-email-form.tsx +147 -0
- package/template/web/components/dashboard/dashboard-sidebar.tsx +2 -2
- package/template/web/components/dashboard/user-menu.tsx +33 -1
- package/template/web/lib/auth-client.ts +279 -0
- package/template/web/lib/auth-config.ts +43 -0
- package/template/web/lib/laravel.ts +26 -1
- package/template/web/lib/password-rules.ts +167 -0
- package/template/web/lib/second-step.ts +35 -0
- package/template/web/lib/session.ts +12 -4
- package/template/web/package.json +3 -2
- package/template.mjs +1 -1
- package/tool.mjs +58 -0
- package/template/web/components/auth/sign-in-form.tsx +0 -50
|
@@ -5,6 +5,7 @@ import { DashboardHeader, type Theme } from "@/components/dashboard/dashboard-he
|
|
|
5
5
|
import { DashboardSidebar } from "@/components/dashboard/dashboard-sidebar";
|
|
6
6
|
import { dashboardLinks } from "@/lib/dashboard-nav";
|
|
7
7
|
import { ADMIN_ROLES, visibleResources } from "@/lib/dashboard";
|
|
8
|
+
import { fileUrl } from "@/lib/files";
|
|
8
9
|
import { requireSession } from "@/lib/session";
|
|
9
10
|
import { site } from "@/lib/site";
|
|
10
11
|
import { listViews } from "@/lib/views";
|
|
@@ -29,7 +30,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
|
|
29
30
|
views: views.filter((view) => view.resource === resource.name).map(({ id, name, query }) => ({ id, name, query })),
|
|
30
31
|
}));
|
|
31
32
|
|
|
32
|
-
const account = { name: user.name, email: user.email };
|
|
33
|
+
const account = { name: user.name, email: user.email, image: user.avatar ? fileUrl(user.avatar, "thumb") : null };
|
|
33
34
|
|
|
34
35
|
return (
|
|
35
36
|
<SidebarProvider defaultOpen={!collapsed}>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { AuthShell } from "@/components/auth/auth-shell";
|
|
3
|
+
import { ForgotPasswordForm } from "@/components/auth/password-forms";
|
|
4
|
+
|
|
5
|
+
export const metadata = { title: "Reset your password" };
|
|
6
|
+
|
|
7
|
+
export default async function ForgotPasswordPage({ searchParams }: { searchParams: Promise<{ email?: string }> }) {
|
|
8
|
+
const { email } = await searchParams;
|
|
9
|
+
return (
|
|
10
|
+
<AuthShell
|
|
11
|
+
title="Reset your password"
|
|
12
|
+
subtitle="We'll email you a link to choose a new one."
|
|
13
|
+
footer={
|
|
14
|
+
<Link href="/sign-in" className="font-medium text-link hover:underline">
|
|
15
|
+
Back to sign in
|
|
16
|
+
</Link>
|
|
17
|
+
}
|
|
18
|
+
>
|
|
19
|
+
<ForgotPasswordForm initialEmail={email ?? ""} />
|
|
20
|
+
</AuthShell>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { AuthShell } from "@/components/auth/auth-shell";
|
|
3
|
+
import { ResetPasswordForm } from "@/components/auth/password-forms";
|
|
4
|
+
import { FormMessage } from "@/components/auth/ui";
|
|
5
|
+
|
|
6
|
+
export const metadata = { title: "Choose a new password" };
|
|
7
|
+
|
|
8
|
+
export default async function ResetPasswordPage({ searchParams }: { searchParams: Promise<{ token?: string; error?: string }> }) {
|
|
9
|
+
const { token, error } = await searchParams;
|
|
10
|
+
return (
|
|
11
|
+
<AuthShell
|
|
12
|
+
title="Choose a new password"
|
|
13
|
+
footer={
|
|
14
|
+
<Link href="/sign-in" className="font-medium text-link hover:underline">
|
|
15
|
+
Back to sign in
|
|
16
|
+
</Link>
|
|
17
|
+
}
|
|
18
|
+
>
|
|
19
|
+
{token && !error ? (
|
|
20
|
+
<ResetPasswordForm token={token} />
|
|
21
|
+
) : (
|
|
22
|
+
<div className="flex flex-col gap-4">
|
|
23
|
+
<FormMessage>This reset link has expired or was already used.</FormMessage>
|
|
24
|
+
<Link href="/forgot-password" className="text-center text-sm font-medium text-link hover:underline">
|
|
25
|
+
Ask for a new link
|
|
26
|
+
</Link>
|
|
27
|
+
</div>
|
|
28
|
+
)}
|
|
29
|
+
</AuthShell>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -1,21 +1,51 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
1
2
|
import { redirect } from "next/navigation";
|
|
2
3
|
import { AuthShell } from "@/components/auth/auth-shell";
|
|
3
|
-
import {
|
|
4
|
+
import { SignInFlow } from "@/components/auth/sign-in-flow";
|
|
5
|
+
import { authConfig, enabledSocialProviders } from "@/lib/auth-config";
|
|
4
6
|
import { getSession, safeRedirectPath } from "@/lib/session";
|
|
5
7
|
import { site } from "@/lib/site";
|
|
6
8
|
import { theme } from "@/lib/theme";
|
|
7
9
|
|
|
8
10
|
export const metadata = { title: "Sign in" };
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
const ERRORS: Record<string, string> = {
|
|
13
|
+
oauth: "Sign-in with that provider didn't finish. Try again.",
|
|
14
|
+
link: "That sign-in link has expired or was already used. Ask for a new one.",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default async function SignInPage({ searchParams }: { searchParams: Promise<{ next?: string; error?: string; reset?: string }> }) {
|
|
11
18
|
const params = await searchParams;
|
|
12
19
|
const next = safeRedirectPath(params.next);
|
|
13
20
|
if (await getSession()) redirect(next);
|
|
14
21
|
|
|
15
22
|
const copy = theme.signIn(site.name);
|
|
16
23
|
return (
|
|
17
|
-
<AuthShell
|
|
18
|
-
|
|
24
|
+
<AuthShell
|
|
25
|
+
page="sign-in"
|
|
26
|
+
title={copy.title}
|
|
27
|
+
subtitle={(copy as { subtitle?: string }).subtitle}
|
|
28
|
+
footer={
|
|
29
|
+
// Only where people may create their own account (config/nevela.php: auth.registration).
|
|
30
|
+
authConfig.registration ? (
|
|
31
|
+
<>
|
|
32
|
+
Don't have an account?{" "}
|
|
33
|
+
<Link href={`/sign-up?next=${encodeURIComponent(next)}`} className="font-medium text-link hover:underline">
|
|
34
|
+
Sign up
|
|
35
|
+
</Link>
|
|
36
|
+
</>
|
|
37
|
+
) : undefined
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
<SignInFlow
|
|
41
|
+
providers={enabledSocialProviders()}
|
|
42
|
+
socialPlacement={theme.social}
|
|
43
|
+
socialStyle={theme.socialStyle}
|
|
44
|
+
methods={{ magicLink: authConfig.magicLink, emailOtp: authConfig.emailOtp, passkeys: authConfig.passkeys }}
|
|
45
|
+
next={next}
|
|
46
|
+
initialError={params.error ? (ERRORS[params.error] ?? "Sign-in didn't finish. Try again.") : null}
|
|
47
|
+
initialNotice={params.reset ? "Your password is changed. Sign in with the new one." : null}
|
|
48
|
+
/>
|
|
19
49
|
</AuthShell>
|
|
20
50
|
);
|
|
21
51
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { AuthShell } from "@/components/auth/auth-shell";
|
|
4
|
+
import { SignUpForm } from "@/components/auth/sign-up-form";
|
|
5
|
+
import { authConfig, enabledSocialProviders } from "@/lib/auth-config";
|
|
6
|
+
import { getSession, safeRedirectPath } from "@/lib/session";
|
|
7
|
+
import { site } from "@/lib/site";
|
|
8
|
+
import { theme } from "@/lib/theme";
|
|
9
|
+
|
|
10
|
+
export const metadata = { title: "Create an account" };
|
|
11
|
+
|
|
12
|
+
export default async function SignUpPage({ searchParams }: { searchParams: Promise<{ next?: string }> }) {
|
|
13
|
+
const params = await searchParams;
|
|
14
|
+
const next = safeRedirectPath(params.next);
|
|
15
|
+
if (await getSession()) redirect(next);
|
|
16
|
+
// Accounts are created by an administrator when registration is off.
|
|
17
|
+
if (!authConfig.registration) redirect(`/sign-in?next=${encodeURIComponent(next)}`);
|
|
18
|
+
|
|
19
|
+
const copy = theme.signUp(site.name);
|
|
20
|
+
return (
|
|
21
|
+
<AuthShell
|
|
22
|
+
page="sign-up"
|
|
23
|
+
title={copy.title}
|
|
24
|
+
subtitle={(copy as { subtitle?: string }).subtitle}
|
|
25
|
+
footer={
|
|
26
|
+
<>
|
|
27
|
+
Already have an account?{" "}
|
|
28
|
+
<Link href={`/sign-in?next=${encodeURIComponent(next)}`} className="font-medium text-link hover:underline">
|
|
29
|
+
Sign in
|
|
30
|
+
</Link>
|
|
31
|
+
</>
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
<SignUpForm
|
|
35
|
+
providers={enabledSocialProviders()}
|
|
36
|
+
socialPlacement={theme.social}
|
|
37
|
+
socialStyle={theme.socialStyle}
|
|
38
|
+
next={next}
|
|
39
|
+
verifyFirst={authConfig.requireEmailVerification}
|
|
40
|
+
/>
|
|
41
|
+
</AuthShell>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { AuthShell } from "@/components/auth/auth-shell";
|
|
3
|
+
import { TwoFactorForm } from "@/components/auth/two-factor-form";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
import { pendingSecondStep } from "@/lib/second-step";
|
|
6
|
+
import { safeRedirectPath } from "@/lib/session";
|
|
7
|
+
|
|
8
|
+
export const metadata = { title: "Verify it's you" };
|
|
9
|
+
|
|
10
|
+
/** Reached after a correct password on an account with two-factor on (lib/auth-client.ts sends people here). */
|
|
11
|
+
export default async function TwoFactorPage({ searchParams }: { searchParams: Promise<{ next?: string }> }) {
|
|
12
|
+
const { next } = await searchParams;
|
|
13
|
+
// Nothing is waiting: they came here directly, or took more than ten minutes.
|
|
14
|
+
const waiting = await pendingSecondStep();
|
|
15
|
+
if (!waiting) redirect(`/sign-in?next=${encodeURIComponent(safeRedirectPath(next))}`);
|
|
16
|
+
return (
|
|
17
|
+
<AuthShell
|
|
18
|
+
title="Verify it's you"
|
|
19
|
+
subtitle="Your account uses two-factor authentication."
|
|
20
|
+
footer={
|
|
21
|
+
<Link href="/sign-in" className="font-medium text-link hover:underline">
|
|
22
|
+
Use a different account
|
|
23
|
+
</Link>
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
{/* What Laravel said this sign-in can be finished with: after an emailed link, not another email. */}
|
|
27
|
+
<TwoFactorForm
|
|
28
|
+
methods={{ authenticator: waiting.methods.includes("totp"), email: waiting.methods.includes("email") }}
|
|
29
|
+
backup={waiting.methods.includes("backup")}
|
|
30
|
+
next={safeRedirectPath(next)}
|
|
31
|
+
/>
|
|
32
|
+
</AuthShell>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { AuthShell } from "@/components/auth/auth-shell";
|
|
4
|
+
import { VerifyEmailForm } from "@/components/auth/verify-email-form";
|
|
5
|
+
import { getSession, safeRedirectPath } from "@/lib/session";
|
|
6
|
+
|
|
7
|
+
export const metadata = { title: "Verify your email" };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Confirm an email address: by the code in the message, or by asking for a fresh link
|
|
11
|
+
* when the one they have has expired.
|
|
12
|
+
*/
|
|
13
|
+
export default async function VerifyEmailPage({ searchParams }: { searchParams: Promise<{ email?: string; next?: string; code?: string }> }) {
|
|
14
|
+
const { email, next, code } = await searchParams;
|
|
15
|
+
const session = await getSession();
|
|
16
|
+
const to = safeRedirectPath(next, "/dashboard/account");
|
|
17
|
+
if (session?.user.emailVerified) redirect(to);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<AuthShell
|
|
21
|
+
title="Verify your email"
|
|
22
|
+
subtitle="One step, so we know we can reach you."
|
|
23
|
+
footer={
|
|
24
|
+
<Link href={session ? "/dashboard" : "/sign-in"} className="font-medium text-link hover:underline">
|
|
25
|
+
{session ? "Back to the dashboard" : "Back to sign in"}
|
|
26
|
+
</Link>
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
{/* Codes are always used here: the email carries one, and its link brings it along. */}
|
|
30
|
+
<VerifyEmailForm initialEmail={email ?? session?.user.email ?? ""} initialCode={/^\d{6}$/.test(code ?? "") ? code : undefined} codes next={to} />
|
|
31
|
+
</AuthShell>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface AccountLink {
|
|
8
|
+
href: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The account's own navigation. Each part of an account is its own page: a single page
|
|
14
|
+
* of everything means scrolling past two-factor to reach your devices, and no way to
|
|
15
|
+
* send someone straight to the part that needs their attention.
|
|
16
|
+
*/
|
|
17
|
+
export function AccountNav({ links }: { links: AccountLink[] }) {
|
|
18
|
+
const pathname = usePathname();
|
|
19
|
+
return (
|
|
20
|
+
<nav aria-label="Account" className="-mb-px flex gap-1 overflow-x-auto border-b">
|
|
21
|
+
{links.map((link) => {
|
|
22
|
+
const current = pathname === link.href;
|
|
23
|
+
return (
|
|
24
|
+
<Link
|
|
25
|
+
key={link.href}
|
|
26
|
+
href={link.href}
|
|
27
|
+
aria-current={current ? "page" : undefined}
|
|
28
|
+
className={cn(
|
|
29
|
+
"border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap transition-colors",
|
|
30
|
+
current ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground",
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{link.label}
|
|
34
|
+
</Link>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
</nav>
|
|
38
|
+
);
|
|
39
|
+
}
|