@shipindays/shipindays 0.1.2

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/index.js ADDED
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+
3
+ // FILE: index.js
4
+ // ROLE: CLI entry point
5
+ // RUNS: npx create-shipindays@latest <project-name>
6
+ //
7
+
8
+ import * as p from "@clack/prompts";
9
+ import chalk from "chalk";
10
+ import fs from "fs-extra";
11
+ import path from "path";
12
+ import { execSync } from "child_process";
13
+ import { fileURLToPath } from "url";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ // Shorthand paths — everything is relative to where this file lives
18
+ const TEMPLATES_DIR = path.join(__dirname, "templates");
19
+ const BASE_DIR = path.join(TEMPLATES_DIR, "base"); // always copied first
20
+ const BLOCKS_DIR = path.join(TEMPLATES_DIR, "blocks"); // provider blocks live here
21
+
22
+ function printBanner() {
23
+ console.log("\n");
24
+ console.log(chalk.green(" ⚡ create-shipindays"));
25
+ console.log(chalk.dim(" Ship your SaaS in days, not months."));
26
+ console.log(chalk.dim(" https://shipindays.nikhilsai.com\n"));
27
+ }
28
+
29
+ //
30
+ // BLOCK REGISTRY
31
+ //
32
+ // Each "feature" (email, auth, payments) has a set of provider options.
33
+ // Each option maps to a folder inside templates/blocks/<feature>/<provider>/
34
+ //
35
+ // HOW TO ADD A NEW PROVIDER (e.g. Postmark for email):
36
+ // 1. Create folder: templates/blocks/email/postmark/
37
+ // 2. Add index.ts inside it with the same exported functions as other providers
38
+ // 3. Add "postmark" entry to EMAIL_PROVIDERS below
39
+ // 4. Add its env vars to ENV_VARS below
40
+ // Done. CLI will show it as an option automatically.
41
+ //
42
+ // HOW TO ADD A NEW FEATURE (e.g. payments):
43
+ // 1. Create folders: templates/blocks/payments/stripe/
44
+ // templates/blocks/payments/lemonsqueezy/
45
+ // 2. Add a PAYMENT_PROVIDERS object below (same shape as EMAIL_PROVIDERS)
46
+ // 3. Add a new p.select() prompt in main() for payments
47
+ // 4. Add injectBlock("payments", paymentProvider, targetPath) call
48
+ // 5. Add env vars to ENV_VARS below
49
+ // Done.
50
+ //
51
+
52
+ const EMAIL_PROVIDERS = {
53
+ // key = folder name inside templates/blocks/email/
54
+ resend: {
55
+ label: "Resend",
56
+ hint: "resend.com — best DX, generous free tier",
57
+ },
58
+ nodemailer: {
59
+ label: "Nodemailer",
60
+ hint: "SMTP — works with Gmail, Outlook, any mail server",
61
+ },
62
+ // ADD MORE HERE ↓
63
+ // postmark: {
64
+ // label: "Postmark",
65
+ // hint: "postmarkapp.com — great deliverability",
66
+ // },
67
+ };
68
+
69
+ //
70
+ // AUTH PROVIDERS
71
+ // Add new auth providers here — same shape as EMAIL_PROVIDERS
72
+ //
73
+ const AUTH_PROVIDERS = {
74
+ // key = folder name inside templates/blocks/auth/
75
+ supabase: {
76
+ label: "Supabase Auth",
77
+ hint: "Magic link + OAuth — no extra API route needed",
78
+ },
79
+ nextauth: {
80
+ label: "NextAuth v5",
81
+ hint: "Credentials + Google + GitHub — self-hostable",
82
+ },
83
+ // ADD MORE HERE ↓
84
+ // clerk: {
85
+ // label: "Clerk",
86
+ // hint: "Drop-in auth UI — easiest setup",
87
+ // },
88
+ };
89
+
90
+ //
91
+ // ENV VARS
92
+ //
93
+ // Defines what goes into .env.example based on the user's choices.
94
+ // Each provider key maps to an array of env var lines.
95
+ //
96
+ // TO ADD NEW ENV VARS for a new provider:
97
+ // Just add a new key matching the provider name.
98
+ //
99
+ const ENV_VARS = {
100
+ // Base env vars — always included regardless of choices
101
+ base: {
102
+ "# App": [
103
+ "NEXT_PUBLIC_APP_URL=http://localhost:3000",
104
+ ],
105
+ },
106
+
107
+ // Email provider env vars — only the chosen one gets added
108
+ email: {
109
+ resend: {
110
+ "# Resend (Email)": [
111
+ "RESEND_API_KEY=", // from resend.com → API Keys
112
+ ],
113
+ },
114
+ nodemailer: {
115
+ "# SMTP (Email via Nodemailer)": [
116
+ "SMTP_HOST=smtp.gmail.com", // your SMTP server
117
+ "SMTP_PORT=587",
118
+ "SMTP_SECURE=false",
119
+ "SMTP_USER=", // your email address
120
+ "SMTP_PASS=", // your email password or app password
121
+ "SMTP_FROM=you@yourdomain.com",
122
+ ],
123
+ },
124
+ // postmark: {
125
+ // "# Postmark (Email) ": [
126
+ // "POSTMARK_API_TOKEN=",
127
+ // ],
128
+ // },
129
+ },
130
+
131
+ // auth provider env vars — only the chosen one gets added
132
+ auth: {
133
+ supabase: {
134
+ "# Supabase Auth": [
135
+ "NEXT_PUBLIC_SUPABASE_URL=", // supabase.com → project → settings → API
136
+ "NEXT_PUBLIC_SUPABASE_ANON_KEY=", // supabase.com → project → settings → API
137
+ "SUPABASE_SERVICE_ROLE_KEY=", // supabase.com → project → settings → API
138
+ ],
139
+ },
140
+ nextauth: {
141
+ "# ── NextAuth ": [
142
+ "AUTH_SECRET=", // run: openssl rand -base64 32
143
+ ],
144
+ "# ── OAuth — Google ": [
145
+ "AUTH_GOOGLE_ID=", // console.cloud.google.com
146
+ "AUTH_GOOGLE_SECRET=",
147
+ ],
148
+ "# ── OAuth — GitHub ": [
149
+ "AUTH_GITHUB_ID=", // github.com → settings → developer settings
150
+ "AUTH_GITHUB_SECRET=",
151
+ ],
152
+ },
153
+ // clerk: {
154
+ // "# ── Clerk ──": [
155
+ // "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=",
156
+ // "CLERK_SECRET_KEY=",
157
+ // ],
158
+ // },
159
+ },
160
+
161
+ // FUTURE: payments
162
+ // payments: {
163
+ // stripe: { ... },
164
+ // lemonsqueezy: { ... },
165
+ // },
166
+ };
167
+
168
+ //
169
+ // BLOCK INJECTOR
170
+ //
171
+ // This is the core of the whole system.
172
+ // It copies a provider block folder ON TOP of the already-copied base template.
173
+ // Because the paths match (e.g. both have src/lib/email/index.ts),
174
+ // the provider file OVERWRITES the placeholder file from base.
175
+ //
176
+ // Example:
177
+ // base has: src/lib/email/index.ts ← placeholder (throws error if called)
178
+ // block has: src/lib/email/index.ts ← real Resend implementation
179
+ // Result: Resend file wins, placeholder is gone
180
+ //
181
+ // This is why your app code never changes — it always imports "@/lib/email",
182
+ // and that file just happens to contain whichever provider the user chose.
183
+ //
184
+ async function injectBlock(feature, provider, targetPath) {
185
+ // e.g. templates/blocks/email/resend/
186
+ const blockSrc = path.join(BLOCKS_DIR, feature, provider);
187
+
188
+ if (!await fs.pathExists(blockSrc)) {
189
+ throw new Error(`Block not found: ${blockSrc}\nMake sure templates/blocks/${feature}/${provider}/ exists.`);
190
+ }
191
+
192
+ // Auth blocks carry multiple files (lib/auth/index.ts, middleware.ts, sometimes api routes)
193
+ // so we copy the entire src/ folder directly into the target src/ folder.
194
+ // Email blocks only have one file so they go into src/lib/<feature>/
195
+ if (feature === "auth") {
196
+ // Copy everything inside the block's src/ folder into target's src/ folder
197
+ // This handles: lib/auth/index.ts, middleware.ts, AND api/auth/[...nextauth]/route.ts
198
+ const blockSrcDir = path.join(blockSrc, "src");
199
+ if (await fs.pathExists(blockSrcDir)) {
200
+ await fs.copy(blockSrcDir, path.join(targetPath, "src"), { overwrite: true });
201
+ }
202
+ } else {
203
+ // For email, payments etc — copy directly into src/lib/<feature>/
204
+ await fs.copy(blockSrc, path.join(targetPath, "src", "lib", feature), {
205
+ overwrite: true,
206
+ });
207
+ }
208
+ }
209
+
210
+ // ENV FILE BUILDER
211
+ // Combines base env vars + all chosen provider env vars into one .env.example
212
+ function buildEnvExample(choices) {
213
+ // choices = { email: "resend" } (will grow as more features are added)
214
+
215
+ let out = [
216
+ "# ────────────────",
217
+ "# shipindays — environment variables",
218
+ "# 1. cp .env.example .env.local",
219
+ "# 2. Fill in every blank value before running npm run dev",
220
+ "# ────────────────",
221
+ "",
222
+ ].join("\n");
223
+
224
+ // Always add base vars first
225
+ for (const [comment, vars] of Object.entries(ENV_VARS.base)) {
226
+ out += `${comment}\n${vars.join("\n")}\n\n`;
227
+ }
228
+
229
+ // Add vars for each chosen provider
230
+ for (const [feature, provider] of Object.entries(choices)) {
231
+ const providerVars = ENV_VARS[feature]?.[provider];
232
+ if (providerVars) {
233
+ for (const [comment, vars] of Object.entries(providerVars)) {
234
+ out += `${comment}\n${vars.join("\n")}\n\n`;
235
+ }
236
+ }
237
+ }
238
+
239
+ return out.trimEnd() + "\n";
240
+ }
241
+
242
+ // PACKAGE.JSON MERGER
243
+ // The base template has its own dependencies.
244
+ // Each provider block has its own package.json with extra dependencies.
245
+ // This merges them together so the final package.json has everything needed.
246
+
247
+ // Example:
248
+ // base package.json: { "dependencies": { "next": "15.0.3" } }
249
+ // resend package.json: { "dependencies": { "resend": "^4.0.0" } }
250
+ // merged result: { "dependencies": { "next": "15.0.3", "resend": "^4.0.0" } }
251
+ //
252
+ async function mergePackageJson(targetPath, feature, provider) {
253
+ const blockPkgPath = path.join(BLOCKS_DIR, feature, provider, "package.json");
254
+
255
+ // Not every block needs extra deps skip if no package.json
256
+ if (!await fs.pathExists(blockPkgPath)) return;
257
+
258
+ const targetPkgPath = path.join(targetPath, "package.json");
259
+ const targetPkg = await fs.readJson(targetPkgPath);
260
+ const blockPkg = await fs.readJson(blockPkgPath);
261
+
262
+ // Deep merge dependencies and devDependencies
263
+ targetPkg.dependencies = { ...targetPkg.dependencies, ...blockPkg.dependencies };
264
+ targetPkg.devDependencies = { ...targetPkg.devDependencies, ...blockPkg.devDependencies };
265
+
266
+ await fs.writeJson(targetPkgPath, targetPkg, { spaces: 2 });
267
+ }
268
+
269
+ // HELPERS (same as before)
270
+ function isValidName(n) { return /^[a-zA-Z0-9-_]+$/.test(n); }
271
+ function toSlug(s) { return s.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-_]/g, ""); }
272
+ function detectPM() {
273
+ const a = process.env.npm_config_user_agent ?? "";
274
+ return a.includes("pnpm") ? "pnpm" : a.includes("yarn") ? "yarn" : "npm";
275
+ }
276
+ function run(cmd, cwd) { execSync(cmd, { cwd, stdio: "inherit" }); }
277
+
278
+ function buildGitignore() {
279
+ return ["node_modules/", ".next/", "out/", ".env", ".env.local", ".env.*.local", ".DS_Store", ".vercel", "drizzle/"].join("\n");
280
+ }
281
+
282
+ function printNextSteps(projectDir, pm, choices) {
283
+ const isHere = projectDir === "." || projectDir === "./";
284
+ const runCmd = pm === "npm" ? "npm run" : pm;
285
+ let n = 1;
286
+ const step = (s) => console.log(chalk.dim(` ${n++}. ${s}`));
287
+
288
+ console.log("\n");
289
+ console.log(chalk.green(" ✓ Your SaaS is scaffolded.\n"));
290
+ console.log(chalk.dim(` Stack: auth → ${choices.auth} · email → ${choices.email}`));
291
+ console.log("");
292
+ if (!isHere) step(`cd ${projectDir.replace(/^\.\//, "")}`);
293
+ step("cp .env.example .env.local");
294
+ step(" → Fill in your keys");
295
+ step(`${runCmd} db:push`);
296
+ step(`${runCmd} dev`);
297
+ console.log("");
298
+ console.log(chalk.dim(" GitHub → https://github.com/nikhilsaiankilla/shipindays"));
299
+ console.log(chalk.dim(" Twitter → https://x.com/itzznikhilsai"));
300
+ console.log("");
301
+ console.log(chalk.green(" ⚡ Now go build what only you can build.\n"));
302
+ }
303
+
304
+ // MAIN
305
+ async function main() {
306
+ printBanner();
307
+ p.intro(chalk.bgGreen(chalk.black(" create-shipindays ")));
308
+
309
+ // 1. Project directory
310
+ let projectDir = process.argv[2];
311
+
312
+ if (!projectDir) {
313
+ const answer = await p.text({
314
+ message: "Where should we create your project?",
315
+ placeholder: "./my-saas",
316
+ defaultValue: "./my-saas",
317
+ validate(val) {
318
+ const clean = val.replace(/^\.\//, "");
319
+ if (clean !== "." && !isValidName(clean)) return "Letters, numbers, hyphens, underscores only.";
320
+ },
321
+ });
322
+ if (p.isCancel(answer)) { p.cancel("Cancelled."); process.exit(0); }
323
+ projectDir = answer;
324
+ }
325
+
326
+ const isCurrentDir = projectDir === "." || projectDir === "./";
327
+ const targetPath = isCurrentDir ? process.cwd() : path.resolve(process.cwd(), projectDir.replace(/^\.\//, ""));
328
+ const projectName = isCurrentDir ? path.basename(process.cwd()) : toSlug(path.basename(projectDir.replace(/^\.\//, "")));
329
+
330
+ if (!isCurrentDir && await fs.pathExists(targetPath)) {
331
+ const files = (await fs.readdir(targetPath)).filter(f => f !== ".git");
332
+ if (files.length > 0) {
333
+ const ok = await p.confirm({ message: `${projectDir} is not empty. Overwrite?`, initialValue: false });
334
+ if (p.isCancel(ok) || !ok) { p.cancel("Cancelled."); process.exit(0); }
335
+ }
336
+ }
337
+
338
+ // 2. Pick auth provider
339
+ const authProvider = await p.select({
340
+ message: "Auth provider",
341
+ options: Object.entries(AUTH_PROVIDERS).map(([value, { label, hint }]) => ({
342
+ value, label, hint,
343
+ })),
344
+ });
345
+ if (p.isCancel(authProvider)) { p.cancel("Cancelled."); process.exit(0); }
346
+
347
+ // 3. Pick email provider
348
+ const emailProvider = await p.select({
349
+ message: "Email provider",
350
+ options: Object.entries(EMAIL_PROVIDERS).map(([value, { label, hint }]) => ({
351
+ value, label, hint,
352
+ })),
353
+ });
354
+ if (p.isCancel(emailProvider)) { p.cancel("Cancelled."); process.exit(0); }
355
+
356
+ // All user choices in one object passed to injectBlock + buildEnvExample
357
+ // Adding payments later = just add: payments: paymentsProvider
358
+ const choices = {
359
+ auth: authProvider,
360
+ email: emailProvider,
361
+ };
362
+
363
+ // 4. Git + install
364
+ const initGit = await p.confirm({ message: "Initialize a git repository?", initialValue: true });
365
+ if (p.isCancel(initGit)) { p.cancel("Cancelled."); process.exit(0); }
366
+
367
+ const pm = detectPM();
368
+ const install = await p.confirm({ message: `Install dependencies with ${pm}?`, initialValue: true });
369
+ if (p.isCancel(install)) { p.cancel("Cancelled."); process.exit(0); }
370
+
371
+ const spin = p.spinner();
372
+
373
+ // 4. Copy BASE template
374
+ // Base = Next.js + Tailwind + shadcn — always the same, no matter what
375
+ spin.start("Copying base template...");
376
+ if (!await fs.pathExists(BASE_DIR)) {
377
+ spin.stop(chalk.red(`Base template not found at: ${BASE_DIR}`));
378
+ process.exit(1);
379
+ }
380
+ await fs.copy(BASE_DIR, targetPath, {
381
+ overwrite: true,
382
+ filter: (src) => !src.includes("node_modules") && !src.includes(".next"),
383
+ });
384
+ spin.stop("Base template copied.");
385
+
386
+ // 5. Inject provider blocks
387
+ // For each choice, copy the provider block on top of the base.
388
+ // The block's files overwrite the base placeholders at the same paths.
389
+
390
+ // Inject auth block
391
+ spin.start(`Injecting auth: ${choices.auth}...`);
392
+ await injectBlock("auth", choices.auth, targetPath);
393
+ await mergePackageJson(targetPath, "auth", choices.auth);
394
+ spin.stop(`Auth: ${choices.auth} injected.`);
395
+
396
+ // Inject email block
397
+ spin.start(`Injecting email: ${choices.email}...`);
398
+ await injectBlock("email", choices.email, targetPath);
399
+ await mergePackageJson(targetPath, "email", choices.email);
400
+ spin.stop(`Email: ${choices.email} injected.`);
401
+
402
+ // FUTURE: add more inject calls here as you add features
403
+ // spin.start(`Injecting payments: ${choices.payments}...`);
404
+ // await injectBlock("payments", choices.payments, targetPath);
405
+ // await mergePackageJson(targetPath, "payments", choices.payments);
406
+ // spin.stop(`Payments: ${choices.payments} injected.`);
407
+
408
+ // 6. Write .env.example
409
+ spin.start("Writing .env.example...");
410
+ await fs.outputFile(path.join(targetPath, ".env.example"), buildEnvExample(choices));
411
+ spin.stop(".env.example written.");
412
+
413
+ // 7. Write .gitignore
414
+ await fs.outputFile(path.join(targetPath, ".gitignore"), buildGitignore());
415
+
416
+ // 8. Update package.json name
417
+ spin.start("Configuring package.json...");
418
+ const pkgPath = path.join(targetPath, "package.json");
419
+ if (await fs.pathExists(pkgPath)) {
420
+ const pkg = await fs.readJson(pkgPath);
421
+ pkg.name = projectName;
422
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
423
+ }
424
+ spin.stop("package.json configured.");
425
+
426
+ // 9. Git init
427
+ if (initGit) {
428
+ spin.start("Initialising git...");
429
+ try {
430
+ run("git init", targetPath);
431
+ run("git add -A", targetPath);
432
+ run(`git commit -m "chore: scaffold from create-shipindays"`, targetPath);
433
+ spin.stop("Git initialised.");
434
+ } catch {
435
+ spin.stop(chalk.yellow("Git skipped."));
436
+ }
437
+ }
438
+
439
+ // 10. Install
440
+ if (install) {
441
+ spin.start(`Installing with ${pm}...`);
442
+ try {
443
+ run(pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm install" : "npm install", targetPath);
444
+ spin.stop("Dependencies installed.");
445
+ } catch {
446
+ spin.stop(chalk.yellow(`Run '${pm} install' manually.`));
447
+ }
448
+ }
449
+
450
+ p.outro(chalk.green("Done!"));
451
+ printNextSteps(projectDir, pm, choices);
452
+ }
453
+
454
+ main().catch((err) => {
455
+ console.error(chalk.red("\n Fatal: " + err.message));
456
+ process.exit(1);
457
+ });
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@shipindays/shipindays",
3
+ "version": "0.1.2",
4
+ "description": "Scaffold a production ready Next.js SaaS in seconds. Pick your stack, get auth + payments + email wired up.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-shipindays": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "templates"
12
+ ],
13
+ "keywords": [
14
+ "nextjs",
15
+ "saas",
16
+ "starter",
17
+ "boilerplate",
18
+ "supabase",
19
+ "drizzle",
20
+ "stripe",
21
+ "resend"
22
+ ],
23
+ "author": "Nikhil Sai <nikhilsaiankilla@gmail.com>",
24
+ "license": "MIT",
25
+ "homepage": "https://shipindays.nikhilsai.com",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/nikhilsaiankilla/shipindays"
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
+ "dependencies": {
48
+ "@clack/prompts": "^0.9.1",
49
+ "chalk": "^5.3.0",
50
+ "fs-extra": "^11.2.0"
51
+ },
52
+ "engines": {
53
+ "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
+ }
64
+ }
@@ -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 exists when user picks NextAuth.
6
+ // Supabase block does NOT have this file so it never gets created.
7
+ //
8
+ // Handles all NextAuth endpoints automatically:
9
+ // GET /api/auth/session
10
+ // GET /api/auth/csrf
11
+ // GET /api/auth/providers
12
+ // POST /api/auth/signin
13
+ // POST /api/auth/signout
14
+ // GET /api/auth/callback/:provider ← OAuth redirect comes back here
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+
17
+ import { handlers } from "@/lib/auth";
18
+
19
+ export const { GET, POST } = handlers;
@@ -0,0 +1,98 @@
1
+ // FILE: src/lib/auth/index.ts
2
+ // ROUTE: not a route — imported by middleware, dashboard, API routes
3
+ // ROLE: NextAuth v5 implementation
4
+ //
5
+ // INJECTED BY CLI when user picks "NextAuth v5"
6
+ // Replaces base placeholder at src/lib/auth/index.ts
7
+ //
8
+ // NextAuth DOES need an API route: src/app/api/auth/[...nextauth]/route.ts
9
+ // That file is also included in this block and gets copied automatically.
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ import NextAuth from "next-auth";
13
+ import GitHub from "next-auth/providers/github";
14
+ import Google from "next-auth/providers/google";
15
+ import Credentials from "next-auth/providers/credentials";
16
+ import { redirect } from "next/navigation";
17
+
18
+ // ─── NextAuth config ──────────────────────────────────────────────────────────
19
+ // handlers → used by /api/auth/[...nextauth]/route.ts
20
+ // auth → used by middleware + server components to read the session
21
+ // signIn → used by login page
22
+ // signOut → used by logout button
23
+ export const { handlers, auth, signIn: nextAuthSignIn, signOut: nextAuthSignOut } = NextAuth({
24
+ providers: [
25
+ Google({
26
+ clientId: process.env.AUTH_GOOGLE_ID!,
27
+ clientSecret: process.env.AUTH_GOOGLE_SECRET!,
28
+ }),
29
+ GitHub({
30
+ clientId: process.env.AUTH_GITHUB_ID!,
31
+ clientSecret: process.env.AUTH_GITHUB_SECRET!,
32
+ }),
33
+ Credentials({
34
+ name: "credentials",
35
+ credentials: {
36
+ email: { label: "Email", type: "email" },
37
+ password: { label: "Password", type: "password" },
38
+ },
39
+ async authorize(credentials) {
40
+ // TODO: validate credentials against your database
41
+ // Example with your User model:
42
+ // const user = await User.findOne({ email: credentials.email })
43
+ // const valid = await bcrypt.compare(credentials.password, user.password)
44
+ // if (!valid) return null
45
+ // return { id: user._id, email: user.email, name: user.name }
46
+ return null;
47
+ },
48
+ }),
49
+ ],
50
+
51
+ callbacks: {
52
+ // Attach user id to the JWT token so we can read it in session
53
+ async jwt({ token, user }) {
54
+ if (user) token.id = user.id;
55
+ return token;
56
+ },
57
+ // Attach user id from token to the session object
58
+ async session({ session, token }) {
59
+ if (token && session.user) session.user.id = token.id as string;
60
+ return session;
61
+ },
62
+ },
63
+
64
+ pages: {
65
+ signIn: "/login", // redirect here when auth is required
66
+ error: "/login", // redirect here on auth errors
67
+ },
68
+
69
+ session: { strategy: "jwt" },
70
+ secret: process.env.AUTH_SECRET,
71
+ });
72
+
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+ // THE CONTRACT — same 3 functions as Supabase block
75
+ // Rest of app calls these, never touches NextAuth directly
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+
78
+ // ─── getCurrentUser ───────────────────────────────────────────────────────────
79
+ // Returns the logged-in user or null.
80
+ export async function getCurrentUser() {
81
+ const session = await auth();
82
+ if (!session?.user) return null;
83
+ return session.user;
84
+ }
85
+
86
+ // ─── requireUser ─────────────────────────────────────────────────────────────
87
+ // Returns the logged-in user OR redirects to /login.
88
+ export async function requireUser() {
89
+ const user = await getCurrentUser();
90
+ if (!user) redirect("/login");
91
+ return user;
92
+ }
93
+
94
+ // ─── signOut ──────────────────────────────────────────────────────────────────
95
+ // Signs the user out and redirects to home.
96
+ export async function signOut() {
97
+ await nextAuthSignOut({ redirectTo: "/" });
98
+ }
@@ -0,0 +1,30 @@
1
+ // FILE: src/middleware.ts
2
+ // ROUTE: runs on every request before the page renders
3
+ // ROLE: NextAuth v5 middleware
4
+ //
5
+ // INJECTED BY CLI when user picks "NextAuth v5"
6
+ // Replaces base placeholder at src/middleware.ts
7
+ //
8
+ // Reads the NextAuth JWT and redirects unauthenticated users
9
+ // away from /dashboard to /login.
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ import { auth } from "@/lib/auth";
13
+ import { NextResponse } from "next/server";
14
+
15
+ export default auth((req) => {
16
+ const isLoggedIn = !!req.auth?.user;
17
+ const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
18
+
19
+ if (isProtected && !isLoggedIn) {
20
+ const loginUrl = new URL("/login", req.url);
21
+ loginUrl.searchParams.set("callbackUrl", req.nextUrl.pathname);
22
+ return NextResponse.redirect(loginUrl);
23
+ }
24
+
25
+ return NextResponse.next();
26
+ });
27
+
28
+ export const config = {
29
+ matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
30
+ };
@@ -0,0 +1,9 @@
1
+ {
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,6 @@
1
+ {
2
+ "dependencies": {
3
+ "@supabase/ssr": "^0.5.1",
4
+ "@supabase/supabase-js": "^2.45.4"
5
+ }
6
+ }