better-ts-stack 0.1.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/README.md +83 -0
- package/dist/builder/configGenerator.d.ts +6 -0
- package/dist/builder/configGenerator.d.ts.map +1 -0
- package/dist/builder/configGenerator.js +110 -0
- package/dist/builder/configGenerator.js.map +1 -0
- package/dist/builder/dependencyInstaller.d.ts +3 -0
- package/dist/builder/dependencyInstaller.d.ts.map +1 -0
- package/dist/builder/dependencyInstaller.js +39 -0
- package/dist/builder/dependencyInstaller.js.map +1 -0
- package/dist/builder/fileProcessor.d.ts +6 -0
- package/dist/builder/fileProcessor.d.ts.map +1 -0
- package/dist/builder/fileProcessor.js +108 -0
- package/dist/builder/fileProcessor.js.map +1 -0
- package/dist/builder/gitInitializer.d.ts +2 -0
- package/dist/builder/gitInitializer.d.ts.map +1 -0
- package/dist/builder/gitInitializer.js +52 -0
- package/dist/builder/gitInitializer.js.map +1 -0
- package/dist/builder/index.d.ts +3 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +126 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/moduleSelector.d.ts +9 -0
- package/dist/builder/moduleSelector.d.ts.map +1 -0
- package/dist/builder/moduleSelector.js +29 -0
- package/dist/builder/moduleSelector.js.map +1 -0
- package/dist/builder/templateContext.d.ts +21 -0
- package/dist/builder/templateContext.d.ts.map +1 -0
- package/dist/builder/templateContext.js +47 -0
- package/dist/builder/templateContext.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/intro.d.ts +2 -0
- package/dist/intro.d.ts.map +1 -0
- package/dist/intro.js +27 -0
- package/dist/intro.js.map +1 -0
- package/dist/modules/registry.d.ts +6 -0
- package/dist/modules/registry.d.ts.map +1 -0
- package/dist/modules/registry.js +64 -0
- package/dist/modules/registry.js.map +1 -0
- package/dist/output/nextSteps.d.ts +4 -0
- package/dist/output/nextSteps.d.ts.map +1 -0
- package/dist/output/nextSteps.js +87 -0
- package/dist/output/nextSteps.js.map +1 -0
- package/dist/prompts/backend/index.d.ts +3 -0
- package/dist/prompts/backend/index.d.ts.map +1 -0
- package/dist/prompts/backend/index.js +128 -0
- package/dist/prompts/backend/index.js.map +1 -0
- package/dist/prompts/frontend/index.d.ts +3 -0
- package/dist/prompts/frontend/index.d.ts.map +1 -0
- package/dist/prompts/frontend/index.js +111 -0
- package/dist/prompts/frontend/index.js.map +1 -0
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +82 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +81 -0
- package/dist/types/index.js.map +1 -0
- package/dist/validators/index.d.ts +5 -0
- package/dist/validators/index.d.ts.map +1 -0
- package/dist/validators/index.js +73 -0
- package/dist/validators/index.js.map +1 -0
- package/package.json +66 -0
- package/templates/backend/express/.eslintrc.js +24 -0
- package/templates/backend/express/.prettierignore +52 -0
- package/templates/backend/express/.prettierrc +16 -0
- package/templates/backend/express/config.json +42 -0
- package/templates/backend/express/eslint.config.mjs +31 -0
- package/templates/backend/express/gitignore +39 -0
- package/templates/backend/express/src/index.ts +46 -0
- package/templates/backend/express/src/routes/health.ts +12 -0
- package/templates/backend/express/tsconfig.eslint.json +9 -0
- package/templates/backend/express/tsconfig.json +23 -0
- package/templates/frontend/nextjs/app/globals.css +99 -0
- package/templates/frontend/nextjs/app/layout.tsx +34 -0
- package/templates/frontend/nextjs/app/page.tsx.hbs +98 -0
- package/templates/frontend/nextjs/components/ui/button.tsx +51 -0
- package/templates/frontend/nextjs/components/ui/card.tsx +60 -0
- package/templates/frontend/nextjs/components/ui/field.tsx +67 -0
- package/templates/frontend/nextjs/components/ui/input.tsx +18 -0
- package/templates/frontend/nextjs/components.json +19 -0
- package/templates/frontend/nextjs/config.json +33 -0
- package/templates/frontend/nextjs/eslint.config.mjs +11 -0
- package/templates/frontend/nextjs/gitignore +41 -0
- package/templates/frontend/nextjs/lib/utils.ts +6 -0
- package/templates/frontend/nextjs/next.config.ts +8 -0
- package/templates/frontend/nextjs/postcss.config.mjs +7 -0
- package/templates/frontend/nextjs/proxy.ts.hbs +23 -0
- package/templates/frontend/nextjs/public/file.svg +1 -0
- package/templates/frontend/nextjs/public/globe.svg +1 -0
- package/templates/frontend/nextjs/public/next.svg +1 -0
- package/templates/frontend/nextjs/public/vercel.svg +1 -0
- package/templates/frontend/nextjs/public/window.svg +1 -0
- package/templates/frontend/nextjs/tsconfig.json +21 -0
- package/templates/modules/auth/express/config.json +1 -0
- package/templates/modules/auth/express/src/controllers/authController.ts.hbs +81 -0
- package/templates/modules/auth/express/src/lib/jwt.ts.hbs +27 -0
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -0
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +19 -0
- package/templates/modules/auth/express/src/services/userStore.ts.hbs +107 -0
- package/templates/modules/auth/nextjs/app/api/auth/[...all]/route.ts +4 -0
- package/templates/modules/auth/nextjs/app/dashboard/page.tsx +96 -0
- package/templates/modules/auth/nextjs/app/sign-in/page.tsx +35 -0
- package/templates/modules/auth/nextjs/app/sign-up/page.tsx +35 -0
- package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +132 -0
- package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +50 -0
- package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +152 -0
- package/templates/modules/auth/nextjs/config.json +31 -0
- package/templates/modules/auth/nextjs/lib/auth-client.ts +3 -0
- package/templates/modules/auth/nextjs/lib/auth-schema.ts +39 -0
- package/templates/modules/auth/nextjs/lib/auth.ts.hbs +35 -0
- package/templates/modules/docker/.dockerignore +13 -0
- package/templates/modules/docker/Dockerfile.hbs +71 -0
- package/templates/modules/docker/config.json +16 -0
- package/templates/modules/docker/docker-compose.yml.hbs +10 -0
- package/templates/modules/drizzle/nextjs/config.json +21 -0
- package/templates/modules/drizzle/nextjs/drizzle.config.ts +13 -0
- package/templates/modules/drizzle/nextjs/lib/db.ts +11 -0
- package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +84 -0
- package/templates/modules/mongoose/config.json +16 -0
- package/templates/modules/mongoose/src/lib/db.ts +43 -0
- package/templates/modules/mongoose/src/lib/db.ts.hbs +56 -0
- package/templates/modules/mongoose/src/models/User.ts +47 -0
- package/templates/modules/prisma/express/config.json +21 -0
- package/templates/modules/prisma/express/prisma/schema.prisma +23 -0
- package/templates/modules/prisma/express/prisma.config.ts +13 -0
- package/templates/modules/prisma/express/src/lib/prisma.ts +37 -0
- package/templates/modules/prisma/nextjs/config.json +23 -0
- package/templates/modules/prisma/nextjs/lib/prisma.ts +18 -0
- package/templates/modules/prisma/nextjs/prisma/schema.prisma.hbs +90 -0
- package/templates/modules/prisma/nextjs/prisma.config.ts +12 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useState, useTransition } from "react";
|
|
6
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
+
import { LoaderCircle } from "lucide-react";
|
|
8
|
+
import { useForm } from "react-hook-form";
|
|
9
|
+
|
|
10
|
+
import { authClient } from "@/lib/auth-client";
|
|
11
|
+
import {
|
|
12
|
+
getAuthErrorMessage,
|
|
13
|
+
signInSchema,
|
|
14
|
+
type SignInValues,
|
|
15
|
+
} from "@/lib/auth-schema";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import {
|
|
18
|
+
Card,
|
|
19
|
+
CardContent,
|
|
20
|
+
CardDescription,
|
|
21
|
+
CardFooter,
|
|
22
|
+
CardHeader,
|
|
23
|
+
CardTitle,
|
|
24
|
+
} from "@/components/ui/card";
|
|
25
|
+
import {
|
|
26
|
+
Field,
|
|
27
|
+
FieldDescription,
|
|
28
|
+
FieldError,
|
|
29
|
+
FieldGroup,
|
|
30
|
+
FieldLabel,
|
|
31
|
+
} from "@/components/ui/field";
|
|
32
|
+
import { Input } from "@/components/ui/input";
|
|
33
|
+
|
|
34
|
+
export function SignInForm() {
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const [isPending, startTransition] = useTransition();
|
|
37
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
38
|
+
const form = useForm<SignInValues>({
|
|
39
|
+
resolver: zodResolver(signInSchema),
|
|
40
|
+
defaultValues: {
|
|
41
|
+
email: "",
|
|
42
|
+
password: "",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
47
|
+
setAuthError(null);
|
|
48
|
+
|
|
49
|
+
startTransition(async () => {
|
|
50
|
+
const result = await authClient.signIn.email({
|
|
51
|
+
email: values.email,
|
|
52
|
+
password: values.password,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (result.error) {
|
|
56
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
router.push("/dashboard");
|
|
61
|
+
router.refresh();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
|
|
67
|
+
<CardHeader className="space-y-2">
|
|
68
|
+
<CardTitle>Login to your account</CardTitle>
|
|
69
|
+
<CardDescription>
|
|
70
|
+
Sign in with your email and password to continue to your dashboard.
|
|
71
|
+
</CardDescription>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent>
|
|
74
|
+
<form className="space-y-6" onSubmit={onSubmit} noValidate>
|
|
75
|
+
<FieldGroup>
|
|
76
|
+
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
77
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
78
|
+
<Input
|
|
79
|
+
id="email"
|
|
80
|
+
type="email"
|
|
81
|
+
autoComplete="email"
|
|
82
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
83
|
+
placeholder="you@example.com"
|
|
84
|
+
{...form.register("email")}
|
|
85
|
+
/>
|
|
86
|
+
<FieldDescription>Use the email you registered with.</FieldDescription>
|
|
87
|
+
<FieldError errors={[form.formState.errors.email]} />
|
|
88
|
+
</Field>
|
|
89
|
+
|
|
90
|
+
<Field data-invalid={Boolean(form.formState.errors.password)}>
|
|
91
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
92
|
+
<Input
|
|
93
|
+
id="password"
|
|
94
|
+
type="password"
|
|
95
|
+
autoComplete="current-password"
|
|
96
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
97
|
+
placeholder="Enter your password"
|
|
98
|
+
{...form.register("password")}
|
|
99
|
+
/>
|
|
100
|
+
<FieldError errors={[form.formState.errors.password]} />
|
|
101
|
+
</Field>
|
|
102
|
+
</FieldGroup>
|
|
103
|
+
|
|
104
|
+
{authError ? (
|
|
105
|
+
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
106
|
+
{authError}
|
|
107
|
+
</div>
|
|
108
|
+
) : null}
|
|
109
|
+
|
|
110
|
+
<Button className="w-full" type="submit" disabled={isPending}>
|
|
111
|
+
{isPending ? (
|
|
112
|
+
<>
|
|
113
|
+
<LoaderCircle className="mr-2 size-4 animate-spin" />
|
|
114
|
+
Signing in
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
"Login"
|
|
118
|
+
)}
|
|
119
|
+
</Button>
|
|
120
|
+
</form>
|
|
121
|
+
</CardContent>
|
|
122
|
+
<CardFooter className="justify-center">
|
|
123
|
+
<p className="text-sm text-slate-600">
|
|
124
|
+
Need an account?{" "}
|
|
125
|
+
<Link className="font-medium text-sky-700 hover:text-sky-800" href="/sign-up">
|
|
126
|
+
Signup
|
|
127
|
+
</Link>
|
|
128
|
+
</p>
|
|
129
|
+
</CardFooter>
|
|
130
|
+
</Card>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useState, useTransition } from "react";
|
|
5
|
+
import { LoaderCircle, LogOut } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { authClient } from "@/lib/auth-client";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
|
|
10
|
+
export function SignOutButton() {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const [isPending, startTransition] = useTransition();
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="space-y-2">
|
|
17
|
+
<Button
|
|
18
|
+
variant="outline"
|
|
19
|
+
onClick={() => {
|
|
20
|
+
setError(null);
|
|
21
|
+
startTransition(async () => {
|
|
22
|
+
const result = await authClient.signOut();
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
setError("Unable to sign out right now. Please try again.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
router.push("/sign-in");
|
|
30
|
+
router.refresh();
|
|
31
|
+
});
|
|
32
|
+
}}
|
|
33
|
+
disabled={isPending}
|
|
34
|
+
>
|
|
35
|
+
{isPending ? (
|
|
36
|
+
<>
|
|
37
|
+
<LoaderCircle className="mr-2 size-4 animate-spin" />
|
|
38
|
+
Signing out
|
|
39
|
+
</>
|
|
40
|
+
) : (
|
|
41
|
+
<>
|
|
42
|
+
<LogOut className="mr-2 size-4" />
|
|
43
|
+
Sign out
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</Button>
|
|
47
|
+
{error ? <p className="text-sm text-rose-600">{error}</p> : null}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useState, useTransition } from "react";
|
|
6
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
+
import { LoaderCircle } from "lucide-react";
|
|
8
|
+
import { useForm } from "react-hook-form";
|
|
9
|
+
|
|
10
|
+
import { authClient } from "@/lib/auth-client";
|
|
11
|
+
import {
|
|
12
|
+
getAuthErrorMessage,
|
|
13
|
+
signUpSchema,
|
|
14
|
+
type SignUpValues,
|
|
15
|
+
} from "@/lib/auth-schema";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import {
|
|
18
|
+
Card,
|
|
19
|
+
CardContent,
|
|
20
|
+
CardDescription,
|
|
21
|
+
CardFooter,
|
|
22
|
+
CardHeader,
|
|
23
|
+
CardTitle,
|
|
24
|
+
} from "@/components/ui/card";
|
|
25
|
+
import {
|
|
26
|
+
Field,
|
|
27
|
+
FieldDescription,
|
|
28
|
+
FieldError,
|
|
29
|
+
FieldGroup,
|
|
30
|
+
FieldLabel,
|
|
31
|
+
} from "@/components/ui/field";
|
|
32
|
+
import { Input } from "@/components/ui/input";
|
|
33
|
+
|
|
34
|
+
export function SignUpForm() {
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const [isPending, startTransition] = useTransition();
|
|
37
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
38
|
+
const form = useForm<SignUpValues>({
|
|
39
|
+
resolver: zodResolver(signUpSchema),
|
|
40
|
+
defaultValues: {
|
|
41
|
+
name: "",
|
|
42
|
+
email: "",
|
|
43
|
+
password: "",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
48
|
+
setAuthError(null);
|
|
49
|
+
|
|
50
|
+
startTransition(async () => {
|
|
51
|
+
const result = await authClient.signUp.email({
|
|
52
|
+
name: values.name,
|
|
53
|
+
email: values.email,
|
|
54
|
+
password: values.password,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (result.error) {
|
|
58
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
router.push("/dashboard");
|
|
63
|
+
router.refresh();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
|
|
69
|
+
<CardHeader className="space-y-2">
|
|
70
|
+
<CardTitle>Create your account</CardTitle>
|
|
71
|
+
<CardDescription>
|
|
72
|
+
Start with a simple email and password sign-up flow powered by Better
|
|
73
|
+
Auth.
|
|
74
|
+
</CardDescription>
|
|
75
|
+
</CardHeader>
|
|
76
|
+
<CardContent>
|
|
77
|
+
<form className="space-y-6" onSubmit={onSubmit} noValidate>
|
|
78
|
+
<FieldGroup>
|
|
79
|
+
<Field data-invalid={Boolean(form.formState.errors.name)}>
|
|
80
|
+
<FieldLabel htmlFor="name">Full name</FieldLabel>
|
|
81
|
+
<Input
|
|
82
|
+
id="name"
|
|
83
|
+
autoComplete="name"
|
|
84
|
+
aria-invalid={Boolean(form.formState.errors.name)}
|
|
85
|
+
placeholder="Ada Lovelace"
|
|
86
|
+
{...form.register("name")}
|
|
87
|
+
/>
|
|
88
|
+
<FieldError errors={[form.formState.errors.name]} />
|
|
89
|
+
</Field>
|
|
90
|
+
|
|
91
|
+
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
92
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
93
|
+
<Input
|
|
94
|
+
id="email"
|
|
95
|
+
type="email"
|
|
96
|
+
autoComplete="email"
|
|
97
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
98
|
+
placeholder="you@example.com"
|
|
99
|
+
{...form.register("email")}
|
|
100
|
+
/>
|
|
101
|
+
<FieldDescription>
|
|
102
|
+
This email will be used for future sign-ins.
|
|
103
|
+
</FieldDescription>
|
|
104
|
+
<FieldError errors={[form.formState.errors.email]} />
|
|
105
|
+
</Field>
|
|
106
|
+
|
|
107
|
+
<Field data-invalid={Boolean(form.formState.errors.password)}>
|
|
108
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
109
|
+
<Input
|
|
110
|
+
id="password"
|
|
111
|
+
type="password"
|
|
112
|
+
autoComplete="new-password"
|
|
113
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
114
|
+
placeholder="Create a strong password"
|
|
115
|
+
{...form.register("password")}
|
|
116
|
+
/>
|
|
117
|
+
<FieldDescription>
|
|
118
|
+
Use at least 8 characters for the generated starter flow.
|
|
119
|
+
</FieldDescription>
|
|
120
|
+
<FieldError errors={[form.formState.errors.password]} />
|
|
121
|
+
</Field>
|
|
122
|
+
</FieldGroup>
|
|
123
|
+
|
|
124
|
+
{authError ? (
|
|
125
|
+
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
126
|
+
{authError}
|
|
127
|
+
</div>
|
|
128
|
+
) : null}
|
|
129
|
+
|
|
130
|
+
<Button className="w-full" type="submit" disabled={isPending}>
|
|
131
|
+
{isPending ? (
|
|
132
|
+
<>
|
|
133
|
+
<LoaderCircle className="mr-2 size-4 animate-spin" />
|
|
134
|
+
Creating account
|
|
135
|
+
</>
|
|
136
|
+
) : (
|
|
137
|
+
"Signup"
|
|
138
|
+
)}
|
|
139
|
+
</Button>
|
|
140
|
+
</form>
|
|
141
|
+
</CardContent>
|
|
142
|
+
<CardFooter className="justify-center">
|
|
143
|
+
<p className="text-sm text-slate-600">
|
|
144
|
+
Already have an account?{" "}
|
|
145
|
+
<Link className="font-medium text-sky-700 hover:text-sky-800" href="/sign-in">
|
|
146
|
+
Login
|
|
147
|
+
</Link>
|
|
148
|
+
</p>
|
|
149
|
+
</CardFooter>
|
|
150
|
+
</Card>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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.2.1",
|
|
8
|
+
"better-auth": "^1.4.19",
|
|
9
|
+
"mongodb": "^6.0.0",
|
|
10
|
+
"react-hook-form": "^7.62.0",
|
|
11
|
+
"zod": "^4.1.5"
|
|
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
|
+
"templateFiles": [
|
|
20
|
+
"lib/auth.ts.hbs",
|
|
21
|
+
"lib/auth-client.ts",
|
|
22
|
+
"app/api/auth/[...all]/route.ts",
|
|
23
|
+
"app/dashboard/page.tsx",
|
|
24
|
+
"app/sign-in/page.tsx",
|
|
25
|
+
"app/sign-up/page.tsx",
|
|
26
|
+
"components/auth/sign-in-form.tsx",
|
|
27
|
+
"components/auth/sign-up-form.tsx",
|
|
28
|
+
"components/auth/sign-out-button.tsx",
|
|
29
|
+
"lib/auth-schema.ts"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const signInSchema = z.object({
|
|
4
|
+
email: z.string().email("Enter a valid email address."),
|
|
5
|
+
password: z.string().min(8, "Password must be at least 8 characters."),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const signUpSchema = signInSchema.extend({
|
|
9
|
+
name: z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.min(2, "Name must be at least 2 characters.")
|
|
13
|
+
.max(50, "Name must be 50 characters or fewer."),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type SignInValues = z.infer<typeof signInSchema>;
|
|
17
|
+
export type SignUpValues = z.infer<typeof signUpSchema>;
|
|
18
|
+
|
|
19
|
+
export function getAuthErrorMessage(error: unknown) {
|
|
20
|
+
if (!error) {
|
|
21
|
+
return "Something went wrong. Please try again.";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof error === "string") {
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof error === "object") {
|
|
29
|
+
if ("message" in error && typeof error.message === "string") {
|
|
30
|
+
return error.message;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ("statusText" in error && typeof error.statusText === "string") {
|
|
34
|
+
return error.statusText;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return "Something went wrong. Please try again.";
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { nextCookies } from "better-auth/next-js";
|
|
3
|
+
{{#if (eq database 'prisma')}}
|
|
4
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
5
|
+
import client from "./prisma";
|
|
6
|
+
{{/if}}
|
|
7
|
+
{{#if (eq database 'drizzle')}}
|
|
8
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
9
|
+
import { db } from "./db";
|
|
10
|
+
{{/if}}
|
|
11
|
+
{{#if (eq database 'mongoose')}}
|
|
12
|
+
import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|
13
|
+
import { MongoClient } from "mongodb";
|
|
14
|
+
|
|
15
|
+
const client = new MongoClient(process.env.MONGODB_URI!);
|
|
16
|
+
const db = client.db();
|
|
17
|
+
{{/if}}
|
|
18
|
+
|
|
19
|
+
export const auth = betterAuth({
|
|
20
|
+
{{#if (eq database 'prisma')}}
|
|
21
|
+
database: prismaAdapter(client, { provider: "postgresql" }),
|
|
22
|
+
{{/if}}
|
|
23
|
+
{{#if (eq database 'drizzle')}}
|
|
24
|
+
database: drizzleAdapter(db, { provider: "pg" }),
|
|
25
|
+
{{/if}}
|
|
26
|
+
{{#if (eq database 'mongoose')}}
|
|
27
|
+
database: mongodbAdapter(db, { client }),
|
|
28
|
+
{{/if}}
|
|
29
|
+
emailAndPassword: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
},
|
|
32
|
+
plugins: [nextCookies()],
|
|
33
|
+
secret: process.env.BETTER_AUTH_SECRET,
|
|
34
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
35
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{{#if (eq framework 'nextjs')}}
|
|
2
|
+
# Base stage
|
|
3
|
+
FROM node:20-alpine AS base
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
RUN apk add --no-cache libc6-compat
|
|
6
|
+
|
|
7
|
+
# Dependencies stage
|
|
8
|
+
FROM base AS deps
|
|
9
|
+
COPY package*.json ./
|
|
10
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
11
|
+
|
|
12
|
+
# Build stage
|
|
13
|
+
FROM base AS builder
|
|
14
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
15
|
+
COPY . .
|
|
16
|
+
RUN npm run build
|
|
17
|
+
|
|
18
|
+
# Production stage
|
|
19
|
+
FROM node:20-alpine AS runner
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
ENV NODE_ENV=production
|
|
22
|
+
|
|
23
|
+
RUN addgroup --system --gid 1001 nodejs && \
|
|
24
|
+
adduser --system --uid 1001 nextjs
|
|
25
|
+
|
|
26
|
+
COPY --from=builder /app/public ./public
|
|
27
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
28
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
29
|
+
|
|
30
|
+
USER nextjs
|
|
31
|
+
EXPOSE {{port}}
|
|
32
|
+
ENV PORT={{port}} HOSTNAME=0.0.0.0
|
|
33
|
+
|
|
34
|
+
CMD ["node", "server.js"]
|
|
35
|
+
|
|
36
|
+
{{else}}
|
|
37
|
+
# Build stage
|
|
38
|
+
FROM node:20-alpine AS builder
|
|
39
|
+
WORKDIR /app
|
|
40
|
+
|
|
41
|
+
COPY package*.json ./
|
|
42
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
43
|
+
|
|
44
|
+
COPY . .
|
|
45
|
+
|
|
46
|
+
# Only build if package.json has a build script
|
|
47
|
+
RUN npm run build || echo "No build script found, skipping build step"
|
|
48
|
+
|
|
49
|
+
# Production stage
|
|
50
|
+
FROM node:20-alpine
|
|
51
|
+
WORKDIR /app
|
|
52
|
+
ENV NODE_ENV=production
|
|
53
|
+
|
|
54
|
+
# Create non-root user for security
|
|
55
|
+
RUN addgroup --system --gid 1001 nodejs && \
|
|
56
|
+
adduser --system --uid 1001 expressuser
|
|
57
|
+
|
|
58
|
+
COPY package*.json ./
|
|
59
|
+
RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
|
|
60
|
+
|
|
61
|
+
# Copy built files (if TypeScript) or source files (if JavaScript)
|
|
62
|
+
COPY --from=builder --chown=expressuser:nodejs /app/dist ./dist 2>/dev/null || \
|
|
63
|
+
COPY --chown=expressuser:nodejs /app/src ./src 2>/dev/null || \
|
|
64
|
+
COPY --chown=expressuser:nodejs /app/*.js ./
|
|
65
|
+
|
|
66
|
+
USER expressuser
|
|
67
|
+
EXPOSE {{port}}
|
|
68
|
+
ENV PORT={{port}}
|
|
69
|
+
|
|
70
|
+
CMD ["node", "{{entrypoint}}"]
|
|
71
|
+
{{/if}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "docker",
|
|
3
|
+
"name": "Docker",
|
|
4
|
+
"description": "Docker containerization with multi-stage build and docker-compose",
|
|
5
|
+
"type": "feature",
|
|
6
|
+
"dependencies": {},
|
|
7
|
+
"devDependencies": {},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"docker:build": "docker build -t {{projectName}} .",
|
|
10
|
+
"docker:up": "docker-compose up -d",
|
|
11
|
+
"docker:down": "docker-compose down",
|
|
12
|
+
"docker:logs": "docker-compose logs -f"
|
|
13
|
+
},
|
|
14
|
+
"envVars": {},
|
|
15
|
+
"templateFiles": ["docker-compose.yml.hbs", "Dockerfile.hbs"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "drizzle",
|
|
3
|
+
"name": "Drizzle ORM",
|
|
4
|
+
"description": "Drizzle ORM for Next.js with PostgreSQL",
|
|
5
|
+
"type": "database",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"drizzle-orm": "^0.41.0",
|
|
8
|
+
"pg": "^8.13.0",
|
|
9
|
+
"dotenv": "^17.3.1"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": { "drizzle-kit": "^0.31.4", "@types/pg": "^8.11.0" },
|
|
12
|
+
"scripts": {
|
|
13
|
+
"db:generate": "drizzle-kit generate",
|
|
14
|
+
"db:migrate": "drizzle-kit migrate",
|
|
15
|
+
"db:studio": "drizzle-kit studio"
|
|
16
|
+
},
|
|
17
|
+
"envVars": {
|
|
18
|
+
"DATABASE_URL": "postgresql://user:password@localhost:5432/mydb?schema=public"
|
|
19
|
+
},
|
|
20
|
+
"templateFiles": ["lib/schema.ts.hbs"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { config } from "dotenv";
|
|
2
|
+
import { defineConfig } from "drizzle-kit";
|
|
3
|
+
|
|
4
|
+
config({ path: ".env" });
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
schema: "./lib/schema.ts",
|
|
8
|
+
out: "./migrations",
|
|
9
|
+
dialect: "postgresql",
|
|
10
|
+
dbCredentials: {
|
|
11
|
+
url: process.env.DATABASE_URL!,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { config } from "dotenv";
|
|
2
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
|
|
5
|
+
config({ path: ".env" }); // or .env.local
|
|
6
|
+
|
|
7
|
+
const pool = new Pool({
|
|
8
|
+
connectionString: process.env.DATABASE_URL,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const db = drizzle(pool);
|