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.
Files changed (113) hide show
  1. package/README.md +154 -0
  2. package/dist/index.js +1135 -0
  3. package/package.json +62 -0
  4. package/template/architectures/microservices/_gitignore +26 -0
  5. package/template/architectures/microservices/apps/web/Dockerfile +11 -0
  6. package/template/architectures/microservices/apps/web/next-env.d.ts +5 -0
  7. package/template/architectures/microservices/apps/web/next.config.mjs +4 -0
  8. package/template/architectures/microservices/apps/web/package.json +25 -0
  9. package/template/architectures/microservices/apps/web/src/app/account/page.tsx +56 -0
  10. package/template/architectures/microservices/apps/web/src/app/globals.css +3 -0
  11. package/template/architectures/microservices/apps/web/src/app/layout.tsx +16 -0
  12. package/template/architectures/microservices/apps/web/src/app/login/page.tsx +70 -0
  13. package/template/architectures/microservices/apps/web/src/app/page.tsx +33 -0
  14. package/template/architectures/microservices/apps/web/src/app/register/page.tsx +71 -0
  15. package/template/architectures/microservices/apps/web/src/lib/api.ts +15 -0
  16. package/template/architectures/microservices/apps/web/tsconfig.json +23 -0
  17. package/template/architectures/microservices/docker-compose.yml +45 -0
  18. package/template/architectures/microservices/gateway/nginx.conf +20 -0
  19. package/template/architectures/microservices/package.json +21 -0
  20. package/template/architectures/microservices/services/api/Dockerfile +11 -0
  21. package/template/architectures/microservices/services/api/package.json +34 -0
  22. package/template/architectures/microservices/services/api/prisma/schema.prisma +17 -0
  23. package/template/architectures/microservices/services/api/prisma.config.ts +17 -0
  24. package/template/architectures/microservices/services/api/src/auth.ts +22 -0
  25. package/template/architectures/microservices/services/api/src/db.ts +12 -0
  26. package/template/architectures/microservices/services/api/src/index.ts +22 -0
  27. package/template/architectures/microservices/services/api/src/routes/auth.ts +73 -0
  28. package/template/architectures/microservices/services/api/tsconfig.json +16 -0
  29. package/template/base/_eslintrc.json +3 -0
  30. package/template/base/_gitignore +24 -0
  31. package/template/base/_prettierrc.json +6 -0
  32. package/template/base/next-env.d.ts +5 -0
  33. package/template/base/next.config.mjs +4 -0
  34. package/template/base/package.json +27 -0
  35. package/template/base/src/app/globals.css +14 -0
  36. package/template/base/src/app/layout.tsx +16 -0
  37. package/template/base/src/app/page.tsx +10 -0
  38. package/template/base/tsconfig.json +23 -0
  39. package/template/extras/account-better-auth/src/server/account.ts +52 -0
  40. package/template/extras/account-clerk/src/server/account.ts +64 -0
  41. package/template/extras/account-nextauth/src/server/account.ts +59 -0
  42. package/template/extras/account-supabase/src/lib/supabase/admin.ts +14 -0
  43. package/template/extras/account-supabase/src/server/account.ts +65 -0
  44. package/template/extras/admin/src/app/admin/page.tsx +55 -0
  45. package/template/extras/auth-pages/src/app/account/page.tsx +37 -0
  46. package/template/extras/auth-pages/src/app/forgot-password/page.tsx +33 -0
  47. package/template/extras/auth-pages/src/app/login/page.tsx +32 -0
  48. package/template/extras/auth-pages/src/app/page.tsx +46 -0
  49. package/template/extras/auth-pages/src/app/register/page.tsx +36 -0
  50. package/template/extras/auth-pages/src/app/reset-password/page.tsx +49 -0
  51. package/template/extras/auth-pages/src/components/auth/ui.tsx +57 -0
  52. package/template/extras/auth-pages-drizzle/src/server/actions.ts +122 -0
  53. package/template/extras/auth-pages-mongoose/src/server/actions.ts +121 -0
  54. package/template/extras/auth-pages-prisma/src/server/actions.ts +117 -0
  55. package/template/extras/better-auth/prisma/schema.prisma +70 -0
  56. package/template/extras/better-auth/src/app/account/page.tsx +42 -0
  57. package/template/extras/better-auth/src/app/api/auth/[...all]/route.ts +5 -0
  58. package/template/extras/better-auth/src/app/login/page.tsx +42 -0
  59. package/template/extras/better-auth/src/app/register/page.tsx +44 -0
  60. package/template/extras/better-auth/src/components/auth/ui.tsx +58 -0
  61. package/template/extras/better-auth/src/lib/auth-client.ts +7 -0
  62. package/template/extras/better-auth/src/server/auth.ts +13 -0
  63. package/template/extras/clerk/middleware.ts +14 -0
  64. package/template/extras/clerk/src/app/account/page.tsx +30 -0
  65. package/template/extras/clerk/src/app/layout.tsx +19 -0
  66. package/template/extras/clerk/src/app/page.tsx +40 -0
  67. package/template/extras/clerk/src/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  68. package/template/extras/clerk/src/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  69. package/template/extras/dashboard/src/app/dashboard/page.tsx +48 -0
  70. package/template/extras/databases/mongodb/docker-compose.yml +13 -0
  71. package/template/extras/databases/mysql/docker-compose.yml +14 -0
  72. package/template/extras/databases/postgres/docker-compose.yml +15 -0
  73. package/template/extras/drizzle/mysql/drizzle.config.ts +9 -0
  74. package/template/extras/drizzle/mysql/src/server/db.ts +11 -0
  75. package/template/extras/drizzle/mysql/src/server/schema.ts +17 -0
  76. package/template/extras/drizzle/postgres/drizzle.config.ts +9 -0
  77. package/template/extras/drizzle/postgres/src/server/db.ts +11 -0
  78. package/template/extras/drizzle/postgres/src/server/schema.ts +20 -0
  79. package/template/extras/landing/src/app/page.tsx +53 -0
  80. package/template/extras/mongoose/src/server/db.ts +12 -0
  81. package/template/extras/mongoose/src/server/models.ts +25 -0
  82. package/template/extras/nextauth/middleware.ts +11 -0
  83. package/template/extras/nextauth/src/app/api/auth/[...nextauth]/route.ts +3 -0
  84. package/template/extras/nextauth/src/server/auth.config.ts +39 -0
  85. package/template/extras/nextauth/src/server/roles.ts +5 -0
  86. package/template/extras/nextauth/src/types/next-auth.d.ts +22 -0
  87. package/template/extras/nextauth-drizzle/src/server/auth.ts +44 -0
  88. package/template/extras/nextauth-mongoose/src/server/auth.ts +42 -0
  89. package/template/extras/nextauth-prisma/src/server/auth.ts +43 -0
  90. package/template/extras/prisma/base/prisma/schema.prisma +67 -0
  91. package/template/extras/prisma/base/prisma.config.ts +10 -0
  92. package/template/extras/prisma/mysql/src/server/db.ts +25 -0
  93. package/template/extras/prisma/postgres/src/server/db.ts +18 -0
  94. package/template/extras/shadcn/components.json +20 -0
  95. package/template/extras/shadcn/postcss.config.mjs +7 -0
  96. package/template/extras/shadcn/src/app/globals.css +59 -0
  97. package/template/extras/shadcn/src/components/ui/button.tsx +42 -0
  98. package/template/extras/shadcn/src/components/ui/card.tsx +54 -0
  99. package/template/extras/shadcn/src/components/ui/input.tsx +19 -0
  100. package/template/extras/shadcn/src/lib/utils.ts +7 -0
  101. package/template/extras/shadcn/tailwind.config.ts +71 -0
  102. package/template/extras/supabase/middleware.ts +13 -0
  103. package/template/extras/supabase/src/app/account/page.tsx +32 -0
  104. package/template/extras/supabase/src/app/login/page.tsx +47 -0
  105. package/template/extras/supabase/src/app/register/page.tsx +52 -0
  106. package/template/extras/supabase/src/components/auth/sign-out-button.tsx +25 -0
  107. package/template/extras/supabase/src/components/auth/ui.tsx +58 -0
  108. package/template/extras/supabase/src/lib/supabase/client.ts +9 -0
  109. package/template/extras/supabase/src/lib/supabase/middleware.ts +34 -0
  110. package/template/extras/supabase/src/lib/supabase/server.ts +29 -0
  111. package/template/extras/tailwind/postcss.config.mjs +7 -0
  112. package/template/extras/tailwind/src/app/globals.css +3 -0
  113. package/template/extras/tailwind/tailwind.config.ts +9 -0
