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,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,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,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
|
+
}
|