create-stackrjs 1.4.0
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 +154 -0
- package/dist/index.js +1135 -0
- package/package.json +62 -0
- package/template/architectures/microservices/_gitignore +26 -0
- package/template/architectures/microservices/apps/web/Dockerfile +11 -0
- package/template/architectures/microservices/apps/web/next-env.d.ts +5 -0
- package/template/architectures/microservices/apps/web/next.config.mjs +4 -0
- package/template/architectures/microservices/apps/web/package.json +25 -0
- package/template/architectures/microservices/apps/web/src/app/account/page.tsx +56 -0
- package/template/architectures/microservices/apps/web/src/app/globals.css +3 -0
- package/template/architectures/microservices/apps/web/src/app/layout.tsx +16 -0
- package/template/architectures/microservices/apps/web/src/app/login/page.tsx +70 -0
- package/template/architectures/microservices/apps/web/src/app/page.tsx +33 -0
- package/template/architectures/microservices/apps/web/src/app/register/page.tsx +71 -0
- package/template/architectures/microservices/apps/web/src/lib/api.ts +15 -0
- package/template/architectures/microservices/apps/web/tsconfig.json +23 -0
- package/template/architectures/microservices/docker-compose.yml +45 -0
- package/template/architectures/microservices/gateway/nginx.conf +20 -0
- package/template/architectures/microservices/package.json +21 -0
- package/template/architectures/microservices/services/api/Dockerfile +11 -0
- package/template/architectures/microservices/services/api/package.json +34 -0
- package/template/architectures/microservices/services/api/prisma/schema.prisma +17 -0
- package/template/architectures/microservices/services/api/prisma.config.ts +17 -0
- package/template/architectures/microservices/services/api/src/auth.ts +22 -0
- package/template/architectures/microservices/services/api/src/db.ts +12 -0
- package/template/architectures/microservices/services/api/src/index.ts +22 -0
- package/template/architectures/microservices/services/api/src/routes/auth.ts +73 -0
- package/template/architectures/microservices/services/api/tsconfig.json +16 -0
- package/template/base/_eslintrc.json +3 -0
- package/template/base/_gitignore +24 -0
- package/template/base/_prettierrc.json +6 -0
- package/template/base/next-env.d.ts +5 -0
- package/template/base/next.config.mjs +4 -0
- package/template/base/package.json +27 -0
- package/template/base/src/app/globals.css +14 -0
- package/template/base/src/app/layout.tsx +16 -0
- package/template/base/src/app/page.tsx +10 -0
- package/template/base/tsconfig.json +23 -0
- package/template/extras/account-better-auth/src/server/account.ts +52 -0
- package/template/extras/account-clerk/src/server/account.ts +64 -0
- package/template/extras/account-nextauth/src/server/account.ts +59 -0
- package/template/extras/account-supabase/src/lib/supabase/admin.ts +14 -0
- package/template/extras/account-supabase/src/server/account.ts +65 -0
- package/template/extras/admin/src/app/admin/page.tsx +55 -0
- package/template/extras/auth-pages/src/app/account/page.tsx +37 -0
- package/template/extras/auth-pages/src/app/forgot-password/page.tsx +33 -0
- package/template/extras/auth-pages/src/app/login/page.tsx +32 -0
- package/template/extras/auth-pages/src/app/page.tsx +46 -0
- package/template/extras/auth-pages/src/app/register/page.tsx +36 -0
- package/template/extras/auth-pages/src/app/reset-password/page.tsx +49 -0
- package/template/extras/auth-pages/src/components/auth/ui.tsx +57 -0
- package/template/extras/auth-pages-drizzle/src/server/actions.ts +122 -0
- package/template/extras/auth-pages-mongoose/src/server/actions.ts +121 -0
- package/template/extras/auth-pages-prisma/src/server/actions.ts +117 -0
- package/template/extras/better-auth/prisma/schema.prisma +70 -0
- package/template/extras/better-auth/src/app/account/page.tsx +42 -0
- package/template/extras/better-auth/src/app/api/auth/[...all]/route.ts +5 -0
- package/template/extras/better-auth/src/app/login/page.tsx +42 -0
- package/template/extras/better-auth/src/app/register/page.tsx +44 -0
- package/template/extras/better-auth/src/components/auth/ui.tsx +58 -0
- package/template/extras/better-auth/src/lib/auth-client.ts +7 -0
- package/template/extras/better-auth/src/server/auth.ts +13 -0
- package/template/extras/clerk/middleware.ts +14 -0
- package/template/extras/clerk/src/app/account/page.tsx +30 -0
- package/template/extras/clerk/src/app/layout.tsx +19 -0
- package/template/extras/clerk/src/app/page.tsx +40 -0
- package/template/extras/clerk/src/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/template/extras/clerk/src/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/template/extras/dashboard/src/app/dashboard/page.tsx +48 -0
- package/template/extras/databases/mongodb/docker-compose.yml +13 -0
- package/template/extras/databases/mysql/docker-compose.yml +14 -0
- package/template/extras/databases/postgres/docker-compose.yml +15 -0
- package/template/extras/drizzle/mysql/drizzle.config.ts +9 -0
- package/template/extras/drizzle/mysql/src/server/db.ts +11 -0
- package/template/extras/drizzle/mysql/src/server/schema.ts +17 -0
- package/template/extras/drizzle/postgres/drizzle.config.ts +9 -0
- package/template/extras/drizzle/postgres/src/server/db.ts +11 -0
- package/template/extras/drizzle/postgres/src/server/schema.ts +20 -0
- package/template/extras/landing/src/app/page.tsx +53 -0
- package/template/extras/mongoose/src/server/db.ts +12 -0
- package/template/extras/mongoose/src/server/models.ts +25 -0
- package/template/extras/nextauth/middleware.ts +11 -0
- package/template/extras/nextauth/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/template/extras/nextauth/src/server/auth.config.ts +39 -0
- package/template/extras/nextauth/src/server/roles.ts +5 -0
- package/template/extras/nextauth/src/types/next-auth.d.ts +22 -0
- package/template/extras/nextauth-drizzle/src/server/auth.ts +44 -0
- package/template/extras/nextauth-mongoose/src/server/auth.ts +42 -0
- package/template/extras/nextauth-prisma/src/server/auth.ts +43 -0
- package/template/extras/prisma/base/prisma/schema.prisma +67 -0
- package/template/extras/prisma/base/prisma.config.ts +10 -0
- package/template/extras/prisma/mysql/src/server/db.ts +25 -0
- package/template/extras/prisma/postgres/src/server/db.ts +18 -0
- package/template/extras/shadcn/components.json +20 -0
- package/template/extras/shadcn/postcss.config.mjs +7 -0
- package/template/extras/shadcn/src/app/globals.css +59 -0
- package/template/extras/shadcn/src/components/ui/button.tsx +42 -0
- package/template/extras/shadcn/src/components/ui/card.tsx +54 -0
- package/template/extras/shadcn/src/components/ui/input.tsx +19 -0
- package/template/extras/shadcn/src/lib/utils.ts +7 -0
- package/template/extras/shadcn/tailwind.config.ts +71 -0
- package/template/extras/supabase/middleware.ts +13 -0
- package/template/extras/supabase/src/app/account/page.tsx +32 -0
- package/template/extras/supabase/src/app/login/page.tsx +47 -0
- package/template/extras/supabase/src/app/register/page.tsx +52 -0
- package/template/extras/supabase/src/components/auth/sign-out-button.tsx +25 -0
- package/template/extras/supabase/src/components/auth/ui.tsx +58 -0
- package/template/extras/supabase/src/lib/supabase/client.ts +9 -0
- package/template/extras/supabase/src/lib/supabase/middleware.ts +34 -0
- package/template/extras/supabase/src/lib/supabase/server.ts +29 -0
- package/template/extras/tailwind/postcss.config.mjs +7 -0
- package/template/extras/tailwind/src/app/globals.css +3 -0
- package/template/extras/tailwind/tailwind.config.ts +9 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type FormEvent } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { authClient } from "@/lib/auth-client";
|
|
6
|
+
import { Alert, AuthShell, Field, FooterLinks, SubmitButton, TextLink } from "@/components/auth/ui";
|
|
7
|
+
|
|
8
|
+
export default function LoginPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [error, setError] = useState<string | null>(null);
|
|
11
|
+
const [pending, setPending] = useState(false);
|
|
12
|
+
|
|
13
|
+
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
setPending(true);
|
|
16
|
+
setError(null);
|
|
17
|
+
const data = new FormData(event.currentTarget);
|
|
18
|
+
const { error } = await authClient.signIn.email({
|
|
19
|
+
email: String(data.get("email")),
|
|
20
|
+
password: String(data.get("password")),
|
|
21
|
+
});
|
|
22
|
+
setPending(false);
|
|
23
|
+
if (error) setError(error.message ?? "Invalid email or password.");
|
|
24
|
+
else router.push("/account");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<AuthShell title="Sign in">
|
|
29
|
+
{error && <Alert kind="error">{error}</Alert>}
|
|
30
|
+
<form onSubmit={onSubmit}>
|
|
31
|
+
<Field label="Email" name="email" type="email" required autoComplete="email" />
|
|
32
|
+
<Field label="Password" name="password" type="password" required autoComplete="current-password" />
|
|
33
|
+
<SubmitButton disabled={pending}>{pending ? "Signing in…" : "Sign in"}</SubmitButton>
|
|
34
|
+
</form>
|
|
35
|
+
<FooterLinks>
|
|
36
|
+
<p>
|
|
37
|
+
No account? <TextLink href="/register">Create one</TextLink>
|
|
38
|
+
</p>
|
|
39
|
+
</FooterLinks>
|
|
40
|
+
</AuthShell>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type FormEvent } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { authClient } from "@/lib/auth-client";
|
|
6
|
+
import { Alert, AuthShell, Field, FooterLinks, SubmitButton, TextLink } from "@/components/auth/ui";
|
|
7
|
+
|
|
8
|
+
export default function RegisterPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [error, setError] = useState<string | null>(null);
|
|
11
|
+
const [pending, setPending] = useState(false);
|
|
12
|
+
|
|
13
|
+
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
setPending(true);
|
|
16
|
+
setError(null);
|
|
17
|
+
const data = new FormData(event.currentTarget);
|
|
18
|
+
const { error } = await authClient.signUp.email({
|
|
19
|
+
name: String(data.get("name")),
|
|
20
|
+
email: String(data.get("email")),
|
|
21
|
+
password: String(data.get("password")),
|
|
22
|
+
});
|
|
23
|
+
setPending(false);
|
|
24
|
+
if (error) setError(error.message ?? "Could not create the account.");
|
|
25
|
+
else router.push("/account");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<AuthShell title="Create account">
|
|
30
|
+
{error && <Alert kind="error">{error}</Alert>}
|
|
31
|
+
<form onSubmit={onSubmit}>
|
|
32
|
+
<Field label="Name" name="name" type="text" required autoComplete="name" />
|
|
33
|
+
<Field label="Email" name="email" type="email" required autoComplete="email" />
|
|
34
|
+
<Field label="Password" name="password" type="password" required minLength={8} autoComplete="new-password" />
|
|
35
|
+
<SubmitButton disabled={pending}>{pending ? "Creating…" : "Create account"}</SubmitButton>
|
|
36
|
+
</form>
|
|
37
|
+
<FooterLinks>
|
|
38
|
+
<p>
|
|
39
|
+
Already have an account? <TextLink href="/login">Sign in</TextLink>
|
|
40
|
+
</p>
|
|
41
|
+
</FooterLinks>
|
|
42
|
+
</AuthShell>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
/** Centered card shell shared by all auth pages. */
|
|
5
|
+
export function AuthShell({ title, children }: { title: string; children: ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<main className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
|
8
|
+
<div className="w-full max-w-sm rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
9
|
+
<h1 className="mb-6 text-2xl font-semibold tracking-tight">{title}</h1>
|
|
10
|
+
{children}
|
|
11
|
+
</div>
|
|
12
|
+
</main>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Field({ label, ...props }: { label: string } & ComponentProps<"input">) {
|
|
17
|
+
return (
|
|
18
|
+
<label className="mb-4 block">
|
|
19
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">{label}</span>
|
|
20
|
+
<input
|
|
21
|
+
{...props}
|
|
22
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900"
|
|
23
|
+
/>
|
|
24
|
+
</label>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function SubmitButton({ children, ...props }: ComponentProps<"button">) {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
{...props}
|
|
32
|
+
type="submit"
|
|
33
|
+
className="w-full rounded-md bg-gray-900 px-3 py-2 text-sm font-medium text-white transition hover:bg-gray-700 disabled:opacity-60"
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Alert({ kind, children }: { kind: "error" | "success"; children: ReactNode }) {
|
|
41
|
+
const styles =
|
|
42
|
+
kind === "error"
|
|
43
|
+
? "bg-red-50 text-red-700 border-red-200"
|
|
44
|
+
: "bg-green-50 text-green-700 border-green-200";
|
|
45
|
+
return <p className={`mb-4 rounded-md border px-3 py-2 text-sm ${styles}`}>{children}</p>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function FooterLinks({ children }: { children: ReactNode }) {
|
|
49
|
+
return <div className="mt-6 space-y-1 text-center text-sm text-gray-500">{children}</div>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function TextLink({ href, children }: { href: string; children: ReactNode }) {
|
|
53
|
+
return (
|
|
54
|
+
<Link href={href} className="font-medium text-gray-900 underline-offset-2 hover:underline">
|
|
55
|
+
{children}
|
|
56
|
+
</Link>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser-side Better Auth client. Calls the `/api/auth/*` routes mounted from
|
|
5
|
+
* `src/server/auth.ts`. Use `authClient.signIn`, `signUp`, `signOut`, `useSession`.
|
|
6
|
+
*/
|
|
7
|
+
export const authClient = createAuthClient();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
3
|
+
import { db } from "./db";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Better Auth server instance, backed by Prisma. Email + password is enabled so
|
|
7
|
+
* the app authenticates with no external provider keys; add social providers,
|
|
8
|
+
* plugins, etc. here. The matching client lives in `src/lib/auth-client.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export const auth = betterAuth({
|
|
11
|
+
database: prismaAdapter(db, { provider: "postgresql" }),
|
|
12
|
+
emailAndPassword: { enabled: true },
|
|
13
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { clerkMiddleware } from "@clerk/nextjs/server";
|
|
2
|
+
|
|
3
|
+
// Clerk session middleware. Add `createRouteMatcher` + `auth.protect()` here to
|
|
4
|
+
// gate specific routes; by default every route is public and pages guard themselves.
|
|
5
|
+
export default clerkMiddleware();
|
|
6
|
+
|
|
7
|
+
export const config = {
|
|
8
|
+
matcher: [
|
|
9
|
+
// Skip Next.js internals and static files, unless found in search params.
|
|
10
|
+
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
|
11
|
+
// Always run for API routes.
|
|
12
|
+
"/(api|trpc)(.*)",
|
|
13
|
+
],
|
|
14
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { currentUser } from "@clerk/nextjs/server";
|
|
3
|
+
import { UserButton } from "@clerk/nextjs";
|
|
4
|
+
|
|
5
|
+
// Reading the session makes this route dynamic; redirect if signed out.
|
|
6
|
+
export default async function AccountPage() {
|
|
7
|
+
const user = await currentUser();
|
|
8
|
+
if (!user) redirect("/sign-in");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main className="mx-auto flex min-h-screen max-w-md flex-col justify-center gap-6 p-8">
|
|
12
|
+
<div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
13
|
+
<div className="mb-4 flex items-center justify-between">
|
|
14
|
+
<h1 className="text-2xl font-semibold tracking-tight">Your account</h1>
|
|
15
|
+
<UserButton />
|
|
16
|
+
</div>
|
|
17
|
+
<dl className="space-y-2 text-sm">
|
|
18
|
+
<div className="flex justify-between">
|
|
19
|
+
<dt className="text-gray-500">Name</dt>
|
|
20
|
+
<dd className="font-medium">{user.firstName ?? "—"}</dd>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="flex justify-between">
|
|
23
|
+
<dt className="text-gray-500">Email</dt>
|
|
24
|
+
<dd className="font-medium">{user.emailAddresses[0]?.emailAddress}</dd>
|
|
25
|
+
</div>
|
|
26
|
+
</dl>
|
|
27
|
+
</div>
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "./globals.css";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { ClerkProvider } from "@clerk/nextjs";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: "Stackr app",
|
|
8
|
+
description: "Generated by Stackr",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
12
|
+
return (
|
|
13
|
+
<ClerkProvider>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<body className="min-h-screen bg-gray-50 text-gray-900 antialiased">{children}</body>
|
|
16
|
+
</html>
|
|
17
|
+
</ClerkProvider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
|
|
3
|
+
|
|
4
|
+
export default function Home() {
|
|
5
|
+
return (
|
|
6
|
+
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8 text-center">
|
|
7
|
+
<h1 className="text-4xl font-bold tracking-tight">It works 🎉</h1>
|
|
8
|
+
<p className="text-gray-600">Authentication is wired with Clerk.</p>
|
|
9
|
+
|
|
10
|
+
<SignedOut>
|
|
11
|
+
<div className="flex gap-3">
|
|
12
|
+
<Link
|
|
13
|
+
href="/sign-in"
|
|
14
|
+
className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
15
|
+
>
|
|
16
|
+
Sign in
|
|
17
|
+
</Link>
|
|
18
|
+
<Link
|
|
19
|
+
href="/sign-up"
|
|
20
|
+
className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium transition hover:bg-gray-100"
|
|
21
|
+
>
|
|
22
|
+
Sign up
|
|
23
|
+
</Link>
|
|
24
|
+
</div>
|
|
25
|
+
</SignedOut>
|
|
26
|
+
|
|
27
|
+
<SignedIn>
|
|
28
|
+
<div className="flex items-center gap-3">
|
|
29
|
+
<Link
|
|
30
|
+
href="/account"
|
|
31
|
+
className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
32
|
+
>
|
|
33
|
+
Your account
|
|
34
|
+
</Link>
|
|
35
|
+
<UserButton />
|
|
36
|
+
</div>
|
|
37
|
+
</SignedIn>
|
|
38
|
+
</main>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { getSessionUser, getUserStats, SIGN_IN_PATH } from "@/server/account";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Protected dashboard. Re-checks the session server-side (safe even if the route
|
|
7
|
+
* isn't covered by middleware) and shows a few live stats. Auth + data come from
|
|
8
|
+
* the per-provider `@/server/account` adapter, so this page is identical for every
|
|
9
|
+
* authentication option.
|
|
10
|
+
*/
|
|
11
|
+
export default async function DashboardPage() {
|
|
12
|
+
const user = await getSessionUser();
|
|
13
|
+
if (!user) redirect(SIGN_IN_PATH);
|
|
14
|
+
|
|
15
|
+
const { users, sessions } = await getUserStats();
|
|
16
|
+
|
|
17
|
+
const stats: { label: string; value: string | number }[] = [
|
|
18
|
+
{ label: "Users", value: users },
|
|
19
|
+
{ label: "Active sessions", value: sessions ?? "—" },
|
|
20
|
+
{ label: "Your role", value: user.role },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<main className="mx-auto max-w-4xl p-8">
|
|
25
|
+
<header className="mb-8">
|
|
26
|
+
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
|
27
|
+
<p className="text-sm text-gray-500">Signed in as {user.name ?? user.email}</p>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<section className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
31
|
+
{stats.map((stat) => (
|
|
32
|
+
<div key={stat.label} className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
33
|
+
<dt className="text-sm text-gray-500">{stat.label}</dt>
|
|
34
|
+
<dd className="mt-2 text-3xl font-semibold tabular-nums">{stat.value}</dd>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</section>
|
|
38
|
+
|
|
39
|
+
<p className="mt-8 text-sm text-gray-500">
|
|
40
|
+
Edit this page in <code>src/app/dashboard/page.tsx</code>. Manage your{" "}
|
|
41
|
+
<Link href="/account" className="font-medium text-gray-900 underline-offset-2 hover:underline">
|
|
42
|
+
account
|
|
43
|
+
</Link>
|
|
44
|
+
.
|
|
45
|
+
</p>
|
|
46
|
+
</main>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
restart: unless-stopped
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: postgres
|
|
7
|
+
POSTGRES_PASSWORD: postgres
|
|
8
|
+
POSTGRES_DB: app
|
|
9
|
+
ports:
|
|
10
|
+
- "5432:5432"
|
|
11
|
+
volumes:
|
|
12
|
+
- postgres_data:/var/lib/postgresql/data
|
|
13
|
+
|
|
14
|
+
volumes:
|
|
15
|
+
postgres_data:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/mysql2";
|
|
2
|
+
import mysql from "mysql2/promise";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
|
|
5
|
+
// Reuse a single pool across hot reloads in development.
|
|
6
|
+
const globalForDb = globalThis as unknown as { pool?: mysql.Pool };
|
|
7
|
+
|
|
8
|
+
const pool = globalForDb.pool ?? mysql.createPool(process.env.DATABASE_URL!);
|
|
9
|
+
if (process.env.NODE_ENV !== "production") globalForDb.pool = pool;
|
|
10
|
+
|
|
11
|
+
export const db = drizzle(pool, { schema, mode: "default" });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { mysqlEnum, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
|
|
2
|
+
|
|
3
|
+
export const users = mysqlTable("users", {
|
|
4
|
+
id: varchar("id", { length: 255 }).primaryKey(),
|
|
5
|
+
name: varchar("name", { length: 255 }),
|
|
6
|
+
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
7
|
+
passwordHash: varchar("password_hash", { length: 255 }),
|
|
8
|
+
role: mysqlEnum("role", ["USER", "ADMIN"]).notNull().default("USER"),
|
|
9
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Issued for password resets (and reusable for email verification).
|
|
13
|
+
export const verificationTokens = mysqlTable("verification_tokens", {
|
|
14
|
+
identifier: varchar("identifier", { length: 255 }).notNull(),
|
|
15
|
+
token: varchar("token", { length: 255 }).notNull().unique(),
|
|
16
|
+
expires: timestamp("expires").notNull(),
|
|
17
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
|
|
5
|
+
// Reuse a single pool across hot reloads in development.
|
|
6
|
+
const globalForDb = globalThis as unknown as { pool?: Pool };
|
|
7
|
+
|
|
8
|
+
const pool = globalForDb.pool ?? new Pool({ connectionString: process.env.DATABASE_URL });
|
|
9
|
+
if (process.env.NODE_ENV !== "production") globalForDb.pool = pool;
|
|
10
|
+
|
|
11
|
+
export const db = drizzle(pool, { schema });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
/** Application roles, mirrored by the Auth.js `Role` type in `roles.ts`. */
|
|
4
|
+
export const roleEnum = pgEnum("role", ["USER", "ADMIN"]);
|
|
5
|
+
|
|
6
|
+
export const users = pgTable("users", {
|
|
7
|
+
id: text("id").primaryKey(),
|
|
8
|
+
name: text("name"),
|
|
9
|
+
email: text("email").notNull().unique(),
|
|
10
|
+
passwordHash: text("password_hash"),
|
|
11
|
+
role: roleEnum("role").notNull().default("USER"),
|
|
12
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Issued for password resets (and reusable for email verification).
|
|
16
|
+
export const verificationTokens = pgTable("verification_tokens", {
|
|
17
|
+
identifier: text("identifier").notNull(),
|
|
18
|
+
token: text("token").notNull().unique(),
|
|
19
|
+
expires: timestamp("expires").notNull(),
|
|
20
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
const features = [
|
|
4
|
+
{ title: "Batteries included", body: "Database, auth and UI wired together and ready to run." },
|
|
5
|
+
{ title: "Type-safe", body: "End-to-end TypeScript with strict mode on from the first commit." },
|
|
6
|
+
{ title: "Yours to shape", body: "Plain Next.js App Router — no magic, edit anything." },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
// Marketing landing page. The CTA points at /account, which your auth setup
|
|
10
|
+
// redirects to the right sign-in page; adjust the links to taste.
|
|
11
|
+
export default function Home() {
|
|
12
|
+
return (
|
|
13
|
+
<main className="flex min-h-screen flex-col">
|
|
14
|
+
<section className="mx-auto flex max-w-3xl flex-1 flex-col items-center justify-center gap-6 px-6 py-24 text-center">
|
|
15
|
+
<span className="rounded-full border border-gray-300 px-3 py-1 text-xs font-medium text-gray-600">
|
|
16
|
+
Built with Stackr
|
|
17
|
+
</span>
|
|
18
|
+
<h1 className="text-5xl font-bold tracking-tight sm:text-6xl">
|
|
19
|
+
Ship your idea, faster.
|
|
20
|
+
</h1>
|
|
21
|
+
<p className="max-w-xl text-lg text-gray-600">
|
|
22
|
+
A runnable full-stack Next.js starter with the boring parts already done — so you can
|
|
23
|
+
build the part that matters.
|
|
24
|
+
</p>
|
|
25
|
+
<div className="flex gap-3">
|
|
26
|
+
<Link
|
|
27
|
+
href="/account"
|
|
28
|
+
className="rounded-md bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
29
|
+
>
|
|
30
|
+
Get started
|
|
31
|
+
</Link>
|
|
32
|
+
<a
|
|
33
|
+
href="#features"
|
|
34
|
+
className="rounded-md border border-gray-300 px-5 py-2.5 text-sm font-medium transition hover:bg-gray-100"
|
|
35
|
+
>
|
|
36
|
+
Learn more
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section id="features" className="border-t border-gray-200 bg-white">
|
|
42
|
+
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 px-6 py-16 sm:grid-cols-3">
|
|
43
|
+
{features.map((feature) => (
|
|
44
|
+
<div key={feature.title} className="rounded-xl border border-gray-200 p-6">
|
|
45
|
+
<h2 className="mb-2 text-lg font-semibold">{feature.title}</h2>
|
|
46
|
+
<p className="text-sm text-gray-600">{feature.body}</p>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
const uri = process.env.DATABASE_URL ?? "mongodb://localhost:27017/app";
|
|
4
|
+
|
|
5
|
+
// Cache the connection promise across hot reloads so we open a single connection.
|
|
6
|
+
const globalForMongoose = globalThis as unknown as { conn?: Promise<typeof mongoose> };
|
|
7
|
+
|
|
8
|
+
/** Ensure a single Mongoose connection; call before any query (e.g. in actions). */
|
|
9
|
+
export function dbConnect(): Promise<typeof mongoose> {
|
|
10
|
+
globalForMongoose.conn ??= mongoose.connect(uri);
|
|
11
|
+
return globalForMongoose.conn;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Schema, model, models, type InferSchemaType } from "mongoose";
|
|
2
|
+
|
|
3
|
+
const userSchema = new Schema(
|
|
4
|
+
{
|
|
5
|
+
name: { type: String },
|
|
6
|
+
email: { type: String, required: true, unique: true },
|
|
7
|
+
passwordHash: { type: String },
|
|
8
|
+
role: { type: String, enum: ["USER", "ADMIN"], default: "USER" },
|
|
9
|
+
},
|
|
10
|
+
{ timestamps: true },
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export type UserDoc = InferSchemaType<typeof userSchema>;
|
|
14
|
+
|
|
15
|
+
// `models.X ?? model(...)` avoids "OverwriteModelError" on hot reload.
|
|
16
|
+
export const User = models.User ?? model("User", userSchema);
|
|
17
|
+
|
|
18
|
+
const verificationTokenSchema = new Schema({
|
|
19
|
+
identifier: { type: String, required: true },
|
|
20
|
+
token: { type: String, required: true, unique: true },
|
|
21
|
+
expires: { type: Date, required: true },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const VerificationToken =
|
|
25
|
+
models.VerificationToken ?? model("VerificationToken", verificationTokenSchema);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import NextAuth from "next-auth";
|
|
2
|
+
import { authConfig } from "@/server/auth.config";
|
|
3
|
+
|
|
4
|
+
// Edge-safe auth middleware. Uses the JWT session to gate routes via the
|
|
5
|
+
// `authorized` callback in auth.config.ts.
|
|
6
|
+
export const { auth: middleware } = NextAuth(authConfig);
|
|
7
|
+
|
|
8
|
+
export const config = {
|
|
9
|
+
// Run on everything except static assets and the auth API itself.
|
|
10
|
+
matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
|
|
11
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { NextAuthConfig } from "next-auth";
|
|
2
|
+
import type { Role } from "./roles";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Edge-safe Auth.js configuration. This file must NOT import the database or
|
|
6
|
+
* bcrypt so it can run in the middleware (edge) runtime. The Credentials
|
|
7
|
+
* provider and Prisma adapter live in `auth.ts` (Node runtime).
|
|
8
|
+
*/
|
|
9
|
+
export const authConfig = {
|
|
10
|
+
pages: {
|
|
11
|
+
signIn: "/login",
|
|
12
|
+
},
|
|
13
|
+
// Providers are added in auth.ts; the middleware only needs to read the JWT.
|
|
14
|
+
providers: [],
|
|
15
|
+
session: { strategy: "jwt" },
|
|
16
|
+
callbacks: {
|
|
17
|
+
/** Route guard evaluated by the middleware. */
|
|
18
|
+
authorized({ auth, request: { nextUrl } }) {
|
|
19
|
+
const isLoggedIn = !!auth?.user;
|
|
20
|
+
const isProtected = nextUrl.pathname.startsWith("/account");
|
|
21
|
+
if (isProtected) return isLoggedIn;
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
jwt({ token, user }) {
|
|
25
|
+
if (user) {
|
|
26
|
+
token.id = user.id;
|
|
27
|
+
token.role = (user as { role?: Role }).role;
|
|
28
|
+
}
|
|
29
|
+
return token;
|
|
30
|
+
},
|
|
31
|
+
session({ session, token }) {
|
|
32
|
+
if (session.user) {
|
|
33
|
+
if (typeof token.id === "string") session.user.id = token.id;
|
|
34
|
+
session.user.role = token.role as Role | undefined;
|
|
35
|
+
}
|
|
36
|
+
return session;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} satisfies NextAuthConfig;
|