better-ts-stack 2.1.1 → 2.2.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/LICENSE +21 -21
- package/README.md +16 -8
- package/dist/args.js +7 -0
- package/dist/builder/buildPlanner.js +7 -1
- package/dist/builder/configGenerator.js +28 -3
- package/dist/builder/index.js +2 -1
- package/dist/builder/moduleSelector.js +6 -1
- package/dist/builder/readmeGenerator.js +8 -2
- package/dist/builder/templateContext.js +1 -0
- package/dist/output/nextSteps.js +2 -4
- package/dist/prompts/stack.js +6 -1
- package/dist/types/index.js +2 -0
- package/dist/ui/messages.js +2 -0
- package/package.json +71 -71
- package/templates/backend/express/.prettierignore +52 -52
- package/templates/backend/express/.prettierrc +16 -16
- package/templates/backend/express/gitignore +3 -1
- package/templates/backend/express/src/env.ts.hbs +13 -2
- package/templates/backend/express/src/index.ts.hbs +31 -2
- package/templates/backend/express/tsconfig.eslint.json +9 -9
- package/templates/backend/nestjs/.prettierignore +52 -52
- package/templates/backend/nestjs/gitignore +3 -1
- package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
- package/templates/backend/nestjs/src/env.ts.hbs +13 -2
- package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
- package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
- package/templates/backend/nestjs/tsconfig.eslint.json +9 -9
- package/templates/frontend/nextjs/app/globals.css +129 -89
- package/templates/frontend/nextjs/app/layout.tsx +36 -34
- package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
- package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
- package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
- package/templates/frontend/nextjs/components/ui/button.tsx +57 -51
- package/templates/frontend/nextjs/components/ui/card.tsx +102 -60
- package/templates/frontend/nextjs/components/ui/field.tsx +238 -67
- package/templates/frontend/nextjs/components/ui/input.tsx +19 -18
- package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
- package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
- package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
- package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
- package/templates/frontend/nextjs/components.json +11 -5
- package/templates/frontend/nextjs/config.json +36 -35
- package/templates/frontend/nextjs/eslint.config.mjs +11 -11
- package/templates/frontend/nextjs/lib/utils.ts +1 -6
- package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
- package/templates/frontend/nextjs/postcss.config.mjs +7 -7
- package/templates/frontend/nextjs/public/file.svg +1 -1
- package/templates/frontend/nextjs/public/globe.svg +1 -1
- package/templates/frontend/nextjs/public/window.svg +1 -1
- package/templates/frontend/tanstack/components.json +25 -18
- package/templates/frontend/tanstack/config.json +5 -2
- package/templates/frontend/tanstack/eslint.config.mjs +23 -23
- package/templates/frontend/tanstack/gitignore +39 -39
- package/templates/frontend/tanstack/public/favicon.svg +7 -7
- package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
- package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
- package/templates/frontend/tanstack/src/components/ui/button.tsx +57 -51
- package/templates/frontend/tanstack/src/components/ui/card.tsx +102 -60
- package/templates/frontend/tanstack/src/components/ui/field.tsx +236 -67
- package/templates/frontend/tanstack/src/components/ui/input.tsx +19 -18
- package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
- package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
- package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
- package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
- package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
- package/templates/frontend/tanstack/src/router.tsx +31 -31
- package/templates/frontend/tanstack/src/routes/__root.tsx +69 -69
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
- package/templates/frontend/tanstack/src/styles/app.css +100 -55
- package/templates/frontend/tanstack/tsconfig.json +22 -22
- package/templates/modules/agents/AGENTS.md.hbs +245 -0
- package/templates/modules/agents/CLAUDE.md +1 -0
- package/templates/modules/agents/config.json +10 -0
- package/templates/modules/auth/express/config.json +12 -11
- package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
- package/templates/modules/auth/express/src/lib/email.ts +37 -0
- package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
- package/templates/modules/auth/express/src/routes/me.ts +13 -0
- package/templates/modules/auth/nestjs/config.json +13 -11
- package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
- package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
- package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
- package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
- package/templates/modules/auth/nextjs/app/api/auth/[...all]/route.ts +4 -4
- package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
- package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
- package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
- package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
- package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
- package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
- package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
- package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
- package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
- package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
- package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
- package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
- package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
- package/templates/modules/auth/nextjs/config.json +24 -18
- package/templates/modules/auth/nextjs/lib/auth-client.ts +3 -3
- package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
- package/templates/modules/auth/nextjs/lib/email.ts +43 -0
- package/templates/modules/auth/nextjs/lib/session.ts +12 -0
- package/templates/modules/auth/tanstack/config.json +25 -19
- package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
- package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
- package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
- package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
- package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
- package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
- package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
- package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
- package/templates/modules/auth/tanstack/src/lib/auth-client.ts +2 -2
- package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +14 -8
- package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +98 -38
- package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
- package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
- package/templates/modules/auth/tanstack/src/routes/api/auth/$.ts +15 -15
- package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +119 -111
- package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
- package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
- package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +44 -37
- package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +44 -37
- package/templates/modules/docker/.dockerignore +2 -0
- package/templates/modules/docker/config.json +15 -15
- package/templates/modules/drizzle/express/config.json +26 -26
- package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nestjs/config.json +26 -26
- package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nextjs/config.json +26 -26
- package/templates/modules/drizzle/nextjs/drizzle.config.ts +13 -13
- package/templates/modules/drizzle/tanstack/config.json +26 -26
- package/templates/modules/drizzle/tanstack/drizzle.config.ts +12 -12
- package/templates/modules/drizzle/tanstack/src/lib/db.ts +13 -13
- package/templates/modules/mongoose/express/config.json +15 -15
- package/templates/modules/mongoose/express/src/lib/db.ts +43 -43
- package/templates/modules/mongoose/express/src/models/User.ts +48 -48
- package/templates/modules/mongoose/nestjs/config.json +16 -15
- package/templates/modules/mongoose/nextjs/config.json +15 -15
- package/templates/modules/mongoose/nextjs/models/User.ts +48 -48
- package/templates/modules/mongoose/tanstack/config.json +16 -16
- package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -48
- package/templates/modules/prisma/express/config.json +27 -27
- package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
- package/templates/modules/prisma/nestjs/config.json +27 -27
- package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
- package/templates/modules/prisma/nextjs/config.json +28 -28
- package/templates/modules/prisma/nextjs/lib/prisma.ts +18 -18
- package/templates/modules/prisma/nextjs/prisma.config.ts +14 -14
- package/templates/modules/prisma/tanstack/config.json +28 -28
- package/templates/modules/prisma/tanstack/prisma/schema.prisma.hbs +91 -91
- package/templates/modules/prisma/tanstack/prisma.config.ts +14 -14
- package/templates/modules/prisma/tanstack/src/lib/prisma.ts +17 -17
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
- package/templates/modules/vscode/.vscode/extensions.json.hbs +29 -0
- package/templates/modules/vscode/.vscode/settings.json.hbs +22 -0
- package/templates/modules/vscode/config.json +10 -0
- package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
- package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
- package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
- package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
- package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
- package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
- package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
- package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
- package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
- package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
- package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
- package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
- package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
- package/templates/modules/prisma/nestjs/prisma/schema.prisma +0 -23
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "auth",
|
|
3
|
-
"name": "Better Auth",
|
|
4
|
-
"description": "Better Auth for Next.js with optional Prisma or Drizzle adapter",
|
|
5
|
-
"type": "feature",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"@hookform/resolvers": "^5.9.2",
|
|
8
|
-
"better-auth": "^1.7.14",
|
|
9
|
-
"react-hook-form": "^7.91.0",
|
|
10
|
-
"zod": "^4.6.7"
|
|
11
|
-
},
|
|
12
|
-
"devDependencies": {},
|
|
13
|
-
"scripts": {},
|
|
14
|
-
"envVars": {
|
|
15
|
-
"BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
|
|
16
|
-
"BETTER_AUTH_URL": "http://localhost:3000"
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
{
|
|
2
|
+
"id": "auth",
|
|
3
|
+
"name": "Better Auth",
|
|
4
|
+
"description": "Better Auth with email verification, password reset, and GitHub and Google sign-in for Next.js with optional Prisma or Drizzle adapter",
|
|
5
|
+
"type": "feature",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@hookform/resolvers": "^5.9.2",
|
|
8
|
+
"better-auth": "^1.7.14",
|
|
9
|
+
"react-hook-form": "^7.91.0",
|
|
10
|
+
"zod": "^4.6.7"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {},
|
|
13
|
+
"scripts": {},
|
|
14
|
+
"envVars": {
|
|
15
|
+
"BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
|
|
16
|
+
"BETTER_AUTH_URL": "http://localhost:3000",
|
|
17
|
+
"GITHUB_CLIENT_ID": "",
|
|
18
|
+
"GITHUB_CLIENT_SECRET": "",
|
|
19
|
+
"GOOGLE_CLIENT_ID": "",
|
|
20
|
+
"GOOGLE_CLIENT_SECRET": "",
|
|
21
|
+
"RESEND_API_KEY": "",
|
|
22
|
+
"EMAIL_FROM": "onboarding@resend.dev"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { createAuthClient } from "better-auth/react";
|
|
2
|
-
|
|
3
|
-
export const authClient = createAuthClient();
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
|
+
export const authClient = createAuthClient();
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
const emailSchema = z.email("Enter a valid email address.");
|
|
4
|
+
const passwordSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.min(8, "Password must be at least 8 characters.");
|
|
7
|
+
|
|
3
8
|
export const signInSchema = z.object({
|
|
4
|
-
email:
|
|
5
|
-
password:
|
|
9
|
+
email: emailSchema,
|
|
10
|
+
password: passwordSchema,
|
|
6
11
|
});
|
|
7
12
|
|
|
8
13
|
export const signUpSchema = signInSchema.extend({
|
|
@@ -13,8 +18,28 @@ export const signUpSchema = signInSchema.extend({
|
|
|
13
18
|
.max(50, "Name must be 50 characters or fewer."),
|
|
14
19
|
});
|
|
15
20
|
|
|
21
|
+
export const forgotPasswordSchema = z.object({ email: emailSchema });
|
|
22
|
+
|
|
23
|
+
export const resetPasswordSchema = z
|
|
24
|
+
.object({ password: passwordSchema, confirmPassword: z.string() })
|
|
25
|
+
.refine((values) => values.password === values.confirmPassword, {
|
|
26
|
+
message: "Passwords do not match.",
|
|
27
|
+
path: ["confirmPassword"],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const changePasswordSchema = z.object({
|
|
31
|
+
currentPassword: z.string().min(1, "Enter your current password."),
|
|
32
|
+
newPassword: passwordSchema,
|
|
33
|
+
});
|
|
34
|
+
|
|
16
35
|
export type SignInValues = z.infer<typeof signInSchema>;
|
|
17
36
|
export type SignUpValues = z.infer<typeof signUpSchema>;
|
|
37
|
+
export type ForgotPasswordValues = z.infer<typeof forgotPasswordSchema>;
|
|
38
|
+
export type ResetPasswordValues = z.infer<typeof resetPasswordSchema>;
|
|
39
|
+
export type ChangePasswordValues = z.infer<typeof changePasswordSchema>;
|
|
40
|
+
|
|
41
|
+
// Sign-in providers the auth server can turn on (see auth.ts)
|
|
42
|
+
export type SocialProvider = "github" | "google";
|
|
18
43
|
|
|
19
44
|
export function getAuthErrorMessage(error: unknown) {
|
|
20
45
|
if (!error) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { after } from "next/server";
|
|
1
2
|
import { betterAuth } from "better-auth";
|
|
2
3
|
import { nextCookies } from "better-auth/next-js";
|
|
4
|
+
|
|
5
|
+
import type { SocialProvider } from "./auth-schema";
|
|
6
|
+
import { sendEmail } from "./email";
|
|
3
7
|
{{#if (eq database 'prisma')}}
|
|
4
8
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
5
9
|
import client from "./prisma";
|
|
@@ -16,6 +20,18 @@ const client = new MongoClient(process.env.MONGODB_URI!);
|
|
|
16
20
|
const db = client.db();
|
|
17
21
|
{{/if}}
|
|
18
22
|
|
|
23
|
+
// Sends the email after the response, so the request does not wait for it.
|
|
24
|
+
// after() keeps a serverless function (such as on Vercel) running until the
|
|
25
|
+
// email is sent. A failed email is logged instead of failing the request: the
|
|
26
|
+
// sign-up or reset already happened, and the user can ask for the email again
|
|
27
|
+
function deliver(email: Parameters<typeof sendEmail>[0]): void {
|
|
28
|
+
after(() =>
|
|
29
|
+
sendEmail(email).catch((error: unknown) => {
|
|
30
|
+
console.error(`Could not send "${email.subject}" to ${email.to}:`, error);
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export const auth = betterAuth({
|
|
20
36
|
{{#if (eq database 'prisma')}}
|
|
21
37
|
database: prismaAdapter(client, { provider: "postgresql" }),
|
|
@@ -30,8 +46,57 @@ export const auth = betterAuth({
|
|
|
30
46
|
{{/if}}
|
|
31
47
|
emailAndPassword: {
|
|
32
48
|
enabled: true,
|
|
49
|
+
// Set to true to block sign-in until the email address is verified
|
|
50
|
+
requireEmailVerification: false,
|
|
51
|
+
// A reset signs the user out everywhere
|
|
52
|
+
revokeSessionsOnPasswordReset: true,
|
|
53
|
+
sendResetPassword: async ({ user, url }) => {
|
|
54
|
+
deliver({
|
|
55
|
+
to: user.email,
|
|
56
|
+
subject: "Reset your password",
|
|
57
|
+
text: `Open this link to choose a new password:
|
|
58
|
+
${url}`,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
emailVerification: {
|
|
63
|
+
sendOnSignUp: true,
|
|
64
|
+
autoSignInAfterVerification: true,
|
|
65
|
+
sendVerificationEmail: async ({ user, url }) => {
|
|
66
|
+
deliver({
|
|
67
|
+
to: user.email,
|
|
68
|
+
subject: "Verify your email address",
|
|
69
|
+
text: `Open this link to verify your email address:
|
|
70
|
+
${url}`,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
// Each provider is turned on once its client ID and secret are set
|
|
75
|
+
socialProviders: {
|
|
76
|
+
...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
|
|
77
|
+
? {
|
|
78
|
+
github: {
|
|
79
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
80
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
: {}),
|
|
84
|
+
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
|
85
|
+
? {
|
|
86
|
+
google: {
|
|
87
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
88
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
33
92
|
},
|
|
34
93
|
plugins: [nextCookies()],
|
|
35
94
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
36
95
|
baseURL: process.env.BETTER_AUTH_URL,
|
|
37
96
|
});
|
|
97
|
+
|
|
98
|
+
// The providers turned on above; the sign-in and sign-up pages show a button
|
|
99
|
+
// for each
|
|
100
|
+
export const socialProviders = Object.keys(
|
|
101
|
+
auth.options.socialProviders
|
|
102
|
+
) as SocialProvider[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Sends through Resend when RESEND_API_KEY is set. Without it the email is
|
|
2
|
+
// printed to the server log, so verification and reset links can be copied
|
|
3
|
+
// from the terminal during development
|
|
4
|
+
export async function sendEmail({
|
|
5
|
+
to,
|
|
6
|
+
subject,
|
|
7
|
+
text,
|
|
8
|
+
}: {
|
|
9
|
+
to: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}): Promise<void> {
|
|
13
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
14
|
+
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
console.log(`
|
|
17
|
+
--- Email to ${to}: ${subject}
|
|
18
|
+
${text}
|
|
19
|
+
---
|
|
20
|
+
`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${apiKey}`,
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
from: process.env.EMAIL_FROM || "onboarding@resend.dev",
|
|
32
|
+
to,
|
|
33
|
+
subject,
|
|
34
|
+
text,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Resend rejected the email (${response.status}): ${await response.text()}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
2
|
+
|
|
3
|
+
import { headers } from "next/headers";
|
|
4
|
+
|
|
5
|
+
import { auth } from "./auth";
|
|
6
|
+
|
|
7
|
+
// The session of the current request, checked on the server. cache() runs the
|
|
8
|
+
// lookup once per request, however many components ask for it, so each part of
|
|
9
|
+
// a page can read the session inside its own <Suspense> boundary
|
|
10
|
+
export const getSession = cache(async () =>
|
|
11
|
+
auth.api.getSession({ headers: await headers() })
|
|
12
|
+
);
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "auth",
|
|
3
|
-
"name": "Better Auth",
|
|
4
|
-
"description": "Better Auth for TanStack Start with optional Prisma, Drizzle, or MongoDB adapter",
|
|
5
|
-
"type": "feature",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"@hookform/resolvers": "^5.9.2",
|
|
8
|
-
"better-auth": "^1.7.14",
|
|
9
|
-
"dotenv": "^17.4.2",
|
|
10
|
-
"react-hook-form": "^7.91.0",
|
|
11
|
-
"zod": "^4.6.7"
|
|
12
|
-
},
|
|
13
|
-
"devDependencies": {},
|
|
14
|
-
"scripts": {},
|
|
15
|
-
"envVars": {
|
|
16
|
-
"BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
|
|
17
|
-
"BETTER_AUTH_URL": "http://localhost:3000"
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
{
|
|
2
|
+
"id": "auth",
|
|
3
|
+
"name": "Better Auth",
|
|
4
|
+
"description": "Better Auth with email verification, password reset, and GitHub and Google sign-in for TanStack Start with optional Prisma, Drizzle, or MongoDB adapter",
|
|
5
|
+
"type": "feature",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@hookform/resolvers": "^5.9.2",
|
|
8
|
+
"better-auth": "^1.7.14",
|
|
9
|
+
"dotenv": "^17.4.2",
|
|
10
|
+
"react-hook-form": "^7.91.0",
|
|
11
|
+
"zod": "^4.6.7"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {},
|
|
14
|
+
"scripts": {},
|
|
15
|
+
"envVars": {
|
|
16
|
+
"BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
|
|
17
|
+
"BETTER_AUTH_URL": "http://localhost:3000",
|
|
18
|
+
"GITHUB_CLIENT_ID": "",
|
|
19
|
+
"GITHUB_CLIENT_SECRET": "",
|
|
20
|
+
"GOOGLE_CLIENT_ID": "",
|
|
21
|
+
"GOOGLE_CLIENT_SECRET": "",
|
|
22
|
+
"RESEND_API_KEY": "",
|
|
23
|
+
"EMAIL_FROM": "onboarding@resend.dev"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState, useTransition } from "react";
|
|
2
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
3
|
+
import { CircleAlert, CircleCheck } from "lucide-react";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
|
|
6
|
+
import { authClient } from "~/lib/auth-client";
|
|
7
|
+
import {
|
|
8
|
+
changePasswordSchema,
|
|
9
|
+
getAuthErrorMessage,
|
|
10
|
+
type ChangePasswordValues,
|
|
11
|
+
} from "~/lib/auth-schema";
|
|
12
|
+
import { Alert, AlertDescription } from "~/components/ui/alert";
|
|
13
|
+
import { Button } from "~/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
Card,
|
|
16
|
+
CardContent,
|
|
17
|
+
CardDescription,
|
|
18
|
+
CardHeader,
|
|
19
|
+
CardTitle,
|
|
20
|
+
} from "~/components/ui/card";
|
|
21
|
+
import {
|
|
22
|
+
Field,
|
|
23
|
+
FieldError,
|
|
24
|
+
FieldGroup,
|
|
25
|
+
FieldLabel,
|
|
26
|
+
} from "~/components/ui/field";
|
|
27
|
+
import { Input } from "~/components/ui/input";
|
|
28
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
29
|
+
|
|
30
|
+
export function ChangePasswordForm() {
|
|
31
|
+
const [isPending, startTransition] = useTransition();
|
|
32
|
+
const [message, setMessage] = useState<{
|
|
33
|
+
text: string;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
} | null>(null);
|
|
36
|
+
const form = useForm<ChangePasswordValues>({
|
|
37
|
+
resolver: zodResolver(changePasswordSchema),
|
|
38
|
+
defaultValues: { currentPassword: "", newPassword: "" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
42
|
+
setMessage(null);
|
|
43
|
+
|
|
44
|
+
startTransition(async () => {
|
|
45
|
+
// Other devices are signed out; this one stays signed in
|
|
46
|
+
const result = await authClient.changePassword({
|
|
47
|
+
currentPassword: values.currentPassword,
|
|
48
|
+
newPassword: values.newPassword,
|
|
49
|
+
revokeOtherSessions: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (result.error) {
|
|
53
|
+
setMessage({ text: getAuthErrorMessage(result.error), isError: true });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
form.reset();
|
|
58
|
+
setMessage({ text: "Password changed.", isError: false });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle>Change password</CardTitle>
|
|
66
|
+
<CardDescription>
|
|
67
|
+
Other devices are signed out after the change.
|
|
68
|
+
</CardDescription>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
<form onSubmit={onSubmit} noValidate>
|
|
72
|
+
<FieldGroup>
|
|
73
|
+
<Field
|
|
74
|
+
data-invalid={Boolean(form.formState.errors.currentPassword)}
|
|
75
|
+
>
|
|
76
|
+
<FieldLabel htmlFor="currentPassword">Current password</FieldLabel>
|
|
77
|
+
<Input
|
|
78
|
+
id="currentPassword"
|
|
79
|
+
type="password"
|
|
80
|
+
autoComplete="current-password"
|
|
81
|
+
aria-invalid={Boolean(form.formState.errors.currentPassword)}
|
|
82
|
+
{...form.register("currentPassword")}
|
|
83
|
+
/>
|
|
84
|
+
<FieldError errors={[form.formState.errors.currentPassword]} />
|
|
85
|
+
</Field>
|
|
86
|
+
|
|
87
|
+
<Field data-invalid={Boolean(form.formState.errors.newPassword)}>
|
|
88
|
+
<FieldLabel htmlFor="newPassword">New password</FieldLabel>
|
|
89
|
+
<Input
|
|
90
|
+
id="newPassword"
|
|
91
|
+
type="password"
|
|
92
|
+
autoComplete="new-password"
|
|
93
|
+
aria-invalid={Boolean(form.formState.errors.newPassword)}
|
|
94
|
+
{...form.register("newPassword")}
|
|
95
|
+
/>
|
|
96
|
+
<FieldError errors={[form.formState.errors.newPassword]} />
|
|
97
|
+
</Field>
|
|
98
|
+
|
|
99
|
+
{message ? (
|
|
100
|
+
<Alert variant={message.isError ? "destructive" : "default"}>
|
|
101
|
+
{message.isError ? <CircleAlert /> : <CircleCheck />}
|
|
102
|
+
<AlertDescription>{message.text}</AlertDescription>
|
|
103
|
+
</Alert>
|
|
104
|
+
) : null}
|
|
105
|
+
|
|
106
|
+
<Field orientation="horizontal">
|
|
107
|
+
<Button type="submit" disabled={isPending}>
|
|
108
|
+
{isPending ? (
|
|
109
|
+
<>
|
|
110
|
+
<Spinner data-icon="inline-start" />
|
|
111
|
+
Saving
|
|
112
|
+
</>
|
|
113
|
+
) : (
|
|
114
|
+
"Change password"
|
|
115
|
+
)}
|
|
116
|
+
</Button>
|
|
117
|
+
</Field>
|
|
118
|
+
</FieldGroup>
|
|
119
|
+
</form>
|
|
120
|
+
</CardContent>
|
|
121
|
+
</Card>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState, useTransition } from "react";
|
|
2
|
+
import { BadgeCheck, MailWarning } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { authClient } from "~/lib/auth-client";
|
|
5
|
+
import { getAuthErrorMessage } from "~/lib/auth-schema";
|
|
6
|
+
import { Badge } from "~/components/ui/badge";
|
|
7
|
+
import { Button } from "~/components/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardAction,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardDescription,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
} from "~/components/ui/card";
|
|
16
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
17
|
+
|
|
18
|
+
// Shows whether the email address is verified, with a button to send the
|
|
19
|
+
// verification email again
|
|
20
|
+
export function EmailVerificationCard({
|
|
21
|
+
email,
|
|
22
|
+
emailVerified,
|
|
23
|
+
}: {
|
|
24
|
+
email: string;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
}) {
|
|
27
|
+
const [isPending, startTransition] = useTransition();
|
|
28
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
29
|
+
|
|
30
|
+
function resend() {
|
|
31
|
+
setMessage(null);
|
|
32
|
+
startTransition(async () => {
|
|
33
|
+
const result = await authClient.sendVerificationEmail({
|
|
34
|
+
email,
|
|
35
|
+
callbackURL: "/dashboard",
|
|
36
|
+
});
|
|
37
|
+
setMessage(
|
|
38
|
+
result.error
|
|
39
|
+
? getAuthErrorMessage(result.error)
|
|
40
|
+
: "Verification email sent. Without an email provider set up, the link is printed in the server terminal."
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader>
|
|
48
|
+
<CardTitle>
|
|
49
|
+
{emailVerified ? "Email verified" : "Verify your email"}
|
|
50
|
+
</CardTitle>
|
|
51
|
+
<CardDescription>
|
|
52
|
+
{emailVerified
|
|
53
|
+
? `${email} is confirmed.`
|
|
54
|
+
: `We sent a link to ${email}. Open it to confirm the address.`}
|
|
55
|
+
</CardDescription>
|
|
56
|
+
<CardAction>
|
|
57
|
+
{emailVerified ? (
|
|
58
|
+
<Badge variant="secondary">
|
|
59
|
+
<BadgeCheck data-icon="inline-start" />
|
|
60
|
+
Verified
|
|
61
|
+
</Badge>
|
|
62
|
+
) : (
|
|
63
|
+
<Badge variant="outline">
|
|
64
|
+
<MailWarning data-icon="inline-start" />
|
|
65
|
+
Pending
|
|
66
|
+
</Badge>
|
|
67
|
+
)}
|
|
68
|
+
</CardAction>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
{emailVerified ? null : (
|
|
71
|
+
<CardContent className="flex flex-col items-start gap-3">
|
|
72
|
+
<Button variant="outline" onClick={resend} disabled={isPending}>
|
|
73
|
+
{isPending ? (
|
|
74
|
+
<>
|
|
75
|
+
<Spinner data-icon="inline-start" />
|
|
76
|
+
Sending
|
|
77
|
+
</>
|
|
78
|
+
) : (
|
|
79
|
+
"Send the email again"
|
|
80
|
+
)}
|
|
81
|
+
</Button>
|
|
82
|
+
{message ? (
|
|
83
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
84
|
+
) : null}
|
|
85
|
+
</CardContent>
|
|
86
|
+
)}
|
|
87
|
+
</Card>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { useState, useTransition } from "react";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { CircleAlert, MailCheck } from "lucide-react";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { authClient } from "~/lib/auth-client";
|
|
8
|
+
import {
|
|
9
|
+
forgotPasswordSchema,
|
|
10
|
+
getAuthErrorMessage,
|
|
11
|
+
type ForgotPasswordValues,
|
|
12
|
+
} from "~/lib/auth-schema";
|
|
13
|
+
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
|
14
|
+
import { Button } from "~/components/ui/button";
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardContent,
|
|
18
|
+
CardDescription,
|
|
19
|
+
CardHeader,
|
|
20
|
+
CardTitle,
|
|
21
|
+
} from "~/components/ui/card";
|
|
22
|
+
import {
|
|
23
|
+
Field,
|
|
24
|
+
FieldDescription,
|
|
25
|
+
FieldError,
|
|
26
|
+
FieldGroup,
|
|
27
|
+
FieldLabel,
|
|
28
|
+
} from "~/components/ui/field";
|
|
29
|
+
import { Input } from "~/components/ui/input";
|
|
30
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
31
|
+
|
|
32
|
+
export function ForgotPasswordForm() {
|
|
33
|
+
const [isPending, startTransition] = useTransition();
|
|
34
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
35
|
+
const [isSent, setIsSent] = useState(false);
|
|
36
|
+
const form = useForm<ForgotPasswordValues>({
|
|
37
|
+
resolver: zodResolver(forgotPasswordSchema),
|
|
38
|
+
defaultValues: { email: "" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
42
|
+
setAuthError(null);
|
|
43
|
+
|
|
44
|
+
startTransition(async () => {
|
|
45
|
+
// The link in the email opens /reset-password with a one-time token
|
|
46
|
+
const result = await authClient.requestPasswordReset({
|
|
47
|
+
email: values.email,
|
|
48
|
+
redirectTo: "/reset-password",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setIsSent(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Card>
|
|
62
|
+
<CardHeader>
|
|
63
|
+
<CardTitle>Forgot your password?</CardTitle>
|
|
64
|
+
<CardDescription>
|
|
65
|
+
Enter your email and we will send you a link to choose a new one.
|
|
66
|
+
</CardDescription>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent>
|
|
69
|
+
{isSent ? (
|
|
70
|
+
<FieldGroup>
|
|
71
|
+
<Alert>
|
|
72
|
+
<MailCheck />
|
|
73
|
+
<AlertTitle>Check your email</AlertTitle>
|
|
74
|
+
<AlertDescription>
|
|
75
|
+
If an account exists for that email, a reset link is on its
|
|
76
|
+
way. Without an email provider set up, the link is printed in
|
|
77
|
+
the server terminal.
|
|
78
|
+
</AlertDescription>
|
|
79
|
+
</Alert>
|
|
80
|
+
<FieldDescription className="text-center">
|
|
81
|
+
<Link to="/sign-in">Back to login</Link>
|
|
82
|
+
</FieldDescription>
|
|
83
|
+
</FieldGroup>
|
|
84
|
+
) : (
|
|
85
|
+
<form onSubmit={onSubmit} noValidate>
|
|
86
|
+
<FieldGroup>
|
|
87
|
+
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
88
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
89
|
+
<Input
|
|
90
|
+
id="email"
|
|
91
|
+
type="email"
|
|
92
|
+
autoComplete="email"
|
|
93
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
94
|
+
placeholder="you@example.com"
|
|
95
|
+
{...form.register("email")}
|
|
96
|
+
/>
|
|
97
|
+
<FieldDescription>
|
|
98
|
+
Use the email you registered with.
|
|
99
|
+
</FieldDescription>
|
|
100
|
+
<FieldError errors={[form.formState.errors.email]} />
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
{authError ? (
|
|
104
|
+
<Alert variant="destructive">
|
|
105
|
+
<CircleAlert />
|
|
106
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
107
|
+
</Alert>
|
|
108
|
+
) : null}
|
|
109
|
+
|
|
110
|
+
<Field>
|
|
111
|
+
<Button type="submit" disabled={isPending}>
|
|
112
|
+
{isPending ? (
|
|
113
|
+
<>
|
|
114
|
+
<Spinner data-icon="inline-start" />
|
|
115
|
+
Sending link
|
|
116
|
+
</>
|
|
117
|
+
) : (
|
|
118
|
+
"Send reset link"
|
|
119
|
+
)}
|
|
120
|
+
</Button>
|
|
121
|
+
<FieldDescription className="text-center">
|
|
122
|
+
Remembered it? <Link to="/sign-in">Back to login</Link>
|
|
123
|
+
</FieldDescription>
|
|
124
|
+
</Field>
|
|
125
|
+
</FieldGroup>
|
|
126
|
+
</form>
|
|
127
|
+
)}
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
);
|
|
131
|
+
}
|