create-better-t-stack 2.46.4 → 2.47.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/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-El86OdG5.js → src-Wv9qeyOh.js} +139 -35
- package/package.json +1 -1
- package/templates/addons/biome/biome.json.hbs +2 -1
- package/templates/addons/ultracite/biome.json.hbs +1 -0
- package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +8 -0
- package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +48 -0
- package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +7 -0
- package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +12 -0
- package/templates/auth/better-auth/convex/backend/convex/privateData.ts.hbs +16 -0
- package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +3 -0
- package/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs +40 -0
- package/templates/auth/better-auth/convex/web/react/next/src/components/sign-in-form.tsx.hbs +129 -0
- package/templates/auth/better-auth/convex/web/react/next/src/components/sign-up-form.tsx.hbs +154 -0
- package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +48 -0
- package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-client.ts.hbs +6 -0
- package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +6 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-in-form.tsx.hbs +133 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-up-form.tsx.hbs +158 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +50 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/lib/auth-client.ts.hbs +10 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs +43 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-in-form.tsx.hbs +133 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-up-form.tsx.hbs +158 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +50 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-client.ts.hbs +6 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +5 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +11 -0
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs +43 -0
- package/templates/frontend/react/next/src/components/providers.tsx.hbs +8 -0
- package/templates/frontend/react/tanstack-router/src/main.tsx.hbs +8 -1
- package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +47 -0
- package/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs +2 -2
- package/templates/frontend/react/web-base/src/components/header.tsx.hbs +2 -2
- package/templates/auth/better-auth/web/react/next/src/components/theme-provider.tsx.hbs +0 -11
- /package/templates/frontend/react/web-base/src/components/{loader.tsx → loader.tsx.hbs} +0 -0
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -122,11 +122,12 @@ const dependencyVersionMap = {
|
|
|
122
122
|
"@trpc/tanstack-react-query": "^11.5.0",
|
|
123
123
|
"@trpc/server": "^11.5.0",
|
|
124
124
|
"@trpc/client": "^11.5.0",
|
|
125
|
-
convex: "^1.
|
|
125
|
+
convex: "^1.27.0",
|
|
126
126
|
"@convex-dev/react-query": "^0.0.0-alpha.8",
|
|
127
127
|
"convex-svelte": "^0.0.11",
|
|
128
128
|
"convex-nuxt": "0.1.5",
|
|
129
129
|
"convex-vue": "^0.1.5",
|
|
130
|
+
"@convex-dev/better-auth": "^0.8.4",
|
|
130
131
|
"@tanstack/svelte-query": "^5.85.3",
|
|
131
132
|
"@tanstack/svelte-query-devtools": "^5.85.3",
|
|
132
133
|
"@tanstack/vue-query-devtools": "^5.83.0",
|
|
@@ -423,11 +424,10 @@ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
|
|
|
423
424
|
if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
|
424
425
|
}
|
|
425
426
|
}
|
|
426
|
-
function validatePaymentsCompatibility(payments, auth,
|
|
427
|
+
function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
|
|
427
428
|
if (!payments || payments === "none") return;
|
|
428
429
|
if (payments === "polar") {
|
|
429
430
|
if (!auth || auth === "none" || auth !== "better-auth") exitWithError("Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.");
|
|
430
|
-
if (backend === "convex") exitWithError("Polar payments is not compatible with Convex backend. Please use a different backend or choose a different payments provider.");
|
|
431
431
|
const { web } = splitFrontends(frontends);
|
|
432
432
|
if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
|
|
433
433
|
}
|
|
@@ -611,23 +611,39 @@ async function getApiChoice(Api, frontend, backend) {
|
|
|
611
611
|
async function getAuthChoice(auth, hasDatabase, backend, frontend) {
|
|
612
612
|
if (auth !== void 0) return auth;
|
|
613
613
|
if (backend === "convex") {
|
|
614
|
-
const
|
|
615
|
-
"
|
|
616
|
-
"
|
|
617
|
-
"
|
|
614
|
+
const supportedBetterAuthFrontends = frontend?.some((f) => [
|
|
615
|
+
"tanstack-router",
|
|
616
|
+
"tanstack-start",
|
|
617
|
+
"next"
|
|
618
|
+
].includes(f));
|
|
619
|
+
const hasClerkCompatibleFrontends = frontend?.some((f) => [
|
|
620
|
+
"react-router",
|
|
621
|
+
"tanstack-router",
|
|
622
|
+
"tanstack-start",
|
|
623
|
+
"next",
|
|
624
|
+
"native-nativewind",
|
|
625
|
+
"native-unistyles"
|
|
618
626
|
].includes(f));
|
|
619
|
-
|
|
627
|
+
const options = [];
|
|
628
|
+
if (supportedBetterAuthFrontends) options.push({
|
|
629
|
+
value: "better-auth",
|
|
630
|
+
label: "Better-Auth",
|
|
631
|
+
hint: "comprehensive auth framework for TypeScript"
|
|
632
|
+
});
|
|
633
|
+
if (hasClerkCompatibleFrontends) options.push({
|
|
634
|
+
value: "clerk",
|
|
635
|
+
label: "Clerk",
|
|
636
|
+
hint: "More than auth, Complete User Management"
|
|
637
|
+
});
|
|
638
|
+
options.push({
|
|
639
|
+
value: "none",
|
|
640
|
+
label: "None",
|
|
641
|
+
hint: "No auth"
|
|
642
|
+
});
|
|
620
643
|
const response$1 = await select({
|
|
621
644
|
message: "Select authentication provider",
|
|
622
|
-
options
|
|
623
|
-
|
|
624
|
-
label: "Clerk",
|
|
625
|
-
hint: "More than auth, Complete User Management"
|
|
626
|
-
}, {
|
|
627
|
-
value: "none",
|
|
628
|
-
label: "None"
|
|
629
|
-
}],
|
|
630
|
-
initialValue: "clerk"
|
|
645
|
+
options,
|
|
646
|
+
initialValue: "none"
|
|
631
647
|
});
|
|
632
648
|
if (isCancel(response$1)) return exitCancelled("Operation cancelled");
|
|
633
649
|
return response$1;
|
|
@@ -1733,7 +1749,15 @@ function validateConvexConstraints(config, providedFlags) {
|
|
|
1733
1749
|
if (has("api") && config.api !== "none") exitWithError("Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.");
|
|
1734
1750
|
if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
|
|
1735
1751
|
if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
|
|
1736
|
-
if (has("auth") && config.auth === "better-auth")
|
|
1752
|
+
if (has("auth") && config.auth === "better-auth") {
|
|
1753
|
+
const supportedFrontends = [
|
|
1754
|
+
"tanstack-router",
|
|
1755
|
+
"tanstack-start",
|
|
1756
|
+
"next"
|
|
1757
|
+
];
|
|
1758
|
+
const hasSupportedFrontend = config.frontend?.some((f) => supportedFrontends.includes(f));
|
|
1759
|
+
if (!hasSupportedFrontend) exitWithError("Better-Auth with Convex backend is only supported with TanStack Router, TanStack Start, or Next.js frontends. Please use '--auth clerk' or '--auth none'.");
|
|
1760
|
+
}
|
|
1737
1761
|
}
|
|
1738
1762
|
function validateBackendNoneConstraints(config, providedFlags) {
|
|
1739
1763
|
const { backend } = config;
|
|
@@ -1980,18 +2004,18 @@ async function updateBtsConfig(projectDir, updates) {
|
|
|
1980
2004
|
//#endregion
|
|
1981
2005
|
//#region src/utils/add-package-deps.ts
|
|
1982
2006
|
const addPackageDependency = async (opts) => {
|
|
1983
|
-
const { dependencies = [], devDependencies = [], projectDir } = opts;
|
|
2007
|
+
const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
|
|
1984
2008
|
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
1985
2009
|
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
1986
2010
|
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
1987
2011
|
if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
|
|
1988
2012
|
for (const pkgName of dependencies) {
|
|
1989
|
-
const version = dependencyVersionMap[pkgName];
|
|
2013
|
+
const version = customDependencies[pkgName] || dependencyVersionMap[pkgName];
|
|
1990
2014
|
if (version) pkgJson.dependencies[pkgName] = version;
|
|
1991
2015
|
else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
|
|
1992
2016
|
}
|
|
1993
2017
|
for (const pkgName of devDependencies) {
|
|
1994
|
-
const version = dependencyVersionMap[pkgName];
|
|
2018
|
+
const version = customDevDependencies[pkgName] || dependencyVersionMap[pkgName];
|
|
1995
2019
|
if (version) pkgJson.devDependencies[pkgName] = version;
|
|
1996
2020
|
else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
|
|
1997
2021
|
}
|
|
@@ -2853,6 +2877,29 @@ async function setupAuthTemplate(projectDir, context) {
|
|
|
2853
2877
|
}
|
|
2854
2878
|
return;
|
|
2855
2879
|
}
|
|
2880
|
+
if (context.backend === "convex" && authProvider === "better-auth") {
|
|
2881
|
+
const convexBackendDestDir = path.join(projectDir, "packages/backend");
|
|
2882
|
+
const convexBetterAuthBackendSrc = path.join(PKG_ROOT, "templates/auth/better-auth/convex/backend");
|
|
2883
|
+
if (await fs.pathExists(convexBetterAuthBackendSrc)) {
|
|
2884
|
+
await fs.ensureDir(convexBackendDestDir);
|
|
2885
|
+
await processAndCopyFiles("**/*", convexBetterAuthBackendSrc, convexBackendDestDir, context);
|
|
2886
|
+
}
|
|
2887
|
+
if (webAppDirExists && hasReactWeb) {
|
|
2888
|
+
const convexBetterAuthWebBaseSrc = path.join(PKG_ROOT, "templates/auth/better-auth/convex/web/react/base");
|
|
2889
|
+
if (await fs.pathExists(convexBetterAuthWebBaseSrc)) await processAndCopyFiles("**/*", convexBetterAuthWebBaseSrc, webAppDir, context);
|
|
2890
|
+
const reactFramework = context.frontend.find((f) => [
|
|
2891
|
+
"tanstack-router",
|
|
2892
|
+
"react-router",
|
|
2893
|
+
"tanstack-start",
|
|
2894
|
+
"next"
|
|
2895
|
+
].includes(f));
|
|
2896
|
+
if (reactFramework) {
|
|
2897
|
+
const convexBetterAuthWebSrc = path.join(PKG_ROOT, `templates/auth/better-auth/convex/web/react/${reactFramework}`);
|
|
2898
|
+
if (await fs.pathExists(convexBetterAuthWebSrc)) await processAndCopyFiles("**/*", convexBetterAuthWebSrc, webAppDir, context);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2856
2903
|
if (serverAppDirExists && context.backend !== "convex") {
|
|
2857
2904
|
const authServerBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/base`);
|
|
2858
2905
|
if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
|
|
@@ -4246,6 +4293,35 @@ async function setupAuth(config) {
|
|
|
4246
4293
|
projectDir: clientDir
|
|
4247
4294
|
});
|
|
4248
4295
|
}
|
|
4296
|
+
if (auth === "better-auth") {
|
|
4297
|
+
const convexBackendDir = path.join(projectDir, "packages/backend");
|
|
4298
|
+
const convexBackendDirExists = await fs.pathExists(convexBackendDir);
|
|
4299
|
+
if (convexBackendDirExists) await addPackageDependency({
|
|
4300
|
+
dependencies: ["better-auth", "@convex-dev/better-auth"],
|
|
4301
|
+
customDependencies: { "better-auth": "1.3.8" },
|
|
4302
|
+
projectDir: convexBackendDir
|
|
4303
|
+
});
|
|
4304
|
+
if (clientDirExists) {
|
|
4305
|
+
const hasNextJs = frontend.includes("next");
|
|
4306
|
+
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
4307
|
+
const hasViteReactOther = frontend.some((f) => ["tanstack-router", "react-router"].includes(f));
|
|
4308
|
+
if (hasNextJs) await addPackageDependency({
|
|
4309
|
+
dependencies: ["better-auth", "@convex-dev/better-auth"],
|
|
4310
|
+
customDependencies: { "better-auth": "1.3.8" },
|
|
4311
|
+
projectDir: clientDir
|
|
4312
|
+
});
|
|
4313
|
+
else if (hasTanStackStart) await addPackageDependency({
|
|
4314
|
+
dependencies: ["better-auth", "@convex-dev/better-auth"],
|
|
4315
|
+
customDependencies: { "better-auth": "1.3.8" },
|
|
4316
|
+
projectDir: clientDir
|
|
4317
|
+
});
|
|
4318
|
+
else if (hasViteReactOther) await addPackageDependency({
|
|
4319
|
+
dependencies: ["better-auth", "@convex-dev/better-auth"],
|
|
4320
|
+
customDependencies: { "better-auth": "1.3.8" },
|
|
4321
|
+
projectDir: clientDir
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4249
4325
|
const hasNativeWind = frontend.includes("native-nativewind");
|
|
4250
4326
|
const hasUnistyles = frontend.includes("native-unistyles");
|
|
4251
4327
|
if (auth === "clerk" && nativeDirExists && (hasNativeWind || hasUnistyles)) await addPackageDependency({
|
|
@@ -4307,7 +4383,7 @@ async function addEnvVariablesToFile(filePath, variables) {
|
|
|
4307
4383
|
let modified = false;
|
|
4308
4384
|
let contentToAdd = "";
|
|
4309
4385
|
const exampleVariables = [];
|
|
4310
|
-
for (const { key, value, condition } of variables) if (condition) {
|
|
4386
|
+
for (const { key, value, condition, comment } of variables) if (condition) {
|
|
4311
4387
|
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
4312
4388
|
const valueToWrite = value ?? "";
|
|
4313
4389
|
exampleVariables.push(`${key}=`);
|
|
@@ -4318,6 +4394,7 @@ async function addEnvVariablesToFile(filePath, variables) {
|
|
|
4318
4394
|
modified = true;
|
|
4319
4395
|
}
|
|
4320
4396
|
} else {
|
|
4397
|
+
if (comment) contentToAdd += `# ${comment}\n`;
|
|
4321
4398
|
contentToAdd += `${key}=${valueToWrite}\n`;
|
|
4322
4399
|
modified = true;
|
|
4323
4400
|
}
|
|
@@ -4403,6 +4480,18 @@ async function setupEnvironmentVariables(config) {
|
|
|
4403
4480
|
});
|
|
4404
4481
|
}
|
|
4405
4482
|
}
|
|
4483
|
+
if (backend === "convex" && auth === "better-auth") {
|
|
4484
|
+
if (hasNextJs) clientVars.push({
|
|
4485
|
+
key: "NEXT_PUBLIC_CONVEX_SITE_URL",
|
|
4486
|
+
value: "https://<YOUR_CONVEX_URL>",
|
|
4487
|
+
condition: true
|
|
4488
|
+
});
|
|
4489
|
+
else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) clientVars.push({
|
|
4490
|
+
key: "VITE_CONVEX_SITE_URL",
|
|
4491
|
+
value: "https://<YOUR_CONVEX_URL>",
|
|
4492
|
+
condition: true
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4406
4495
|
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
|
4407
4496
|
}
|
|
4408
4497
|
}
|
|
@@ -4428,7 +4517,34 @@ async function setupEnvironmentVariables(config) {
|
|
|
4428
4517
|
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
|
4429
4518
|
}
|
|
4430
4519
|
}
|
|
4431
|
-
if (backend === "convex")
|
|
4520
|
+
if (backend === "convex") {
|
|
4521
|
+
if (auth === "better-auth") {
|
|
4522
|
+
const convexBackendDir = path.join(projectDir, "packages/backend");
|
|
4523
|
+
if (await fs.pathExists(convexBackendDir)) {
|
|
4524
|
+
const envLocalPath = path.join(convexBackendDir, ".env.local");
|
|
4525
|
+
if (!await fs.pathExists(envLocalPath) || !(await fs.readFile(envLocalPath, "utf8")).includes("npx convex env set")) {
|
|
4526
|
+
const convexCommands = `# Set Convex environment variables
|
|
4527
|
+
npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
|
|
4528
|
+
npx convex env set SITE_URL http://localhost:3001
|
|
4529
|
+
|
|
4530
|
+
`;
|
|
4531
|
+
await fs.appendFile(envLocalPath, convexCommands);
|
|
4532
|
+
}
|
|
4533
|
+
const convexBackendVars = [{
|
|
4534
|
+
key: hasNextJs ? "NEXT_PUBLIC_CONVEX_SITE_URL" : "VITE_CONVEX_SITE_URL",
|
|
4535
|
+
value: "",
|
|
4536
|
+
condition: true,
|
|
4537
|
+
comment: "Same as CONVEX_URL but ends in .site"
|
|
4538
|
+
}, {
|
|
4539
|
+
key: "SITE_URL",
|
|
4540
|
+
value: "http://localhost:3001",
|
|
4541
|
+
condition: true
|
|
4542
|
+
}];
|
|
4543
|
+
await addEnvVariablesToFile(envLocalPath, convexBackendVars);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4432
4548
|
const serverDir = path.join(projectDir, "apps/server");
|
|
4433
4549
|
if (!await fs.pathExists(serverDir)) return;
|
|
4434
4550
|
const envPath = path.join(serverDir, ".env");
|
|
@@ -5713,17 +5829,6 @@ async function setupNodeRuntime(serverDir, backend) {
|
|
|
5713
5829
|
});
|
|
5714
5830
|
}
|
|
5715
5831
|
|
|
5716
|
-
//#endregion
|
|
5717
|
-
//#region src/helpers/core/convex-codegen.ts
|
|
5718
|
-
async function runConvexCodegen(projectDir, packageManager) {
|
|
5719
|
-
const backendDir = path.join(projectDir, "packages/backend");
|
|
5720
|
-
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
|
5721
|
-
await execa(cmd, {
|
|
5722
|
-
cwd: backendDir,
|
|
5723
|
-
shell: true
|
|
5724
|
-
});
|
|
5725
|
-
}
|
|
5726
|
-
|
|
5727
5832
|
//#endregion
|
|
5728
5833
|
//#region src/helpers/core/create-readme.ts
|
|
5729
5834
|
async function createReadme(projectDir, options) {
|
|
@@ -6515,7 +6620,6 @@ async function createProject(options, cliInput) {
|
|
|
6515
6620
|
await setupServerDeploy(options);
|
|
6516
6621
|
await createReadme(projectDir, options);
|
|
6517
6622
|
await writeBtsConfig(options);
|
|
6518
|
-
if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
|
|
6519
6623
|
log.success("Project template successfully scaffolded!");
|
|
6520
6624
|
if (options.install) await installDependencies({
|
|
6521
6625
|
projectDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.47.0",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"!**/.expo",
|
|
23
23
|
"!**/.wrangler",
|
|
24
24
|
"!**/.alchemy",
|
|
25
|
+
"!**/.svelte-kit",
|
|
25
26
|
"!**/wrangler.jsonc",
|
|
26
27
|
"!**/.source"
|
|
27
28
|
]
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"quoteStyle": "double"
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
{{#if (or (
|
|
70
|
+
{{#if (or (includes frontend "svelte") (includes frontend "nuxt"))}}
|
|
70
71
|
,
|
|
71
72
|
"overrides": [
|
|
72
73
|
{
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
|
|
2
|
+
{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
|
|
3
|
+
import { convex } from "@convex-dev/better-auth/plugins";
|
|
4
|
+
{{else}}
|
|
5
|
+
import { convex, crossDomain } from "@convex-dev/better-auth/plugins";
|
|
6
|
+
{{/if}}
|
|
7
|
+
import { components } from "./_generated/api";
|
|
8
|
+
import { DataModel } from "./_generated/dataModel";
|
|
9
|
+
import { query } from "./_generated/server";
|
|
10
|
+
import { betterAuth } from "better-auth";
|
|
11
|
+
|
|
12
|
+
const siteUrl = process.env.SITE_URL!;
|
|
13
|
+
|
|
14
|
+
export const authComponent = createClient<DataModel>(components.betterAuth);
|
|
15
|
+
|
|
16
|
+
export const createAuth = (
|
|
17
|
+
ctx: GenericCtx<DataModel>,
|
|
18
|
+
{ optionsOnly } = { optionsOnly: false },
|
|
19
|
+
) => {
|
|
20
|
+
return betterAuth({
|
|
21
|
+
logger: {
|
|
22
|
+
disabled: optionsOnly,
|
|
23
|
+
},
|
|
24
|
+
{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
|
|
25
|
+
baseUrl: siteUrl,
|
|
26
|
+
{{else}}
|
|
27
|
+
trustedOrigins: [siteUrl],
|
|
28
|
+
{{/if}}
|
|
29
|
+
database: authComponent.adapter(ctx),
|
|
30
|
+
emailAndPassword: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
requireEmailVerification: false,
|
|
33
|
+
},
|
|
34
|
+
plugins: [
|
|
35
|
+
{{#unless (or (includes frontend "tanstack-start") (includes frontend "next"))}}
|
|
36
|
+
crossDomain({ siteUrl }),
|
|
37
|
+
{{/unless}}
|
|
38
|
+
convex(),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getCurrentUser = query({
|
|
44
|
+
args: {},
|
|
45
|
+
handler: async (ctx) => {
|
|
46
|
+
return authComponent.getAuthUser(ctx);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { httpRouter } from "convex/server";
|
|
2
|
+
import { authComponent, createAuth } from "./auth";
|
|
3
|
+
|
|
4
|
+
const http = httpRouter();
|
|
5
|
+
|
|
6
|
+
{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
|
|
7
|
+
authComponent.registerRoutes(http, createAuth);
|
|
8
|
+
{{else}}
|
|
9
|
+
authComponent.registerRoutes(http, createAuth, { cors: true });
|
|
10
|
+
{{/if}}
|
|
11
|
+
|
|
12
|
+
export default http;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { query } from "./_generated/server";
|
|
2
|
+
|
|
3
|
+
export const get = query({
|
|
4
|
+
args: {},
|
|
5
|
+
handler: async (ctx) => {
|
|
6
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
7
|
+
if (identity === null) {
|
|
8
|
+
return {
|
|
9
|
+
message: "Not authenticated",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
message: "This is private",
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import SignInForm from "@/components/sign-in-form";
|
|
4
|
+
import SignUpForm from "@/components/sign-up-form";
|
|
5
|
+
import UserMenu from "@/components/user-menu";
|
|
6
|
+
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
|
7
|
+
import {
|
|
8
|
+
Authenticated,
|
|
9
|
+
AuthLoading,
|
|
10
|
+
Unauthenticated,
|
|
11
|
+
useQuery,
|
|
12
|
+
} from "convex/react";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
|
|
15
|
+
export default function DashboardPage() {
|
|
16
|
+
const [showSignIn, setShowSignIn] = useState(false);
|
|
17
|
+
const privateData = useQuery(api.privateData.get);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<Authenticated>
|
|
22
|
+
<div>
|
|
23
|
+
<h1>Dashboard</h1>
|
|
24
|
+
<p>privateData: {privateData?.message}</p>
|
|
25
|
+
<UserMenu />
|
|
26
|
+
</div>
|
|
27
|
+
</Authenticated>
|
|
28
|
+
<Unauthenticated>
|
|
29
|
+
{showSignIn ? (
|
|
30
|
+
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
|
|
31
|
+
) : (
|
|
32
|
+
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
|
|
33
|
+
)}
|
|
34
|
+
</Unauthenticated>
|
|
35
|
+
<AuthLoading>
|
|
36
|
+
<div>Loading...</div>
|
|
37
|
+
</AuthLoading>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { useForm } from "@tanstack/react-form";
|
|
3
|
+
import { toast } from "sonner";
|
|
4
|
+
import z from "zod";
|
|
5
|
+
import { Button } from "./ui/button";
|
|
6
|
+
import { Input } from "./ui/input";
|
|
7
|
+
import { Label } from "./ui/label";
|
|
8
|
+
import { useRouter } from "next/navigation";
|
|
9
|
+
|
|
10
|
+
export default function SignInForm({
|
|
11
|
+
onSwitchToSignUp,
|
|
12
|
+
}: {
|
|
13
|
+
onSwitchToSignUp: () => void;
|
|
14
|
+
}) {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
|
|
17
|
+
const form = useForm({
|
|
18
|
+
defaultValues: {
|
|
19
|
+
email: "",
|
|
20
|
+
password: "",
|
|
21
|
+
},
|
|
22
|
+
onSubmit: async ({ value }) => {
|
|
23
|
+
await authClient.signIn.email(
|
|
24
|
+
{
|
|
25
|
+
email: value.email,
|
|
26
|
+
password: value.password,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
onSuccess: () => {
|
|
30
|
+
router.push("/dashboard");
|
|
31
|
+
toast.success("Sign in successful");
|
|
32
|
+
},
|
|
33
|
+
onError: (error) => {
|
|
34
|
+
toast.error(error.error.message || error.error.statusText);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
validators: {
|
|
40
|
+
onSubmit: z.object({
|
|
41
|
+
email: z.email("Invalid email address"),
|
|
42
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
|
49
|
+
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
|
|
50
|
+
|
|
51
|
+
<form
|
|
52
|
+
onSubmit={(e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
form.handleSubmit();
|
|
56
|
+
}}
|
|
57
|
+
className="space-y-4"
|
|
58
|
+
>
|
|
59
|
+
<div>
|
|
60
|
+
<form.Field name="email">
|
|
61
|
+
{(field) => (
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
<Label htmlFor={field.name}>Email</Label>
|
|
64
|
+
<Input
|
|
65
|
+
id={field.name}
|
|
66
|
+
name={field.name}
|
|
67
|
+
type="email"
|
|
68
|
+
value={field.state.value}
|
|
69
|
+
onBlur={field.handleBlur}
|
|
70
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
71
|
+
/>
|
|
72
|
+
{field.state.meta.errors.map((error) => (
|
|
73
|
+
<p key={error?.message} className="text-red-500">
|
|
74
|
+
{error?.message}
|
|
75
|
+
</p>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</form.Field>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div>
|
|
83
|
+
<form.Field name="password">
|
|
84
|
+
{(field) => (
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<Label htmlFor={field.name}>Password</Label>
|
|
87
|
+
<Input
|
|
88
|
+
id={field.name}
|
|
89
|
+
name={field.name}
|
|
90
|
+
type="password"
|
|
91
|
+
value={field.state.value}
|
|
92
|
+
onBlur={field.handleBlur}
|
|
93
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
94
|
+
/>
|
|
95
|
+
{field.state.meta.errors.map((error) => (
|
|
96
|
+
<p key={error?.message} className="text-red-500">
|
|
97
|
+
{error?.message}
|
|
98
|
+
</p>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</form.Field>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<form.Subscribe>
|
|
106
|
+
{(state) => (
|
|
107
|
+
<Button
|
|
108
|
+
type="submit"
|
|
109
|
+
className="w-full"
|
|
110
|
+
disabled={!state.canSubmit || state.isSubmitting}
|
|
111
|
+
>
|
|
112
|
+
{state.isSubmitting ? "Submitting..." : "Sign In"}
|
|
113
|
+
</Button>
|
|
114
|
+
)}
|
|
115
|
+
</form.Subscribe>
|
|
116
|
+
</form>
|
|
117
|
+
|
|
118
|
+
<div className="mt-4 text-center">
|
|
119
|
+
<Button
|
|
120
|
+
variant="link"
|
|
121
|
+
onClick={onSwitchToSignUp}
|
|
122
|
+
className="text-indigo-600 hover:text-indigo-800"
|
|
123
|
+
>
|
|
124
|
+
Need an account? Sign Up
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|