better-ts-stack 2.0.1 → 2.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 +114 -113
- package/dist/args.js +24 -20
- package/dist/builder/buildPlanner.js +1 -1
- package/dist/builder/configGenerator.js +1 -5
- package/dist/builder/index.js +15 -3
- package/dist/builder/moduleSelector.js +4 -2
- package/dist/builder/readmeGenerator.js +101 -0
- package/dist/builder/templateContext.js +30 -0
- package/dist/index.js +7 -1
- package/dist/output/nextSteps.js +22 -23
- package/dist/prompts/index.js +2 -1
- package/dist/prompts/stack.js +21 -2
- package/dist/types/index.js +2 -1
- package/dist/ui/messages.js +2 -1
- package/dist/validators/index.js +5 -2
- package/package.json +1 -1
- package/templates/backend/express/config.json +42 -40
- package/templates/backend/express/src/env.ts.hbs +39 -0
- package/templates/backend/express/src/index.ts.hbs +111 -75
- package/templates/backend/nestjs/config.json +43 -41
- package/templates/backend/nestjs/src/app.module.ts.hbs +13 -0
- package/templates/backend/nestjs/src/env.ts.hbs +39 -0
- package/templates/backend/nestjs/src/main.ts +3 -2
- package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
- package/templates/frontend/tanstack/config.json +45 -45
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
- package/templates/frontend/tanstack/vite.config.ts +22 -14
- package/templates/modules/auth/express/config.json +20 -20
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +27 -19
- package/templates/modules/auth/nestjs/config.json +20 -20
- package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs} +15 -0
- package/templates/modules/auth/nextjs/lib/auth-schema.ts +1 -1
- package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +132 -134
- package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +46 -48
- package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +150 -152
- package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +38 -38
- package/templates/modules/docker/.dockerignore +1 -0
- package/templates/modules/docker/Dockerfile.hbs +147 -136
- package/templates/modules/docker/docker-compose.yml.hbs +96 -1
- package/templates/modules/drizzle/express/src/lib/schema.ts +3 -1
- package/templates/modules/drizzle/nestjs/src/database/schema.ts +3 -1
- package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +11 -5
- package/templates/modules/drizzle/tanstack/src/lib/schema.ts.hbs +90 -84
- package/templates/modules/rate-limit/express/config.json +12 -0
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +21 -0
- package/templates/modules/rate-limit/nestjs/config.json +12 -0
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "auth",
|
|
3
|
-
"name": "JWT Auth",
|
|
4
|
-
"description": "JWT authentication for NestJS with bcrypt password hashing",
|
|
5
|
-
"type": "feature",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"bcrypt": "^6.0.0",
|
|
8
|
-
"jsonwebtoken": "^9.0.3",
|
|
9
|
-
"zod": "^4.6.5"
|
|
10
|
-
},
|
|
11
|
-
"devDependencies": {
|
|
12
|
-
"@types/bcrypt": "^6.0.0",
|
|
13
|
-
"@types/jsonwebtoken": "^9.0.10"
|
|
14
|
-
},
|
|
15
|
-
"scripts": {},
|
|
16
|
-
"envVars": {
|
|
17
|
-
"JWT_SECRET": "please-change-me",
|
|
18
|
-
"JWT_EXPIRES_IN": "1h"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "auth",
|
|
3
|
+
"name": "JWT Auth",
|
|
4
|
+
"description": "JWT authentication for NestJS with bcrypt password hashing",
|
|
5
|
+
"type": "feature",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"bcrypt": "^6.0.0",
|
|
8
|
+
"jsonwebtoken": "^9.0.3",
|
|
9
|
+
"zod": "^4.6.5"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bcrypt": "^6.0.0",
|
|
13
|
+
"@types/jsonwebtoken": "^9.0.10"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {},
|
|
16
|
+
"envVars": {
|
|
17
|
+
"JWT_SECRET": "please-change-me",
|
|
18
|
+
"JWT_EXPIRES_IN": "1h"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs}
RENAMED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
Req,
|
|
8
8
|
UseGuards,
|
|
9
9
|
} from "@nestjs/common";
|
|
10
|
+
{{#if useRateLimit}}
|
|
11
|
+
import { Throttle } from "@nestjs/throttler";
|
|
12
|
+
{{/if}}
|
|
10
13
|
|
|
11
14
|
import { type AuthenticatedRequest, AuthGuard } from "./auth.guard.js";
|
|
12
15
|
import {
|
|
@@ -19,11 +22,20 @@ import { type AuthResponse, AuthService } from "./auth.service.js";
|
|
|
19
22
|
import type { AuthTokenPayload } from "./jwt.js";
|
|
20
23
|
import { ZodValidationPipe } from "./zod-validation.pipe.js";
|
|
21
24
|
|
|
25
|
+
{{#if useRateLimit}}
|
|
26
|
+
// Sign-up and login: 10 attempts per client IP in 15 minutes, against
|
|
27
|
+
// password guessing
|
|
28
|
+
const AUTH_LIMIT = { default: { ttl: 15 * 60 * 1000, limit: 10 } };
|
|
29
|
+
|
|
30
|
+
{{/if}}
|
|
22
31
|
@Controller("auth")
|
|
23
32
|
export class AuthController {
|
|
24
33
|
constructor(private readonly authService: AuthService) {}
|
|
25
34
|
|
|
26
35
|
@Post("register")
|
|
36
|
+
{{#if useRateLimit}}
|
|
37
|
+
@Throttle(AUTH_LIMIT)
|
|
38
|
+
{{/if}}
|
|
27
39
|
register(
|
|
28
40
|
@Body(new ZodValidationPipe(registerSchema)) body: RegisterInput
|
|
29
41
|
): Promise<AuthResponse> {
|
|
@@ -32,6 +44,9 @@ export class AuthController {
|
|
|
32
44
|
|
|
33
45
|
@Post("login")
|
|
34
46
|
@HttpCode(200)
|
|
47
|
+
{{#if useRateLimit}}
|
|
48
|
+
@Throttle(AUTH_LIMIT)
|
|
49
|
+
{{/if}}
|
|
35
50
|
login(
|
|
36
51
|
@Body(new ZodValidationPipe(loginSchema)) body: LoginInput
|
|
37
52
|
): Promise<AuthResponse> {
|
|
@@ -1,135 +1,133 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</Card>
|
|
134
|
-
);
|
|
1
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
2
|
+
import { useState, useTransition } from "react";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { LoaderCircle } from "lucide-react";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { authClient } from "~/lib/auth-client";
|
|
8
|
+
import {
|
|
9
|
+
getAuthErrorMessage,
|
|
10
|
+
signInSchema,
|
|
11
|
+
type SignInValues,
|
|
12
|
+
} from "~/lib/auth-schema";
|
|
13
|
+
import { Button } from "~/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
Card,
|
|
16
|
+
CardContent,
|
|
17
|
+
CardDescription,
|
|
18
|
+
CardFooter,
|
|
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
|
+
|
|
31
|
+
export function SignInForm() {
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const [isPending, startTransition] = useTransition();
|
|
34
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
35
|
+
const form = useForm<SignInValues>({
|
|
36
|
+
resolver: zodResolver(signInSchema),
|
|
37
|
+
defaultValues: {
|
|
38
|
+
email: "",
|
|
39
|
+
password: "",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
44
|
+
setAuthError(null);
|
|
45
|
+
|
|
46
|
+
startTransition(async () => {
|
|
47
|
+
const result = await authClient.signIn.email({
|
|
48
|
+
email: values.email,
|
|
49
|
+
password: values.password,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (result.error) {
|
|
53
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
navigate({ to: "/dashboard" });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
|
|
63
|
+
<CardHeader className="space-y-2">
|
|
64
|
+
<CardTitle>Login to your account</CardTitle>
|
|
65
|
+
<CardDescription>
|
|
66
|
+
Sign in with your email and password to continue to your dashboard.
|
|
67
|
+
</CardDescription>
|
|
68
|
+
</CardHeader>
|
|
69
|
+
<CardContent>
|
|
70
|
+
<form className="space-y-6" onSubmit={onSubmit} noValidate>
|
|
71
|
+
<FieldGroup>
|
|
72
|
+
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
73
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
74
|
+
<Input
|
|
75
|
+
id="email"
|
|
76
|
+
type="email"
|
|
77
|
+
autoComplete="email"
|
|
78
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
79
|
+
placeholder="you@example.com"
|
|
80
|
+
{...form.register("email")}
|
|
81
|
+
/>
|
|
82
|
+
<FieldDescription>
|
|
83
|
+
Use the email you registered with.
|
|
84
|
+
</FieldDescription>
|
|
85
|
+
<FieldError errors={[form.formState.errors.email]} />
|
|
86
|
+
</Field>
|
|
87
|
+
|
|
88
|
+
<Field data-invalid={Boolean(form.formState.errors.password)}>
|
|
89
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
90
|
+
<Input
|
|
91
|
+
id="password"
|
|
92
|
+
type="password"
|
|
93
|
+
autoComplete="current-password"
|
|
94
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
95
|
+
placeholder="Enter your password"
|
|
96
|
+
{...form.register("password")}
|
|
97
|
+
/>
|
|
98
|
+
<FieldError errors={[form.formState.errors.password]} />
|
|
99
|
+
</Field>
|
|
100
|
+
</FieldGroup>
|
|
101
|
+
|
|
102
|
+
{authError ? (
|
|
103
|
+
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
104
|
+
{authError}
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
|
|
108
|
+
<Button className="w-full" type="submit" disabled={isPending}>
|
|
109
|
+
{isPending ? (
|
|
110
|
+
<>
|
|
111
|
+
<LoaderCircle className="mr-2 size-4 animate-spin" />
|
|
112
|
+
Signing in
|
|
113
|
+
</>
|
|
114
|
+
) : (
|
|
115
|
+
"Login"
|
|
116
|
+
)}
|
|
117
|
+
</Button>
|
|
118
|
+
</form>
|
|
119
|
+
</CardContent>
|
|
120
|
+
<CardFooter className="justify-center">
|
|
121
|
+
<p className="text-sm text-slate-600">
|
|
122
|
+
Need an account?{" "}
|
|
123
|
+
<Link
|
|
124
|
+
className="font-medium text-sky-700 hover:text-sky-800"
|
|
125
|
+
to="/sign-up"
|
|
126
|
+
>
|
|
127
|
+
Signup
|
|
128
|
+
</Link>
|
|
129
|
+
</p>
|
|
130
|
+
</CardFooter>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
135
133
|
}
|
|
@@ -1,49 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
1
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
2
|
+
import { useState, useTransition } from "react";
|
|
3
|
+
import { LoaderCircle, LogOut } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { authClient } from "~/lib/auth-client";
|
|
6
|
+
import { Button } from "~/components/ui/button";
|
|
7
|
+
|
|
8
|
+
export function SignOutButton() {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const [isPending, startTransition] = useTransition();
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-2">
|
|
15
|
+
<Button
|
|
16
|
+
variant="outline"
|
|
17
|
+
onClick={() => {
|
|
18
|
+
setError(null);
|
|
19
|
+
startTransition(async () => {
|
|
20
|
+
const result = await authClient.signOut();
|
|
21
|
+
|
|
22
|
+
if (result.error) {
|
|
23
|
+
setError("Unable to sign out right now. Please try again.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
navigate({ to: "/sign-in" });
|
|
28
|
+
});
|
|
29
|
+
}}
|
|
30
|
+
disabled={isPending}
|
|
31
|
+
>
|
|
32
|
+
{isPending ? (
|
|
33
|
+
<>
|
|
34
|
+
<LoaderCircle className="mr-2 size-4 animate-spin" />
|
|
35
|
+
Signing out
|
|
36
|
+
</>
|
|
37
|
+
) : (
|
|
38
|
+
<>
|
|
39
|
+
<LogOut className="mr-2 size-4" />
|
|
40
|
+
Sign out
|
|
41
|
+
</>
|
|
42
|
+
)}
|
|
43
|
+
</Button>
|
|
44
|
+
{error ? <p className="text-sm text-rose-600">{error}</p> : null}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
49
47
|
}
|