@shipindays/shipindays 0.1.2 → 0.1.3
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.
Potentially problematic release.
This version of @shipindays/shipindays might be problematic. Click here for more details.
- package/README.md +1 -1
- package/index.js +293 -241
- package/package.json +1 -27
- package/templates/blocks/auth/nextauth/package.json +8 -8
- package/templates/blocks/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts +19 -0
- package/templates/blocks/auth/nextauth/src/app/api/auth/login/route.ts +40 -0
- package/templates/blocks/auth/nextauth/src/app/api/auth/logout/route.ts +12 -0
- package/templates/blocks/auth/nextauth/src/app/api/auth/signup/route.ts +61 -0
- package/templates/blocks/auth/nextauth/src/app/layout.tsx +35 -0
- package/templates/blocks/auth/nextauth/src/app/types/next-auth.d.ts +18 -0
- package/templates/blocks/auth/nextauth/src/lib/auth/index.ts +141 -0
- package/templates/blocks/auth/nextauth/src/middleware.ts +40 -0
- package/templates/blocks/auth/nextauth/src/types/next-auth.d.ts +0 -0
- package/templates/blocks/auth/supabase/package.json +5 -5
- package/templates/blocks/auth/supabase/src/app/api/auth/login/route.ts +28 -0
- package/templates/blocks/auth/supabase/src/app/api/auth/logout/route.ts +13 -0
- package/templates/blocks/auth/supabase/src/app/api/auth/signup/route.ts +42 -0
- package/templates/blocks/auth/supabase/src/lib/auth/index.ts +31 -54
- package/templates/blocks/auth/supabase/src/lib/supabase/client.ts +30 -0
- package/templates/blocks/auth/supabase/src/lib/supabase/server.ts +40 -0
- package/templates/blocks/auth/supabase/src/middleware.ts +27 -14
- package/templates/blocks/email/resend/{index.ts → src/lib/resend/index.ts} +27 -15
- package/templates/blocks/auth/nextauth/app/api/auth/[...nextauth]/route.ts +0 -19
- package/templates/blocks/auth/nextauth/lib/auth/index.ts +0 -98
- package/templates/blocks/auth/nextauth/middleware.ts +0 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipindays/shipindays",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Scaffold a production ready Next.js SaaS in seconds. Pick your stack, get auth + payments + email wired up.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,23 +27,6 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "https://github.com/nikhilsaiankilla/shipindays"
|
|
29
29
|
},
|
|
30
|
-
"lint-staged": {
|
|
31
|
-
"*.js": [
|
|
32
|
-
"eslint --fix",
|
|
33
|
-
"prettier --write"
|
|
34
|
-
],
|
|
35
|
-
"*.ts": [
|
|
36
|
-
"eslint --fix",
|
|
37
|
-
"prettier --write"
|
|
38
|
-
],
|
|
39
|
-
"*.tsx": [
|
|
40
|
-
"eslint --fix",
|
|
41
|
-
"prettier --write"
|
|
42
|
-
],
|
|
43
|
-
"*.json": [
|
|
44
|
-
"prettier --write"
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
30
|
"dependencies": {
|
|
48
31
|
"@clack/prompts": "^0.9.1",
|
|
49
32
|
"chalk": "^5.3.0",
|
|
@@ -51,14 +34,5 @@
|
|
|
51
34
|
},
|
|
52
35
|
"engines": {
|
|
53
36
|
"node": ">=18.0.0"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@commitlint/cli": "^20.5.0",
|
|
57
|
-
"@commitlint/config-conventional": "^20.5.0",
|
|
58
|
-
"husky": "^9.1.7",
|
|
59
|
-
"lint-staged": "^16.4.0"
|
|
60
|
-
},
|
|
61
|
-
"scripts": {
|
|
62
|
-
"prepare": "husky || true"
|
|
63
37
|
}
|
|
64
38
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"next-auth": "^5.0.0-beta.22",
|
|
4
|
+
"bcryptjs": "^2.4.3"
|
|
5
|
+
},
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bcryptjs": "^2.4.6"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/[...nextauth]/route.ts
|
|
2
|
+
// ROUTE: /api/auth/* (GET + POST)
|
|
3
|
+
// ROLE: NextAuth v5 route handler
|
|
4
|
+
//
|
|
5
|
+
// INJECTED BY CLI — ONLY the nextauth block has this file.
|
|
6
|
+
// Supabase block does NOT have this file — it never gets created for Supabase users.
|
|
7
|
+
//
|
|
8
|
+
// Handles all NextAuth endpoints automatically:
|
|
9
|
+
// GET /api/auth/session ← reads current session
|
|
10
|
+
// GET /api/auth/csrf ← CSRF token
|
|
11
|
+
// GET /api/auth/providers ← lists configured providers
|
|
12
|
+
// POST /api/auth/signin ← sign in
|
|
13
|
+
// POST /api/auth/signout ← sign out
|
|
14
|
+
// GET /api/auth/callback/:p ← OAuth redirect lands here
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
import { handlers } from "@/src/lib/auth";
|
|
18
|
+
|
|
19
|
+
export const { GET, POST } = handlers;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/login/route.ts
|
|
2
|
+
// ROUTE: POST /api/auth/login
|
|
3
|
+
// ROLE: handles login form submission — called by /login page
|
|
4
|
+
//
|
|
5
|
+
// Uses NextAuth signIn() server-side to authenticate with credentials.
|
|
6
|
+
// OAuth (Google, GitHub) is handled automatically by NextAuth at /api/auth/signin.
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
10
|
+
import { nextAuthSignIn } from "@/src/lib/auth";
|
|
11
|
+
import { AuthError } from "next-auth";
|
|
12
|
+
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
const { email, password } = await req.json();
|
|
15
|
+
|
|
16
|
+
if (!email || !password) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: "Email and password are required." },
|
|
19
|
+
{ status: 400 },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await nextAuthSignIn("credentials", {
|
|
25
|
+
email,
|
|
26
|
+
password,
|
|
27
|
+
redirect: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ success: true });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof AuthError) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: "Invalid email or password." },
|
|
35
|
+
{ status: 401 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/logout/route.ts
|
|
2
|
+
// ROUTE: POST /api/auth/logout
|
|
3
|
+
// ROLE: signs the user out — called by LogoutButton component
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { NextResponse } from "next/server";
|
|
7
|
+
import { nextAuthSignOut } from "@/src/lib/auth";
|
|
8
|
+
|
|
9
|
+
export async function POST() {
|
|
10
|
+
await nextAuthSignOut({ redirect: false });
|
|
11
|
+
return NextResponse.json({ success: true });
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/signup/route.ts
|
|
2
|
+
// ROUTE: POST /api/auth/signup
|
|
3
|
+
// ROLE: creates a new user account — called by /signup page
|
|
4
|
+
//
|
|
5
|
+
// Hashes the password with bcrypt and saves to your database.
|
|
6
|
+
// TODO: replace the DB save section with your actual DB (Mongoose or Drizzle).
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
10
|
+
import bcrypt from "bcryptjs";
|
|
11
|
+
import { nextAuthSignIn } from "@/src/lib/auth";
|
|
12
|
+
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
const { name, email, password } = await req.json();
|
|
15
|
+
|
|
16
|
+
if (!email || !password) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: "Email and password are required." },
|
|
19
|
+
{ status: 400 },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (password.length < 8) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: "Password must be at least 8 characters." },
|
|
26
|
+
{ status: 400 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Hash password — never store plain text passwords
|
|
31
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
32
|
+
|
|
33
|
+
// ── TODO: Save user to your database ─────────────────────────────────────
|
|
34
|
+
// Replace this section with your actual DB call.
|
|
35
|
+
//
|
|
36
|
+
// Example with Mongoose:
|
|
37
|
+
// import User from "@/lib/db/models/User";
|
|
38
|
+
// import { connectDB } from "@/lib/db";
|
|
39
|
+
// await connectDB();
|
|
40
|
+
// const existing = await User.findOne({ email });
|
|
41
|
+
// if (existing) return NextResponse.json({ error: "Email already in use." }, { status: 409 });
|
|
42
|
+
// await User.create({ name, email, password: hashedPassword });
|
|
43
|
+
//
|
|
44
|
+
// Example with Drizzle:
|
|
45
|
+
// import { db } from "@/lib/db";
|
|
46
|
+
// import { users } from "@/lib/db/schema";
|
|
47
|
+
// import { eq } from "drizzle-orm";
|
|
48
|
+
// const [existing] = await db.select().from(users).where(eq(users.email, email));
|
|
49
|
+
// if (existing) return NextResponse.json({ error: "Email already in use." }, { status: 409 });
|
|
50
|
+
// await db.insert(users).values({ email, name, password: hashedPassword });
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
// Auto sign-in after signup
|
|
54
|
+
await nextAuthSignIn("credentials", {
|
|
55
|
+
email,
|
|
56
|
+
password, // use plain password here — authorize() will hash-compare
|
|
57
|
+
redirect: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return NextResponse.json({ success: true }, { status: 201 });
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// FILE: src/app/layout.tsx
|
|
2
|
+
// ROUTE: wraps every single page in the app
|
|
3
|
+
// ROLE: root layout with SessionProvider for NextAuth
|
|
4
|
+
//
|
|
5
|
+
// INJECTED BY CLI — overwrites base layout.tsx
|
|
6
|
+
// NextAuth requires SessionProvider to make useSession() work in client components.
|
|
7
|
+
// Supabase does NOT need this — base layout.tsx is used as-is for Supabase.
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
import type { Metadata } from "next";
|
|
11
|
+
import { Inter } from "next/font/google";
|
|
12
|
+
import { SessionProvider } from "next-auth/react";
|
|
13
|
+
import "../globals.css";
|
|
14
|
+
|
|
15
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
16
|
+
|
|
17
|
+
export const metadata: Metadata = {
|
|
18
|
+
title: "My SaaS",
|
|
19
|
+
description: "Built with shipindays",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default function RootLayout({
|
|
23
|
+
children,
|
|
24
|
+
}: {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<html lang="en" suppressHydrationWarning>
|
|
29
|
+
<body className={inter.className}>
|
|
30
|
+
{/* SessionProvider makes useSession() available in all client components */}
|
|
31
|
+
<SessionProvider>{children}</SessionProvider>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// FILE: src/types/next-auth.d.ts
|
|
2
|
+
// ROUTE: not a route — picked up automatically by TypeScript
|
|
3
|
+
// ROLE: augments NextAuth Session type to include user.id
|
|
4
|
+
// without this, session.user.id gives a TypeScript error
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
import "next-auth";
|
|
8
|
+
|
|
9
|
+
declare module "next-auth" {
|
|
10
|
+
interface Session {
|
|
11
|
+
user: {
|
|
12
|
+
id: string;
|
|
13
|
+
email: string;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
image?: string | null;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// FILE: src/lib/auth/index.ts
|
|
2
|
+
// ROUTE: not a route — imported everywhere auth is needed
|
|
3
|
+
// ROLE: NextAuth v5 implementation of the auth contract
|
|
4
|
+
//
|
|
5
|
+
// INJECTED BY CLI when user picks "NextAuth v5"
|
|
6
|
+
// Overwrites base placeholder at src/lib/auth/index.ts
|
|
7
|
+
//
|
|
8
|
+
// Exports the 3 contract functions every auth block must have:
|
|
9
|
+
// getCurrentUser() — returns user or null
|
|
10
|
+
// requireUser() — returns user or redirects to /login
|
|
11
|
+
// signOut() — signs out and redirects to /
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
import NextAuth from "next-auth";
|
|
15
|
+
import GitHub from "next-auth/providers/github";
|
|
16
|
+
import Google from "next-auth/providers/google";
|
|
17
|
+
import Credentials from "next-auth/providers/credentials";
|
|
18
|
+
import bcrypt from "bcryptjs";
|
|
19
|
+
import { redirect } from "next/navigation";
|
|
20
|
+
|
|
21
|
+
// ─── NextAuth config ──────────────────────────────────────────────────────────
|
|
22
|
+
// handlers → used by /api/auth/[...nextauth]/route.ts
|
|
23
|
+
// auth → used by middleware + server components to read session
|
|
24
|
+
// signIn → used by login API route
|
|
25
|
+
// signOut → used by logout API route
|
|
26
|
+
export const {
|
|
27
|
+
handlers,
|
|
28
|
+
auth,
|
|
29
|
+
signIn: nextAuthSignIn,
|
|
30
|
+
signOut: nextAuthSignOut,
|
|
31
|
+
} = NextAuth({
|
|
32
|
+
providers: [
|
|
33
|
+
// ── Google OAuth ──────────────────────────────────────────────────────────
|
|
34
|
+
// Needs: AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET in .env.local
|
|
35
|
+
// Setup: console.cloud.google.com → APIs → Credentials → OAuth 2.0
|
|
36
|
+
Google({
|
|
37
|
+
clientId: process.env.AUTH_GOOGLE_ID!,
|
|
38
|
+
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
// ── GitHub OAuth ──────────────────────────────────────────────────────────
|
|
42
|
+
// Needs: AUTH_GITHUB_ID and AUTH_GITHUB_SECRET in .env.local
|
|
43
|
+
// Setup: github.com → Settings → Developer settings → OAuth Apps
|
|
44
|
+
GitHub({
|
|
45
|
+
clientId: process.env.AUTH_GITHUB_ID!,
|
|
46
|
+
clientSecret: process.env.AUTH_GITHUB_SECRET!,
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
// ── Email + Password ──────────────────────────────────────────────────────
|
|
50
|
+
// Validates credentials against hashed password in DB
|
|
51
|
+
// TODO: swap the User.findOne() call with your actual DB query
|
|
52
|
+
Credentials({
|
|
53
|
+
name: "credentials",
|
|
54
|
+
credentials: {
|
|
55
|
+
email: { label: "Email", type: "email" },
|
|
56
|
+
password: { label: "Password", type: "password" },
|
|
57
|
+
},
|
|
58
|
+
async authorize(credentials) {
|
|
59
|
+
if (!credentials?.email || !credentials?.password) return null;
|
|
60
|
+
|
|
61
|
+
// TODO: replace this with your actual DB lookup
|
|
62
|
+
// Example with Mongoose:
|
|
63
|
+
// import User from "@/lib/db/models/User";
|
|
64
|
+
// const user = await User.findOne({ email: credentials.email }).select("+password");
|
|
65
|
+
// if (!user || !user.password) return null;
|
|
66
|
+
// const valid = await bcrypt.compare(credentials.password as string, user.password);
|
|
67
|
+
// if (!valid) return null;
|
|
68
|
+
// return { id: user._id.toString(), email: user.email, name: user.name };
|
|
69
|
+
|
|
70
|
+
// Example with Drizzle:
|
|
71
|
+
// import { db } from "@/lib/db";
|
|
72
|
+
// import { users } from "@/lib/db/schema";
|
|
73
|
+
// import { eq } from "drizzle-orm";
|
|
74
|
+
// const [user] = await db.select().from(users).where(eq(users.email, credentials.email as string));
|
|
75
|
+
// if (!user || !user.password) return null;
|
|
76
|
+
// const valid = await bcrypt.compare(credentials.password as string, user.password);
|
|
77
|
+
// if (!valid) return null;
|
|
78
|
+
// return { id: user.id, email: user.email, name: user.name };
|
|
79
|
+
|
|
80
|
+
return null; // replace this line with the above
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
callbacks: {
|
|
86
|
+
// ── JWT callback ──────────────────────────────────────────────────────────
|
|
87
|
+
// Runs when JWT is created or updated.
|
|
88
|
+
// Attach extra fields here to make them available in session.
|
|
89
|
+
async jwt({ token, user }) {
|
|
90
|
+
if (user) {
|
|
91
|
+
token.id = user.id;
|
|
92
|
+
token.name = user.name;
|
|
93
|
+
}
|
|
94
|
+
return token;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// ── Session callback ──────────────────────────────────────────────────────
|
|
98
|
+
// Runs whenever session is accessed.
|
|
99
|
+
// Pulls fields from JWT token into session.user.
|
|
100
|
+
async session({ session, token }) {
|
|
101
|
+
if (token && session.user) {
|
|
102
|
+
session.user.id = token.id as string;
|
|
103
|
+
session.user.name = token.name as string;
|
|
104
|
+
}
|
|
105
|
+
return session;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
pages: {
|
|
110
|
+
signIn: "/login", // redirect here when auth is required
|
|
111
|
+
error: "/login", // redirect here on auth errors
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
session: { strategy: "jwt" },
|
|
115
|
+
secret: process.env.AUTH_SECRET,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── getCurrentUser ───────────────────────────────────────────────────────────
|
|
119
|
+
// Returns the logged-in user or null.
|
|
120
|
+
// Use when you want to show different UI for logged-in vs logged-out users.
|
|
121
|
+
export async function getCurrentUser() {
|
|
122
|
+
const session = await auth();
|
|
123
|
+
if (!session?.user) return null;
|
|
124
|
+
return session.user;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── requireUser ─────────────────────────────────────────────────────────────
|
|
128
|
+
// Returns the logged-in user OR redirects to /login.
|
|
129
|
+
// Use on any protected page — user is guaranteed after this call.
|
|
130
|
+
export async function requireUser() {
|
|
131
|
+
const user = await getCurrentUser();
|
|
132
|
+
if (!user) redirect("/login");
|
|
133
|
+
return user;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── signOut ──────────────────────────────────────────────────────────────────
|
|
137
|
+
// Signs the user out and redirects to /.
|
|
138
|
+
// Called from /api/auth/logout route.
|
|
139
|
+
export async function signOut() {
|
|
140
|
+
await nextAuthSignOut({ redirectTo: "/" });
|
|
141
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// FILE: src/middleware.ts
|
|
2
|
+
// ROUTE: runs on every request before page renders
|
|
3
|
+
// ROLE: NextAuth v5 middleware
|
|
4
|
+
//
|
|
5
|
+
// INJECTED BY CLI when user picks "NextAuth v5"
|
|
6
|
+
// Overwrites base placeholder at src/middleware.ts
|
|
7
|
+
//
|
|
8
|
+
// Does two things:
|
|
9
|
+
// 1. Reads the NextAuth JWT on every request
|
|
10
|
+
// 2. Redirects unauthenticated users away from /dashboard to /login
|
|
11
|
+
// 3. Redirects logged-in users away from /login and /signup to /dashboard
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
import { auth } from "@/src/lib/auth";
|
|
15
|
+
import { NextResponse } from "next/server";
|
|
16
|
+
|
|
17
|
+
export default auth((req) => {
|
|
18
|
+
const isLoggedIn = !!req.auth?.user;
|
|
19
|
+
const { pathname } = req.nextUrl;
|
|
20
|
+
|
|
21
|
+
// Protect /dashboard and all nested routes
|
|
22
|
+
const isProtected = pathname.startsWith("/dashboard");
|
|
23
|
+
if (isProtected && !isLoggedIn) {
|
|
24
|
+
const loginUrl = new URL("/login", req.url);
|
|
25
|
+
loginUrl.searchParams.set("callbackUrl", pathname);
|
|
26
|
+
return NextResponse.redirect(loginUrl);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Redirect logged-in users away from /login and /signup
|
|
30
|
+
const isAuthPage = pathname === "/login" || pathname === "/signup";
|
|
31
|
+
if (isAuthPage && isLoggedIn) {
|
|
32
|
+
return NextResponse.redirect(new URL("/dashboard", req.url));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return NextResponse.next();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const config = {
|
|
39
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
|
40
|
+
};
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@supabase/ssr": "^0.5.1",
|
|
4
|
+
"@supabase/supabase-js": "^2.45.4"
|
|
5
|
+
}
|
|
6
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/login/route.ts
|
|
2
|
+
// ROUTE: POST /api/auth/login
|
|
3
|
+
// ROLE: handles login form submission — called by /login page
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
import { createSupabaseServerClient } from "@/src/lib/supabase/server";
|
|
8
|
+
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
const { email, password } = await req.json();
|
|
11
|
+
|
|
12
|
+
if (!email || !password) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "Email and password are required." },
|
|
15
|
+
{ status: 400 },
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const supabase = await createSupabaseServerClient();
|
|
20
|
+
|
|
21
|
+
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
22
|
+
|
|
23
|
+
if (error) {
|
|
24
|
+
return NextResponse.json({ error: error.message }, { status: 401 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return NextResponse.json({ success: true });
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/logout/route.ts
|
|
2
|
+
// ROUTE: POST /api/auth/logout
|
|
3
|
+
// ROLE: signs the user out — called by LogoutButton component
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { NextResponse } from "next/server";
|
|
7
|
+
import { createSupabaseServerClient } from "@/src/lib/supabase/server";
|
|
8
|
+
|
|
9
|
+
export async function POST() {
|
|
10
|
+
const supabase = await createSupabaseServerClient();
|
|
11
|
+
await supabase.auth.signOut();
|
|
12
|
+
return NextResponse.json({ success: true });
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// FILE: src/app/api/auth/signup/route.ts
|
|
2
|
+
// ROUTE: POST /api/auth/signup
|
|
3
|
+
// ROLE: handles signup form submission — called by /signup page
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
import { createSupabaseServerClient } from "@/src/lib/supabase/server";
|
|
8
|
+
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
const { name, email, password } = await req.json();
|
|
11
|
+
|
|
12
|
+
if (!email || !password) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "Email and password are required." },
|
|
15
|
+
{ status: 400 },
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (password.length < 8) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Password must be at least 8 characters." },
|
|
22
|
+
{ status: 400 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const supabase = await createSupabaseServerClient();
|
|
27
|
+
|
|
28
|
+
const { error } = await supabase.auth.signUp({
|
|
29
|
+
email,
|
|
30
|
+
password,
|
|
31
|
+
options: {
|
|
32
|
+
data: { name }, // stored in user_metadata
|
|
33
|
+
emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
return NextResponse.json({ error: error.message }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return NextResponse.json({ success: true }, { status: 201 });
|
|
42
|
+
}
|
|
@@ -1,75 +1,52 @@
|
|
|
1
1
|
// FILE: src/lib/auth/index.ts
|
|
2
|
-
// ROUTE: not a route — imported
|
|
3
|
-
// ROLE: Supabase Auth implementation
|
|
2
|
+
// ROUTE: not a route — imported everywhere auth is needed
|
|
3
|
+
// ROLE: Supabase Auth implementation of the auth contract
|
|
4
4
|
//
|
|
5
5
|
// INJECTED BY CLI when user picks "Supabase Auth"
|
|
6
|
-
//
|
|
6
|
+
// Overwrites base placeholder at src/lib/auth/index.ts
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// Exports the 3 contract functions every auth block must have:
|
|
9
|
+
// getCurrentUser() — returns user or null
|
|
10
|
+
// requireUser() — returns user or redirects to /login
|
|
11
|
+
// signOut() — signs out and redirects to /
|
|
9
12
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
13
|
|
|
11
|
-
import {
|
|
12
|
-
import { cookies } from "next/headers";
|
|
14
|
+
import { createSupabaseServerClient } from "@/src/lib/supabase/server";
|
|
13
15
|
import { redirect } from "next/navigation";
|
|
14
16
|
|
|
15
|
-
// ─── Supabase server client ───────────────────────────────────────────────────
|
|
16
|
-
// Creates a Supabase client that reads/writes session cookies server-side.
|
|
17
|
-
// Called fresh on every request — cookies() gives us the current request's cookies.
|
|
18
|
-
export async function createClient() {
|
|
19
|
-
const cookieStore = await cookies();
|
|
20
|
-
|
|
21
|
-
return createServerClient(
|
|
22
|
-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
23
|
-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
24
|
-
{
|
|
25
|
-
cookies: {
|
|
26
|
-
getAll() { return cookieStore.getAll(); },
|
|
27
|
-
setAll(toSet) {
|
|
28
|
-
try {
|
|
29
|
-
toSet.forEach(({ name, value, options }) =>
|
|
30
|
-
cookieStore.set(name, value, options)
|
|
31
|
-
);
|
|
32
|
-
} catch {
|
|
33
|
-
// called from Server Component — safe to ignore
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
17
|
// ─── getCurrentUser ───────────────────────────────────────────────────────────
|
|
42
|
-
// Returns the logged-in user or null.
|
|
43
|
-
// Use this when you want to show different UI for logged-in vs logged-out users.
|
|
18
|
+
// Returns the currently logged-in user, or null if not logged in.
|
|
44
19
|
//
|
|
45
|
-
//
|
|
20
|
+
// Use this when you want to show different UI for logged-in vs logged-out:
|
|
46
21
|
// const user = await getCurrentUser();
|
|
47
|
-
// if (user) {
|
|
22
|
+
// if (user) { show dashboard link } else { show login link }
|
|
48
23
|
export async function getCurrentUser() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
24
|
+
const supabase = await createSupabaseServerClient();
|
|
25
|
+
const {
|
|
26
|
+
data: { user },
|
|
27
|
+
error,
|
|
28
|
+
} = await supabase.auth.getUser();
|
|
29
|
+
if (error || !user) return null;
|
|
30
|
+
return user;
|
|
53
31
|
}
|
|
54
32
|
|
|
55
33
|
// ─── requireUser ─────────────────────────────────────────────────────────────
|
|
56
|
-
// Returns the logged-in user OR redirects to /login.
|
|
57
|
-
// Use this on any protected page/route.
|
|
34
|
+
// Returns the logged-in user OR redirects to /login if not authenticated.
|
|
58
35
|
//
|
|
59
|
-
//
|
|
60
|
-
// const user = await requireUser(); //
|
|
61
|
-
// // user is guaranteed to
|
|
36
|
+
// Use this on any protected page:
|
|
37
|
+
// const user = await requireUser(); // if not logged in, redirects automatically
|
|
38
|
+
// // user is guaranteed here — safe to use
|
|
62
39
|
export async function requireUser() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
40
|
+
const user = await getCurrentUser();
|
|
41
|
+
if (!user) redirect("/login");
|
|
42
|
+
return user;
|
|
66
43
|
}
|
|
67
44
|
|
|
68
45
|
// ─── signOut ──────────────────────────────────────────────────────────────────
|
|
69
|
-
// Signs the user out and redirects to
|
|
70
|
-
//
|
|
46
|
+
// Signs the user out and redirects to /.
|
|
47
|
+
// Called from /api/auth/logout route.
|
|
71
48
|
export async function signOut() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
49
|
+
const supabase = await createSupabaseServerClient();
|
|
50
|
+
await supabase.auth.signOut();
|
|
51
|
+
redirect("/");
|
|
52
|
+
}
|