@@ -0,0 +1,22 @@
1
+ import type { DefaultSession } from "next-auth";
2
+ import type { Role } from "@/server/roles";
3
+
4
+ declare module "next-auth" {
5
+ interface User {
6
+ role?: Role;
7
+ }
8
+
9
+ interface Session {
10
+ user: {
11
+ id: string;
12
+ role?: Role;
13
+ } & DefaultSession["user"];
14
+ }
15
+ }
16
+
17
+ declare module "next-auth/jwt" {
18
+ interface JWT {
19
+ id?: string;
20
+ role?: Role;
21
+ }
22
+ }
@@ -0,0 +1,44 @@
1
+ import NextAuth from "next-auth";
2
+ import Credentials from "next-auth/providers/credentials";
3
+ import bcrypt from "bcryptjs";
4
+ import { eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+ import { db } from "./db";
7
+ import { users } from "./schema";
8
+ import { authConfig } from "./auth.config";
9
+
10
+ const credentialsSchema = z.object({
11
+ email: z.string().email(),
12
+ password: z.string().min(1),
13
+ });
14
+
15
+ /**
16
+ * Auth.js setup for the Drizzle stack. Uses a JWT session strategy (no database
17
+ * adapter needed) with a Credentials provider that looks the user up in the
18
+ * Drizzle `users` table. Add OAuth providers here; switch to the Drizzle adapter
19
+ * if you want database sessions.
20
+ */
21
+ export const { handlers, auth, signIn, signOut } = NextAuth({
22
+ ...authConfig,
23
+ providers: [
24
+ Credentials({
25
+ credentials: {
26
+ email: { label: "Email", type: "email" },
27
+ password: { label: "Password", type: "password" },
28
+ },
29
+ async authorize(raw) {
30
+ const parsed = credentialsSchema.safeParse(raw);
31
+ if (!parsed.success) return null;
32
+
33
+ const { email, password } = parsed.data;
34
+ const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1);
35
+ if (!user?.passwordHash) return null;
36
+
37
+ const valid = await bcrypt.compare(password, user.passwordHash);
38
+ if (!valid) return null;
39
+
40
+ return { id: user.id, email: user.email, name: user.name, role: user.role };
41
+ },
42
+ }),
43
+ ],
44
+ });
@@ -0,0 +1,42 @@
1
+ import NextAuth from "next-auth";
2
+ import Credentials from "next-auth/providers/credentials";
3
+ import bcrypt from "bcryptjs";
4
+ import { z } from "zod";
5
+ import { dbConnect } from "./db";
6
+ import { User } from "./models";
7
+ import { authConfig } from "./auth.config";
8
+
9
+ const credentialsSchema = z.object({
10
+ email: z.string().email(),
11
+ password: z.string().min(1),
12
+ });
13
+
14
+ /**
15
+ * Auth.js setup for the Mongoose stack. JWT session strategy (no database
16
+ * adapter) with a Credentials provider that looks the user up in MongoDB via
17
+ * Mongoose. Add OAuth providers here as needed.
18
+ */
19
+ export const { handlers, auth, signIn, signOut } = NextAuth({
20
+ ...authConfig,
21
+ providers: [
22
+ Credentials({
23
+ credentials: {
24
+ email: { label: "Email", type: "email" },
25
+ password: { label: "Password", type: "password" },
26
+ },
27
+ async authorize(raw) {
28
+ const parsed = credentialsSchema.safeParse(raw);
29
+ if (!parsed.success) return null;
30
+
31
+ await dbConnect();
32
+ const user = await User.findOne({ email: parsed.data.email });
33
+ if (!user?.passwordHash) return null;
34
+
35
+ const valid = await bcrypt.compare(parsed.data.password, user.passwordHash);
36
+ if (!valid) return null;
37
+
38
+ return { id: String(user._id), email: user.email, name: user.name, role: user.role };
39
+ },
40
+ }),
41
+ ],
42
+ });
@@ -0,0 +1,43 @@
1
+ import NextAuth from "next-auth";
2
+ import Credentials from "next-auth/providers/credentials";
3
+ import { PrismaAdapter } from "@auth/prisma-adapter";
4
+ import bcrypt from "bcryptjs";
5
+ import { z } from "zod";
6
+ import { db } from "./db";
7
+ import { authConfig } from "./auth.config";
8
+
9
+ const credentialsSchema = z.object({
10
+ email: z.string().email(),
11
+ password: z.string().min(1),
12
+ });
13
+
14
+ /**
15
+ * Full Auth.js setup (Node runtime). Uses the Prisma adapter for user storage
16
+ * and a Credentials provider (email + bcrypt password) so the app authenticates
17
+ * with no external OAuth keys. Add OAuth providers (Discord, GitHub, ...) here.
18
+ */
19
+ export const { handlers, auth, signIn, signOut } = NextAuth({
20
+ ...authConfig,
21
+ adapter: PrismaAdapter(db),
22
+ providers: [
23
+ Credentials({
24
+ credentials: {
25
+ email: { label: "Email", type: "email" },
26
+ password: { label: "Password", type: "password" },
27
+ },
28
+ async authorize(raw) {
29
+ const parsed = credentialsSchema.safeParse(raw);
30
+ if (!parsed.success) return null;
31
+
32
+ const { email, password } = parsed.data;
33
+ const user = await db.user.findUnique({ where: { email } });
34
+ if (!user?.passwordHash) return null;
35
+
36
+ const valid = await bcrypt.compare(password, user.passwordHash);
37
+ if (!valid) return null;
38
+
39
+ return { id: user.id, email: user.email, name: user.name, role: user.role };
40
+ },
41
+ }),
42
+ ],
43
+ });
@@ -0,0 +1,67 @@
1
+ // Prisma schema generated by Stackr.
2
+ // Models include the NextAuth (Auth.js) adapter tables plus a password hash and
3
+ // role on User to support the Credentials provider and role-based access.
4
+
5
+ generator client {
6
+ provider = "prisma-client"
7
+ output = "../src/generated/prisma"
8
+ }
9
+
10
+ datasource db {
11
+ provider = "postgresql"
12
+ }
13
+
14
+ enum Role {
15
+ USER
16
+ ADMIN
17
+ }
18
+
19
+ model User {
20
+ id String @id @default(cuid())
21
+ name String?
22
+ email String? @unique
23
+ emailVerified DateTime?
24
+ image String?
25
+ passwordHash String?
26
+ role Role @default(USER)
27
+ accounts Account[]
28
+ sessions Session[]
29
+ createdAt DateTime @default(now())
30
+ updatedAt DateTime @updatedAt
31
+ }
32
+
33
+ model Account {
34
+ id String @id @default(cuid())
35
+ userId String
36
+ type String
37
+ provider String
38
+ providerAccountId String
39
+ refresh_token String? @db.Text
40
+ access_token String? @db.Text
41
+ expires_at Int?
42
+ token_type String?
43
+ scope String?
44
+ id_token String? @db.Text
45
+ session_state String?
46
+
47
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
48
+
49
+ @@unique([provider, providerAccountId])
50
+ }
51
+
52
+ model Session {
53
+ id String @id @default(cuid())
54
+ sessionToken String @unique
55
+ userId String
56
+ expires DateTime
57
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
58
+ }
59
+
60
+ // Used by Auth.js for email verification and reused for password-reset tokens.
61
+ model VerificationToken {
62
+ identifier String
63
+ token String @unique
64
+ expires DateTime
65
+
66
+ @@unique([identifier, token])
67
+ }
@@ -0,0 +1,10 @@
1
+ import "dotenv/config";
2
+ import { defineConfig, env } from "prisma/config";
3
+
4
+ // Prisma 7 moved connection config out of the schema. The CLI (migrate, studio)
5
+ // reads the database URL from here; the runtime client uses a driver adapter.
6
+ export default defineConfig({
7
+ schema: "prisma/schema.prisma",
8
+ migrations: { path: "prisma/migrations" },
9
+ datasource: { url: env("DATABASE_URL") },
10
+ });
@@ -0,0 +1,25 @@
1
+ import { PrismaClient } from "@/generated/prisma/client";
2
+ import { PrismaMariaDb } from "@prisma/adapter-mariadb";
3
+
4
+ // Reuse a single PrismaClient across hot reloads in development. Prisma 7 connects
5
+ // through a driver adapter; we parse DATABASE_URL into connection fields so the
6
+ // `mysql://` URL works regardless of the adapter's URL-scheme expectations.
7
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
8
+
9
+ const url = new URL(process.env.DATABASE_URL ?? "mysql://root:root@localhost:3306/app");
10
+ const adapter = new PrismaMariaDb({
11
+ host: url.hostname,
12
+ port: Number(url.port || 3306),
13
+ user: decodeURIComponent(url.username),
14
+ password: decodeURIComponent(url.password),
15
+ database: url.pathname.replace(/^\//, ""),
16
+ });
17
+
18
+ export const db =
19
+ globalForPrisma.prisma ??
20
+ new PrismaClient({
21
+ adapter,
22
+ log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
23
+ });
24
+
25
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
@@ -0,0 +1,18 @@
1
+ import { PrismaClient } from "@/generated/prisma/client";
2
+ import { PrismaPg } from "@prisma/adapter-pg";
3
+
4
+ // Reuse a single PrismaClient across hot reloads in development to avoid
5
+ // exhausting the database connection pool. Prisma 7 connects through a driver
6
+ // adapter (the connection string lives in `.env`, not the schema).
7
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
8
+
9
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
10
+
11
+ export const db =
12
+ globalForPrisma.prisma ??
13
+ new PrismaClient({
14
+ adapter,
15
+ log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
16
+ });
17
+
18
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ export default {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ autoprefixer: {},
6
+ },
7
+ };
@@ -0,0 +1,59 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 222.2 47.4% 11.2%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 222.2 84% 4.9%;
26
+ --radius: 0.5rem;
27
+ }
28
+
29
+ .dark {
30
+ --background: 222.2 84% 4.9%;
31
+ --foreground: 210 40% 98%;
32
+ --card: 222.2 84% 4.9%;
33
+ --card-foreground: 210 40% 98%;
34
+ --popover: 222.2 84% 4.9%;
35
+ --popover-foreground: 210 40% 98%;
36
+ --primary: 210 40% 98%;
37
+ --primary-foreground: 222.2 47.4% 11.2%;
38
+ --secondary: 217.2 32.6% 17.5%;
39
+ --secondary-foreground: 210 40% 98%;
40
+ --muted: 217.2 32.6% 17.5%;
41
+ --muted-foreground: 215 20.2% 65.1%;
42
+ --accent: 217.2 32.6% 17.5%;
43
+ --accent-foreground: 210 40% 98%;
44
+ --destructive: 0 62.8% 30.6%;
45
+ --destructive-foreground: 210 40% 98%;
46
+ --border: 217.2 32.6% 17.5%;
47
+ --input: 217.2 32.6% 17.5%;
48
+ --ring: 212.7 26.8% 83.9%;
49
+ }
50
+ }
51
+
52
+ @layer base {
53
+ * {
54
+ @apply border-border;
55
+ }
56
+ body {
57
+ @apply bg-background text-foreground;
58
+ }
59
+ }
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
12
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
13
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ ghost: "hover:bg-accent hover:text-accent-foreground",
15
+ link: "text-primary underline-offset-4 hover:underline",
16
+ },
17
+ size: {
18
+ default: "h-10 px-4 py-2",
19
+ sm: "h-9 rounded-md px-3",
20
+ lg: "h-11 rounded-md px-8",
21
+ icon: "h-10 w-10",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ size: "default",
27
+ },
28
+ },
29
+ );
30
+
31
+ export interface ButtonProps
32
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
33
+ VariantProps<typeof buttonVariants> {}
34
+
35
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
36
+ ({ className, variant, size, ...props }, ref) => (
37
+ <button ref={ref} className={cn(buttonVariants({ variant, size, className }))} {...props} />
38
+ ),
39
+ );
40
+ Button.displayName = "Button";
41
+
42
+ export { Button, buttonVariants };
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
9
+ {...props}
10
+ />
11
+ ),
12
+ );
13
+ Card.displayName = "Card";
14
+
15
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
16
+ ({ className, ...props }, ref) => (
17
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
18
+ ),
19
+ );
20
+ CardHeader.displayName = "CardHeader";
21
+
22
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
23
+ ({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
27
+ {...props}
28
+ />
29
+ ),
30
+ );
31
+ CardTitle.displayName = "CardTitle";
32
+
33
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
34
+ ({ className, ...props }, ref) => (
35
+ <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
36
+ ),
37
+ );
38
+ CardDescription.displayName = "CardDescription";
39
+
40
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
41
+ ({ className, ...props }, ref) => (
42
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
43
+ ),
44
+ );
45
+ CardContent.displayName = "CardContent";
46
+
47
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
48
+ ({ className, ...props }, ref) => (
49
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
50
+ ),
51
+ );
52
+ CardFooter.displayName = "CardFooter";
53
+
54
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, type, ...props }, ref) => (
6
+ <input
7
+ type={type}
8
+ ref={ref}
9
+ className={cn(
10
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ ),
16
+ );
17
+ Input.displayName = "Input";
18
+
19
+ export { Input };
@@ -0,0 +1,7 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /** Merge Tailwind class names, resolving conflicts (shadcn/ui convention). */
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
@@ -0,0 +1,71 @@
1
+ import type { Config } from "tailwindcss";
2
+ import animate from "tailwindcss-animate";
3
+
4
+ export default {
5
+ darkMode: ["class"],
6
+ content: ["./src/**/*.{ts,tsx,js,jsx,mdx}"],
7
+ theme: {
8
+ container: {
9
+ center: true,
10
+ padding: "2rem",
11
+ screens: { "2xl": "1400px" },
12
+ },
13
+ extend: {
14
+ colors: {
15
+ border: "hsl(var(--border))",
16
+ input: "hsl(var(--input))",
17
+ ring: "hsl(var(--ring))",
18
+ background: "hsl(var(--background))",
19
+ foreground: "hsl(var(--foreground))",
20
+ primary: {
21
+ DEFAULT: "hsl(var(--primary))",
22
+ foreground: "hsl(var(--primary-foreground))",
23
+ },
24
+ secondary: {
25
+ DEFAULT: "hsl(var(--secondary))",
26
+ foreground: "hsl(var(--secondary-foreground))",
27
+ },
28
+ destructive: {
29
+ DEFAULT: "hsl(var(--destructive))",
30
+ foreground: "hsl(var(--destructive-foreground))",
31
+ },
32
+ muted: {
33
+ DEFAULT: "hsl(var(--muted))",
34
+ foreground: "hsl(var(--muted-foreground))",
35
+ },
36
+ accent: {
37
+ DEFAULT: "hsl(var(--accent))",
38
+ foreground: "hsl(var(--accent-foreground))",
39
+ },
40
+ popover: {
41
+ DEFAULT: "hsl(var(--popover))",
42
+ foreground: "hsl(var(--popover-foreground))",
43
+ },
44
+ card: {
45
+ DEFAULT: "hsl(var(--card))",
46
+ foreground: "hsl(var(--card-foreground))",
47
+ },
48
+ },
49
+ borderRadius: {
50
+ lg: "var(--radius)",
51
+ md: "calc(var(--radius) - 2px)",
52
+ sm: "calc(var(--radius) - 4px)",
53
+ },
54
+ keyframes: {
55
+ "accordion-down": {
56
+ from: { height: "0" },
57
+ to: { height: "var(--radix-accordion-content-height)" },
58
+ },
59
+ "accordion-up": {
60
+ from: { height: "var(--radix-accordion-content-height)" },
61
+ to: { height: "0" },
62
+ },
63
+ },
64
+ animation: {
65
+ "accordion-down": "accordion-down 0.2s ease-out",
66
+ "accordion-up": "accordion-up 0.2s ease-out",
67
+ },
68
+ },
69
+ },
70
+ plugins: [animate],
71
+ } satisfies Config;
@@ -0,0 +1,13 @@
1
+ import { type NextRequest } from "next/server";
2
+ import { updateSession } from "@/lib/supabase/middleware";
3
+
4
+ export async function middleware(request: NextRequest) {
5
+ return await updateSession(request);
6
+ }
7
+
8
+ export const config = {
9
+ matcher: [
10
+ // Run on everything except static assets and image files.
11
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
12
+ ],
13
+ };
@@ -0,0 +1,32 @@
1
+ import { redirect } from "next/navigation";
2
+ import { createClient } from "@/lib/supabase/server";
3
+ import { SignOutButton } from "@/components/auth/sign-out-button";
4
+
5
+ export default async function AccountPage() {
6
+ const supabase = await createClient();
7
+ const {
8
+ data: { user },
9
+ } = await supabase.auth.getUser();
10
+ if (!user) redirect("/login");
11
+
12
+ return (
13
+ <main className="mx-auto flex min-h-screen max-w-md flex-col justify-center gap-6 p-8">
14
+ <div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
15
+ <h1 className="mb-4 text-2xl font-semibold tracking-tight">Your account</h1>
16
+ <dl className="space-y-2 text-sm">
17
+ <div className="flex justify-between">
18
+ <dt className="text-gray-500">Email</dt>
19
+ <dd className="font-medium">{user.email}</dd>
20
+ </div>
21
+ <div className="flex justify-between">
22
+ <dt className="text-gray-500">User ID</dt>
23
+ <dd className="font-mono text-xs">{user.id}</dd>
24
+ </div>
25
+ </dl>
26
+ <div className="mt-6">
27
+ <SignOutButton />
28
+ </div>
29
+ </div>
30
+ </main>
31
+ );
32
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useState, type FormEvent } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { createClient } from "@/lib/supabase/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 supabase = createClient();
19
+ const { error } = await supabase.auth.signInWithPassword({
20
+ email: String(data.get("email")),
21
+ password: String(data.get("password")),
22
+ });
23
+ setPending(false);
24
+ if (error) {
25
+ setError(error.message);
26
+ } else {
27
+ router.push("/account");
28
+ router.refresh();
29
+ }
30
+ }
31
+
32
+ return (
33
+ <AuthShell title="Sign in">
34
+ {error && <Alert kind="error">{error}</Alert>}
35
+ <form onSubmit={onSubmit}>
36
+ <Field label="Email" name="email" type="email" required autoComplete="email" />
37
+ <Field label="Password" name="password" type="password" required autoComplete="current-password" />
38
+ <SubmitButton disabled={pending}>{pending ? "Signing in…" : "Sign in"}</SubmitButton>
39
+ </form>
40
+ <FooterLinks>
41
+ <p>
42
+ No account? <TextLink href="/register">Create one</TextLink>
43
+ </p>
44
+ </FooterLinks>
45
+ </AuthShell>
46
+ );
47
+ }