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,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
className={cn(
|
|
10
|
+
"flex h-11 w-full rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-950 shadow-sm transition-[color,box-shadow] outline-none placeholder:text-slate-400 focus-visible:border-sky-500 focus-visible:ring-[3px] focus-visible:ring-sky-500/20 disabled:cursor-not-allowed disabled:opacity-50",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Input };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"iconLibrary": "lucide",
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"lib": "@/lib",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"utils": "@/lib/utils"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nextjs",
|
|
3
|
+
"name": "Next.js",
|
|
4
|
+
"description": "Next.js 16 App Router with TypeScript and Tailwind CSS",
|
|
5
|
+
"type": "base",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "eslint"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"class-variance-authority": "^0.7.1",
|
|
14
|
+
"clsx": "^2.1.1",
|
|
15
|
+
"lucide-react": "^0.542.0",
|
|
16
|
+
"next": "^16.2.0",
|
|
17
|
+
"react": "19.2.3",
|
|
18
|
+
"react-dom": "19.2.3",
|
|
19
|
+
"tailwind-merge": "^3.3.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@tailwindcss/postcss": "^4",
|
|
23
|
+
"@types/node": "^20",
|
|
24
|
+
"@types/react": "^19",
|
|
25
|
+
"@types/react-dom": "^19",
|
|
26
|
+
"eslint": "^9",
|
|
27
|
+
"eslint-config-next": "16.1.6",
|
|
28
|
+
"tailwindcss": "^4",
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
},
|
|
31
|
+
"envVars": { "NODE_ENV": "development", "PORT": "3000" },
|
|
32
|
+
"templateFiles": ["app/page.tsx.hbs", "proxy.ts.hbs"]
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
2
|
+
import nextTs from "eslint-config-next/typescript";
|
|
3
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
/.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
|
|
27
|
+
# debug
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
yarn-error.log*
|
|
31
|
+
.pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# env files (can opt-in for committing if needed)
|
|
34
|
+
.env*
|
|
35
|
+
|
|
36
|
+
# vercel
|
|
37
|
+
.vercel
|
|
38
|
+
|
|
39
|
+
# typescript
|
|
40
|
+
*.tsbuildinfo
|
|
41
|
+
next-env.d.ts
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
|
|
3
|
+
{{#if useAuth}}
|
|
4
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
5
|
+
|
|
6
|
+
export async function proxy(request: NextRequest) {
|
|
7
|
+
const sessionCookie = getSessionCookie(request);
|
|
8
|
+
|
|
9
|
+
if (!sessionCookie) {
|
|
10
|
+
return NextResponse.redirect(new URL("/sign-in", request.url));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return NextResponse.next();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const config = {
|
|
17
|
+
matcher: ["/dashboard/:path*"],
|
|
18
|
+
};
|
|
19
|
+
{{else}}
|
|
20
|
+
export function proxy(_request: NextRequest) {
|
|
21
|
+
return NextResponse.next();
|
|
22
|
+
}
|
|
23
|
+
{{/if}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"auth","name":"JWT Auth","description":"JWT authentication with bcrypt password hashing","type":"feature","dependencies":{"bcrypt":"^5.1.1","jsonwebtoken":"^9.0.2"},"devDependencies":{"@types/bcrypt":"^5.0.0","@types/jsonwebtoken":"^9.0.5"},"scripts":{},"envVars":{"JWT_SECRET":"please-change-me","JWT_EXPIRES_IN":"1h"},"templateFiles":["src/lib/jwt.ts.hbs","src/middleware/requireAuth.ts.hbs","src/services/userStore.ts.hbs","src/routes/auth.ts.hbs","src/controllers/authController.ts.hbs"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
import { signToken } from "../lib/jwt";
|
|
4
|
+
import { AuthenticatedRequest } from "../middleware/requireAuth";
|
|
5
|
+
import { createUser, findUserByEmail, toPublicUser } from "../services/userStore";
|
|
6
|
+
|
|
7
|
+
const SALT_ROUNDS = 10;
|
|
8
|
+
|
|
9
|
+
interface RegisterBody {
|
|
10
|
+
email?: string;
|
|
11
|
+
password?: string;
|
|
12
|
+
name?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface LoginBody {
|
|
16
|
+
email?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function register(
|
|
21
|
+
req: Request<object, unknown, RegisterBody>,
|
|
22
|
+
res: Response,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const { email, password, name } = req.body;
|
|
25
|
+
|
|
26
|
+
if (!email || !password) {
|
|
27
|
+
res.status(400).json({ error: "Email and password are required" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const existingUser = await findUserByEmail(email);
|
|
32
|
+
if (existingUser) {
|
|
33
|
+
res.status(409).json({ error: "User already exists" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
|
38
|
+
const user = await createUser({ email, name, password: hashedPassword });
|
|
39
|
+
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
40
|
+
|
|
41
|
+
res.status(201).json({ user: toPublicUser(user), token });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function login(
|
|
45
|
+
req: Request<object, unknown, LoginBody>,
|
|
46
|
+
res: Response,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
const { email, password } = req.body;
|
|
49
|
+
|
|
50
|
+
if (!email || !password) {
|
|
51
|
+
res.status(400).json({ error: "Email and password are required" });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const user = await findUserByEmail(email);
|
|
56
|
+
|
|
57
|
+
if (!user) {
|
|
58
|
+
res.status(401).json({ error: "Invalid credentials" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
63
|
+
|
|
64
|
+
if (!isValidPassword) {
|
|
65
|
+
res.status(401).json({ error: "Invalid credentials" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
70
|
+
|
|
71
|
+
res.json({ user: toPublicUser(user), token });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function me(req: AuthenticatedRequest, res: Response): void {
|
|
75
|
+
if (!req.user) {
|
|
76
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
res.json({ user: req.user });
|
|
81
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import jwt, { type SignOptions } from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
export interface AuthTokenPayload {
|
|
4
|
+
sub: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name?: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { JWT_SECRET, JWT_EXPIRES_IN } = process.env;
|
|
10
|
+
|
|
11
|
+
function ensureSecret(): string {
|
|
12
|
+
if (!JWT_SECRET) {
|
|
13
|
+
throw new Error("JWT_SECRET is not set");
|
|
14
|
+
}
|
|
15
|
+
return JWT_SECRET;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function signToken(payload: AuthTokenPayload): string {
|
|
19
|
+
const secret = ensureSecret();
|
|
20
|
+
const expiresIn = JWT_EXPIRES_IN || "1h";
|
|
21
|
+
return jwt.sign(payload, secret, { expiresIn } as SignOptions);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function verifyToken(token: string): AuthTokenPayload {
|
|
25
|
+
const secret = ensureSecret();
|
|
26
|
+
return jwt.verify(token, secret) as AuthTokenPayload;
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import { AuthTokenPayload, verifyToken } from '../lib/jwt';
|
|
3
|
+
|
|
4
|
+
export interface AuthenticatedRequest extends Request {
|
|
5
|
+
user?: AuthTokenPayload;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
|
|
9
|
+
const authHeader = req.headers.authorization;
|
|
10
|
+
|
|
11
|
+
if (!authHeader || !authHeader.startsWith('Bearer')) {
|
|
12
|
+
res.status(401).json({ error: 'Authorization header missing or invalid' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const token = authHeader.substring('Bearer '.length);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const payload = verifyToken(token);
|
|
20
|
+
req.user = payload;
|
|
21
|
+
next();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('JWT verification failed:', error);
|
|
24
|
+
res.status(401).json({ error: 'Invalid or expired token' });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, Router } from 'express';
|
|
2
|
+
import { login, me, register } from '../controllers/authController';
|
|
3
|
+
import { requireAuth } from '../middleware/requireAuth';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
function asyncHandler(
|
|
8
|
+
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
|
|
9
|
+
): (req: Request, res: Response, next: NextFunction) => void {
|
|
10
|
+
return (req, res, next) => {
|
|
11
|
+
void fn(req, res, next).catch(next);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
router.post('/register', asyncHandler(register));
|
|
16
|
+
router.post('/login', asyncHandler(login));
|
|
17
|
+
router.get('/me', requireAuth, me);
|
|
18
|
+
|
|
19
|
+
export default router;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{{#if (eq database 'none')}}
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if (eq database 'prisma')}}
|
|
5
|
+
import type { User } from '../../app/generated/prisma';
|
|
6
|
+
import { prisma } from '../lib/prisma';
|
|
7
|
+
{{/if}}
|
|
8
|
+
{{#if (eq database 'mongoose')}}
|
|
9
|
+
import UserModel from '../models/User';
|
|
10
|
+
{{/if}}
|
|
11
|
+
|
|
12
|
+
export interface AuthUser {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
name?: string | null;
|
|
16
|
+
password: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CreateUserInput {
|
|
20
|
+
email: string;
|
|
21
|
+
name?: string | null;
|
|
22
|
+
password: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
{{#if (eq database 'none')}}
|
|
26
|
+
const users: AuthUser[] = [];
|
|
27
|
+
{{/if}}
|
|
28
|
+
|
|
29
|
+
export async function findUserByEmail(email: string): Promise<AuthUser | null> {
|
|
30
|
+
{{#if (eq database 'prisma')}}
|
|
31
|
+
const user: User | null = await prisma.user.findUnique({ where: { email } });
|
|
32
|
+
if (!user) return null;
|
|
33
|
+
return {
|
|
34
|
+
id: user.id,
|
|
35
|
+
email: user.email,
|
|
36
|
+
name: user.name,
|
|
37
|
+
password: user.password,
|
|
38
|
+
};
|
|
39
|
+
{{/if}}
|
|
40
|
+
|
|
41
|
+
{{#if (eq database 'mongoose')}}
|
|
42
|
+
const user = await UserModel.findOne({ email }).exec();
|
|
43
|
+
if (!user) return null;
|
|
44
|
+
return {
|
|
45
|
+
id: user.id,
|
|
46
|
+
email: user.email,
|
|
47
|
+
name: user.name,
|
|
48
|
+
password: user.password,
|
|
49
|
+
};
|
|
50
|
+
{{/if}}
|
|
51
|
+
|
|
52
|
+
{{#if (eq database 'none')}}
|
|
53
|
+
const user = users.find((candidate) => candidate.email === email);
|
|
54
|
+
return user ?? null;
|
|
55
|
+
{{/if}}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function createUser(data: CreateUserInput): Promise<AuthUser> {
|
|
59
|
+
{{#if (eq database 'prisma')}}
|
|
60
|
+
const user: User = await prisma.user.create({
|
|
61
|
+
data: {
|
|
62
|
+
email: data.email,
|
|
63
|
+
name: data.name,
|
|
64
|
+
password: data.password,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: user.id,
|
|
70
|
+
email: user.email,
|
|
71
|
+
name: user.name,
|
|
72
|
+
password: user.password,
|
|
73
|
+
};
|
|
74
|
+
{{/if}}
|
|
75
|
+
|
|
76
|
+
{{#if (eq database 'mongoose')}}
|
|
77
|
+
const user = await UserModel.create({
|
|
78
|
+
email: data.email,
|
|
79
|
+
name: data.name,
|
|
80
|
+
password: data.password,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id: user.id,
|
|
85
|
+
email: user.email,
|
|
86
|
+
name: user.name,
|
|
87
|
+
password: user.password,
|
|
88
|
+
};
|
|
89
|
+
{{/if}}
|
|
90
|
+
|
|
91
|
+
{{#if (eq database 'none')}}
|
|
92
|
+
const newUser: AuthUser = {
|
|
93
|
+
id: crypto.randomUUID(),
|
|
94
|
+
email: data.email,
|
|
95
|
+
name: data.name,
|
|
96
|
+
password: data.password,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
users.push(newUser);
|
|
100
|
+
return newUser;
|
|
101
|
+
{{/if}}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function toPublicUser(user: AuthUser): Omit<AuthUser, 'password'> {
|
|
105
|
+
const { id, email, name } = user;
|
|
106
|
+
return { id, email, name };
|
|
107
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { ArrowRight, ShieldCheck } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { SignOutButton } from "@/components/auth/sign-out-button";
|
|
7
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardDescription,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
} from "@/components/ui/card";
|
|
15
|
+
import { auth } from "@/lib/auth";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
export default async function DashboardPage() {
|
|
19
|
+
const session = await auth.api.getSession({
|
|
20
|
+
headers: await headers(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!session) {
|
|
24
|
+
redirect("/sign-in");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<main className="min-h-screen bg-[linear-gradient(180deg,_#f8fafc_0%,_#eef2ff_100%)] px-6 py-16 sm:px-10">
|
|
29
|
+
<div className="mx-auto max-w-5xl space-y-8">
|
|
30
|
+
<header className="flex flex-col gap-6 rounded-[2rem] border border-white/70 bg-white/85 p-8 shadow-xl shadow-slate-200/60 backdrop-blur md:flex-row md:items-end md:justify-between">
|
|
31
|
+
<div className="space-y-3">
|
|
32
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-emerald-700">
|
|
33
|
+
<ShieldCheck className="size-4" />
|
|
34
|
+
Authenticated session
|
|
35
|
+
</span>
|
|
36
|
+
<div className="space-y-1">
|
|
37
|
+
<h1 className="text-3xl font-semibold tracking-tight text-slate-950">
|
|
38
|
+
Welcome {session.user.name}
|
|
39
|
+
</h1>
|
|
40
|
+
<p className="text-sm text-slate-600">{session.user.email}</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<SignOutButton />
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<section className="grid gap-5 md:grid-cols-2">
|
|
47
|
+
<Card>
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<CardTitle>Protected route example</CardTitle>
|
|
50
|
+
<CardDescription>
|
|
51
|
+
This page verifies the session on the server before rendering any
|
|
52
|
+
content.
|
|
53
|
+
</CardDescription>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent className="space-y-4 text-sm text-slate-600">
|
|
56
|
+
<p>
|
|
57
|
+
The generated proxy only performs an optimistic cookie check. The
|
|
58
|
+
authoritative guard happens here with{" "}
|
|
59
|
+
<code className="font-mono text-slate-900">
|
|
60
|
+
auth.api.getSession({"{"} headers: await headers() {"}"})
|
|
61
|
+
</code>
|
|
62
|
+
.
|
|
63
|
+
</p>
|
|
64
|
+
<p>
|
|
65
|
+
That keeps route protection aligned with Better Auth's
|
|
66
|
+
Next.js guidance while staying easy to extend.
|
|
67
|
+
</p>
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
|
|
71
|
+
<Card>
|
|
72
|
+
<CardHeader>
|
|
73
|
+
<CardTitle>Suggested next steps</CardTitle>
|
|
74
|
+
<CardDescription>
|
|
75
|
+
Keep the generated auth flow, then customize the app around your
|
|
76
|
+
product.
|
|
77
|
+
</CardDescription>
|
|
78
|
+
</CardHeader>
|
|
79
|
+
<CardContent className="space-y-3 text-sm text-slate-600">
|
|
80
|
+
<p>Replace the demo dashboard with your first authenticated page.</p>
|
|
81
|
+
<p>Extend the schema with profile or onboarding fields.</p>
|
|
82
|
+
<p>Add provider-based auth or email verification later if needed.</p>
|
|
83
|
+
<Link
|
|
84
|
+
href="/"
|
|
85
|
+
className={cn(buttonVariants({ variant: "outline" }), "w-fit gap-2")}
|
|
86
|
+
>
|
|
87
|
+
Back to home
|
|
88
|
+
<ArrowRight className="size-4" />
|
|
89
|
+
</Link>
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
</section>
|
|
93
|
+
</div>
|
|
94
|
+
</main>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
|
|
4
|
+
import { SignInForm } from "@/components/auth/sign-in-form";
|
|
5
|
+
import { auth } from "@/lib/auth";
|
|
6
|
+
|
|
7
|
+
export default async function SignInPage() {
|
|
8
|
+
const session = await auth.api.getSession({
|
|
9
|
+
headers: await headers(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (session) {
|
|
13
|
+
redirect("/dashboard");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(14,165,233,0.16),_transparent_30%),linear-gradient(180deg,_#f8fafc_0%,_#eef2ff_100%)] px-6 py-16 sm:px-10">
|
|
18
|
+
<div className="mx-auto flex min-h-[calc(100vh-8rem)] max-w-5xl items-center gap-12 lg:grid lg:grid-cols-[0.9fr_1.1fr]">
|
|
19
|
+
<section className="hidden space-y-5 lg:block">
|
|
20
|
+
<span className="inline-flex rounded-full border border-sky-200 bg-white/80 px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-sky-700">
|
|
21
|
+
Better Auth
|
|
22
|
+
</span>
|
|
23
|
+
<h1 className="text-4xl font-semibold tracking-tight text-slate-950">
|
|
24
|
+
Welcome back.
|
|
25
|
+
</h1>
|
|
26
|
+
<p className="max-w-md text-base leading-7 text-slate-600">
|
|
27
|
+
This generated starter includes protected routes, session-aware page
|
|
28
|
+
guards, and reusable auth form primitives you can extend from here.
|
|
29
|
+
</p>
|
|
30
|
+
</section>
|
|
31
|
+
<SignInForm />
|
|
32
|
+
</div>
|
|
33
|
+
</main>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
|
|
4
|
+
import { SignUpForm } from "@/components/auth/sign-up-form";
|
|
5
|
+
import { auth } from "@/lib/auth";
|
|
6
|
+
|
|
7
|
+
export default async function SignUpPage() {
|
|
8
|
+
const session = await auth.api.getSession({
|
|
9
|
+
headers: await headers(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (session) {
|
|
13
|
+
redirect("/dashboard");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(56,189,248,0.18),_transparent_28%),linear-gradient(180deg,_#f8fafc_0%,_#ecfeff_100%)] px-6 py-16 sm:px-10">
|
|
18
|
+
<div className="mx-auto flex min-h-[calc(100vh-8rem)] max-w-5xl items-center gap-12 lg:grid lg:grid-cols-[0.9fr_1.1fr]">
|
|
19
|
+
<section className="hidden space-y-5 lg:block">
|
|
20
|
+
<span className="inline-flex rounded-full border border-sky-200 bg-white/80 px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-sky-700">
|
|
21
|
+
Better Auth
|
|
22
|
+
</span>
|
|
23
|
+
<h1 className="text-4xl font-semibold tracking-tight text-slate-950">
|
|
24
|
+
Create your account.
|
|
25
|
+
</h1>
|
|
26
|
+
<p className="max-w-md text-base leading-7 text-slate-600">
|
|
27
|
+
Start with email and password today, then layer in profile data,
|
|
28
|
+
onboarding, and provider-based auth when your product needs it.
|
|
29
|
+
</p>
|
|
30
|
+
</section>
|
|
31
|
+
<SignUpForm />
|
|
32
|
+
</div>
|
|
33
|
+
</main>
|
|
34
|
+
);
|
|
35
|
+
}
|