create-better-t-stack 2.45.4 → 2.46.0-canary.85c43fef

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.
Files changed (75) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.js +1 -1
  4. package/dist/{src-Cun9EO6e.js → src-lN80CwOs.js} +396 -162
  5. package/package.json +1 -1
  6. package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +8 -0
  7. package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +48 -0
  8. package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +7 -0
  9. package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +12 -0
  10. package/templates/auth/better-auth/convex/backend/convex/privateData.ts.hbs +16 -0
  11. package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +3 -0
  12. package/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs +40 -0
  13. package/templates/auth/better-auth/convex/web/react/next/src/components/sign-in-form.tsx.hbs +129 -0
  14. package/templates/auth/better-auth/convex/web/react/next/src/components/sign-up-form.tsx.hbs +154 -0
  15. package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +48 -0
  16. package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-client.ts.hbs +6 -0
  17. package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +6 -0
  18. package/templates/auth/better-auth/convex/web/react/tanstack-router/src/lib/auth-client.ts.hbs +10 -0
  19. package/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs +43 -0
  20. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-in-form.tsx.hbs +133 -0
  21. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-up-form.tsx.hbs +158 -0
  22. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +50 -0
  23. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-client.ts.hbs +6 -0
  24. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +5 -0
  25. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +11 -0
  26. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs +43 -0
  27. package/templates/auth/better-auth/server/base/src/lib/auth.ts.hbs +163 -16
  28. package/templates/auth/better-auth/web/nuxt/app/pages/dashboard.vue.hbs +36 -2
  29. package/templates/auth/better-auth/web/nuxt/app/plugins/auth-client.ts.hbs +22 -0
  30. package/templates/auth/better-auth/web/react/base/src/lib/auth-client.ts.hbs +6 -0
  31. package/templates/auth/better-auth/web/react/next/src/app/dashboard/dashboard.tsx.hbs +58 -0
  32. package/templates/auth/better-auth/web/react/next/src/app/dashboard/page.tsx.hbs +31 -41
  33. package/templates/auth/better-auth/web/react/react-router/src/routes/dashboard.tsx.hbs +37 -3
  34. package/templates/auth/better-auth/web/react/tanstack-router/src/routes/dashboard.tsx.hbs +50 -32
  35. package/templates/auth/better-auth/web/react/tanstack-start/src/routes/dashboard.tsx.hbs +51 -36
  36. package/templates/auth/better-auth/web/solid/src/lib/auth-client.ts.hbs +11 -0
  37. package/templates/auth/better-auth/web/solid/src/routes/dashboard.tsx.hbs +52 -29
  38. package/templates/auth/better-auth/web/svelte/src/lib/{auth-client.ts → auth-client.ts.hbs} +6 -0
  39. package/templates/auth/better-auth/web/svelte/src/routes/dashboard/+page.svelte.hbs +24 -3
  40. package/templates/backend/server/server-base/package.json.hbs +1 -1
  41. package/templates/extras/bunfig.toml.hbs +4 -0
  42. package/templates/frontend/react/next/src/components/providers.tsx.hbs +8 -0
  43. package/templates/frontend/react/tanstack-router/src/main.tsx.hbs +8 -1
  44. package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +51 -0
  45. package/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs +2 -2
  46. package/templates/frontend/react/web-base/src/components/header.tsx.hbs +2 -2
  47. package/templates/frontend/solid/package.json.hbs +10 -10
  48. package/templates/payments/polar/server/base/src/lib/payments.ts.hbs +6 -0
  49. package/templates/payments/polar/web/nuxt/app/pages/success.vue.hbs +11 -0
  50. package/templates/payments/polar/web/react/next/src/app/success/page.tsx.hbs +15 -0
  51. package/templates/payments/polar/web/react/react-router/src/routes/success.tsx.hbs +13 -0
  52. package/templates/payments/polar/web/react/tanstack-router/src/routes/success.tsx.hbs +19 -0
  53. package/templates/payments/polar/web/react/tanstack-start/src/routes/success.tsx.hbs +19 -0
  54. package/templates/payments/polar/web/solid/src/routes/success.tsx.hbs +23 -0
  55. package/templates/payments/polar/web/svelte/src/routes/success/+page.svelte.hbs +12 -0
  56. package/templates/auth/better-auth/web/nuxt/app/plugins/auth-client.ts +0 -16
  57. package/templates/auth/better-auth/web/react/next/src/components/theme-provider.tsx +0 -11
  58. package/templates/auth/better-auth/web/solid/src/lib/auth-client.ts +0 -5
  59. /package/templates/auth/better-auth/web/nuxt/app/components/{SignInForm.vue → SignInForm.vue.hbs} +0 -0
  60. /package/templates/auth/better-auth/web/nuxt/app/components/{SignUpForm.vue → SignUpForm.vue.hbs} +0 -0
  61. /package/templates/auth/better-auth/web/nuxt/app/components/{UserMenu.vue → UserMenu.vue.hbs} +0 -0
  62. /package/templates/auth/better-auth/web/nuxt/app/pages/{login.vue → login.vue.hbs} +0 -0
  63. /package/templates/auth/better-auth/web/react/next/src/app/login/{page.tsx → page.tsx.hbs} +0 -0
  64. /package/templates/auth/better-auth/web/react/next/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  65. /package/templates/auth/better-auth/web/react/next/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  66. /package/templates/auth/better-auth/web/react/next/src/components/{user-menu.tsx → user-menu.tsx.hbs} +0 -0
  67. /package/templates/auth/better-auth/web/react/react-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  68. /package/templates/auth/better-auth/web/react/react-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  69. /package/templates/auth/better-auth/web/react/react-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +0 -0
  70. /package/templates/auth/better-auth/web/react/react-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  71. /package/templates/auth/better-auth/web/svelte/src/components/{SignInForm.svelte → SignInForm.svelte.hbs} +0 -0
  72. /package/templates/auth/better-auth/web/svelte/src/components/{SignUpForm.svelte → SignUpForm.svelte.hbs} +0 -0
  73. /package/templates/auth/better-auth/web/svelte/src/components/{UserMenu.svelte → UserMenu.svelte.hbs} +0 -0
  74. /package/templates/auth/better-auth/web/svelte/src/routes/login/{+page.svelte → +page.svelte.hbs} +0 -0
  75. /package/templates/frontend/react/web-base/src/components/{loader.tsx → loader.tsx.hbs} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.45.4",
3
+ "version": "2.46.0-canary.85c43fef",
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",
@@ -0,0 +1,8 @@
1
+ export default {
2
+ providers: [
3
+ {
4
+ domain: process.env.CONVEX_SITE_URL,
5
+ applicationID: "convex",
6
+ },
7
+ ],
8
+ };
@@ -0,0 +1,48 @@
1
+ import { createClient, type GenericCtx } from "@convex-dev/better-auth";
2
+ {{#if (or (eq frontend "tanstack-start") (eq 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 (eq frontend "tanstack-start") (eq 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 (eq frontend "tanstack-start") (eq 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,7 @@
1
+ import { defineApp } from "convex/server";
2
+ import betterAuth from "@convex-dev/better-auth/convex.config";
3
+
4
+ const app = defineApp();
5
+ app.use(betterAuth);
6
+
7
+ export default app;
@@ -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 (eq frontend "tanstack-start") (eq 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,3 @@
1
+ import { nextJsHandler } from "@convex-dev/better-auth/nextjs";
2
+
3
+ export const { GET, POST } = nextJsHandler();
@@ -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
+ }
@@ -0,0 +1,154 @@
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 SignUpForm({
11
+ onSwitchToSignIn,
12
+ }: {
13
+ onSwitchToSignIn: () => void;
14
+ }) {
15
+ const router = useRouter();
16
+
17
+ const form = useForm({
18
+ defaultValues: {
19
+ email: "",
20
+ password: "",
21
+ name: "",
22
+ },
23
+ onSubmit: async ({ value }) => {
24
+ await authClient.signUp.email(
25
+ {
26
+ email: value.email,
27
+ password: value.password,
28
+ name: value.name,
29
+ },
30
+ {
31
+ onSuccess: () => {
32
+ router.push("/dashboard");
33
+ toast.success("Sign up successful");
34
+ },
35
+ onError: (error) => {
36
+ toast.error(error.error.message || error.error.statusText);
37
+ },
38
+ },
39
+ );
40
+ },
41
+ validators: {
42
+ onSubmit: z.object({
43
+ name: z.string().min(2, "Name must be at least 2 characters"),
44
+ email: z.email("Invalid email address"),
45
+ password: z.string().min(8, "Password must be at least 8 characters"),
46
+ }),
47
+ },
48
+ });
49
+
50
+ return (
51
+ <div className="mx-auto w-full mt-10 max-w-md p-6">
52
+ <h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
53
+
54
+ <form
55
+ onSubmit={(e) => {
56
+ e.preventDefault();
57
+ e.stopPropagation();
58
+ form.handleSubmit();
59
+ }}
60
+ className="space-y-4"
61
+ >
62
+ <div>
63
+ <form.Field name="name">
64
+ {(field) => (
65
+ <div className="space-y-2">
66
+ <Label htmlFor={field.name}>Name</Label>
67
+ <Input
68
+ id={field.name}
69
+ name={field.name}
70
+ value={field.state.value}
71
+ onBlur={field.handleBlur}
72
+ onChange={(e) => field.handleChange(e.target.value)}
73
+ />
74
+ {field.state.meta.errors.map((error) => (
75
+ <p key={error?.message} className="text-red-500">
76
+ {error?.message}
77
+ </p>
78
+ ))}
79
+ </div>
80
+ )}
81
+ </form.Field>
82
+ </div>
83
+
84
+ <div>
85
+ <form.Field name="email">
86
+ {(field) => (
87
+ <div className="space-y-2">
88
+ <Label htmlFor={field.name}>Email</Label>
89
+ <Input
90
+ id={field.name}
91
+ name={field.name}
92
+ type="email"
93
+ value={field.state.value}
94
+ onBlur={field.handleBlur}
95
+ onChange={(e) => field.handleChange(e.target.value)}
96
+ />
97
+ {field.state.meta.errors.map((error) => (
98
+ <p key={error?.message} className="text-red-500">
99
+ {error?.message}
100
+ </p>
101
+ ))}
102
+ </div>
103
+ )}
104
+ </form.Field>
105
+ </div>
106
+
107
+ <div>
108
+ <form.Field name="password">
109
+ {(field) => (
110
+ <div className="space-y-2">
111
+ <Label htmlFor={field.name}>Password</Label>
112
+ <Input
113
+ id={field.name}
114
+ name={field.name}
115
+ type="password"
116
+ value={field.state.value}
117
+ onBlur={field.handleBlur}
118
+ onChange={(e) => field.handleChange(e.target.value)}
119
+ />
120
+ {field.state.meta.errors.map((error) => (
121
+ <p key={error?.message} className="text-red-500">
122
+ {error?.message}
123
+ </p>
124
+ ))}
125
+ </div>
126
+ )}
127
+ </form.Field>
128
+ </div>
129
+
130
+ <form.Subscribe>
131
+ {(state) => (
132
+ <Button
133
+ type="submit"
134
+ className="w-full"
135
+ disabled={!state.canSubmit || state.isSubmitting}
136
+ >
137
+ {state.isSubmitting ? "Submitting..." : "Sign Up"}
138
+ </Button>
139
+ )}
140
+ </form.Subscribe>
141
+ </form>
142
+
143
+ <div className="mt-4 text-center">
144
+ <Button
145
+ variant="link"
146
+ onClick={onSwitchToSignIn}
147
+ className="text-indigo-600 hover:text-indigo-800"
148
+ >
149
+ Already have an account? Sign In
150
+ </Button>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ DropdownMenu,
3
+ DropdownMenuContent,
4
+ DropdownMenuItem,
5
+ DropdownMenuLabel,
6
+ DropdownMenuSeparator,
7
+ DropdownMenuTrigger,
8
+ } from "@/components/ui/dropdown-menu";
9
+ import { authClient } from "@/lib/auth-client";
10
+ import { Button } from "./ui/button";
11
+ import { useRouter } from "next/navigation";
12
+ import { useQuery } from "convex/react";
13
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
14
+
15
+ export default function UserMenu() {
16
+ const router = useRouter();
17
+ const user = useQuery(api.auth.getCurrentUser)
18
+
19
+ return (
20
+ <DropdownMenu>
21
+ <DropdownMenuTrigger asChild>
22
+ <Button variant="outline">{user?.name}</Button>
23
+ </DropdownMenuTrigger>
24
+ <DropdownMenuContent className="bg-card">
25
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
26
+ <DropdownMenuSeparator />
27
+ <DropdownMenuItem>{user?.email}</DropdownMenuItem>
28
+ <DropdownMenuItem asChild>
29
+ <Button
30
+ variant="destructive"
31
+ className="w-full"
32
+ onClick={() => {
33
+ authClient.signOut({
34
+ fetchOptions: {
35
+ onSuccess: () => {
36
+ router.push("/dashboard");
37
+ },
38
+ },
39
+ });
40
+ }}
41
+ >
42
+ Sign Out
43
+ </Button>
44
+ </DropdownMenuItem>
45
+ </DropdownMenuContent>
46
+ </DropdownMenu>
47
+ );
48
+ }
@@ -0,0 +1,6 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+ import { convexClient } from "@convex-dev/better-auth/client/plugins";
3
+
4
+ export const authClient = createAuthClient({
5
+ plugins: [convexClient()],
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createAuth } from "@{{projectName}}/backend/convex/auth";
2
+ import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs";
3
+
4
+ export const getToken = () => {
5
+ return getTokenNextjs(createAuth);
6
+ };
@@ -0,0 +1,10 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+ import {
3
+ convexClient,
4
+ crossDomainClient,
5
+ } from "@convex-dev/better-auth/client/plugins";
6
+
7
+ export const authClient = createAuthClient({
8
+ baseURL: import.meta.env.VITE_CONVEX_SITE_URL,
9
+ plugins: [convexClient(), crossDomainClient()],
10
+ });
@@ -0,0 +1,43 @@
1
+ import SignInForm from "@/components/sign-in-form";
2
+ import SignUpForm from "@/components/sign-up-form";
3
+ import UserMenu from "@/components/user-menu";
4
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
5
+ import { createFileRoute } from "@tanstack/react-router";
6
+ import {
7
+ Authenticated,
8
+ AuthLoading,
9
+ Unauthenticated,
10
+ useQuery,
11
+ } from "convex/react";
12
+ import { useState } from "react";
13
+
14
+ export const Route = createFileRoute("/dashboard")({
15
+ component: RouteComponent,
16
+ });
17
+
18
+ function RouteComponent() {
19
+ const [showSignIn, setShowSignIn] = useState(false);
20
+ const privateData = useQuery(api.privateData.get);
21
+
22
+ return (
23
+ <>
24
+ <Authenticated>
25
+ <div>
26
+ <h1>Dashboard</h1>
27
+ <p>privateData: {privateData?.message}</p>
28
+ <UserMenu />
29
+ </div>
30
+ </Authenticated>
31
+ <Unauthenticated>
32
+ {showSignIn ? (
33
+ <SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
34
+ ) : (
35
+ <SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
36
+ )}
37
+ </Unauthenticated>
38
+ <AuthLoading>
39
+ <div>Loading...</div>
40
+ </AuthLoading>
41
+ </>
42
+ );
43
+ }
@@ -0,0 +1,133 @@
1
+ import { authClient } from "@/lib/auth-client";
2
+ import { useForm } from "@tanstack/react-form";
3
+ import { useNavigate } from "@tanstack/react-router";
4
+ import { toast } from "sonner";
5
+ import z from "zod";
6
+ import { Button } from "./ui/button";
7
+ import { Input } from "./ui/input";
8
+ import { Label } from "./ui/label";
9
+
10
+ export default function SignInForm({
11
+ onSwitchToSignUp,
12
+ }: {
13
+ onSwitchToSignUp: () => void;
14
+ }) {
15
+ const navigate = useNavigate({
16
+ from: "/",
17
+ });
18
+
19
+ const form = useForm({
20
+ defaultValues: {
21
+ email: "",
22
+ password: "",
23
+ },
24
+ onSubmit: async ({ value }) => {
25
+ await authClient.signIn.email(
26
+ {
27
+ email: value.email,
28
+ password: value.password,
29
+ },
30
+ {
31
+ onSuccess: () => {
32
+ navigate({
33
+ to: "/dashboard",
34
+ });
35
+ toast.success("Sign in successful");
36
+ },
37
+ onError: (error) => {
38
+ toast.error(error.error.message || error.error.statusText);
39
+ },
40
+ },
41
+ );
42
+ },
43
+ validators: {
44
+ onSubmit: z.object({
45
+ email: z.email("Invalid email address"),
46
+ password: z.string().min(8, "Password must be at least 8 characters"),
47
+ }),
48
+ },
49
+ });
50
+
51
+ return (
52
+ <div className="mx-auto w-full mt-10 max-w-md p-6">
53
+ <h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
54
+
55
+ <form
56
+ onSubmit={(e) => {
57
+ e.preventDefault();
58
+ e.stopPropagation();
59
+ form.handleSubmit();
60
+ }}
61
+ className="space-y-4"
62
+ >
63
+ <div>
64
+ <form.Field name="email">
65
+ {(field) => (
66
+ <div className="space-y-2">
67
+ <Label htmlFor={field.name}>Email</Label>
68
+ <Input
69
+ id={field.name}
70
+ name={field.name}
71
+ type="email"
72
+ value={field.state.value}
73
+ onBlur={field.handleBlur}
74
+ onChange={(e) => field.handleChange(e.target.value)}
75
+ />
76
+ {field.state.meta.errors.map((error) => (
77
+ <p key={error?.message} className="text-red-500">
78
+ {error?.message}
79
+ </p>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </form.Field>
84
+ </div>
85
+
86
+ <div>
87
+ <form.Field name="password">
88
+ {(field) => (
89
+ <div className="space-y-2">
90
+ <Label htmlFor={field.name}>Password</Label>
91
+ <Input
92
+ id={field.name}
93
+ name={field.name}
94
+ type="password"
95
+ value={field.state.value}
96
+ onBlur={field.handleBlur}
97
+ onChange={(e) => field.handleChange(e.target.value)}
98
+ />
99
+ {field.state.meta.errors.map((error) => (
100
+ <p key={error?.message} className="text-red-500">
101
+ {error?.message}
102
+ </p>
103
+ ))}
104
+ </div>
105
+ )}
106
+ </form.Field>
107
+ </div>
108
+
109
+ <form.Subscribe>
110
+ {(state) => (
111
+ <Button
112
+ type="submit"
113
+ className="w-full"
114
+ disabled={!state.canSubmit || state.isSubmitting}
115
+ >
116
+ {state.isSubmitting ? "Submitting..." : "Sign In"}
117
+ </Button>
118
+ )}
119
+ </form.Subscribe>
120
+ </form>
121
+
122
+ <div className="mt-4 text-center">
123
+ <Button
124
+ variant="link"
125
+ onClick={onSwitchToSignUp}
126
+ className="text-indigo-600 hover:text-indigo-800"
127
+ >
128
+ Need an account? Sign Up
129
+ </Button>
130
+ </div>
131
+ </div>
132
+ );
133
+ }