better-ts-stack 1.1.2 → 1.1.3
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 +16 -27
- package/dist/builder/configGenerator.d.ts.map +1 -1
- package/dist/builder/configGenerator.js +5 -1
- package/dist/builder/configGenerator.js.map +1 -1
- package/dist/builder/moduleSelector.d.ts.map +1 -1
- package/dist/builder/moduleSelector.js +2 -5
- package/dist/builder/moduleSelector.js.map +1 -1
- package/dist/output/nextSteps.d.ts.map +1 -1
- package/dist/output/nextSteps.js +22 -0
- package/dist/output/nextSteps.js.map +1 -1
- package/package.json +1 -1
- package/templates/backend/express/src/index.ts +19 -4
- package/templates/frontend/nextjs/config.json +1 -2
- package/templates/frontend/nextjs/next.config.ts.hbs +12 -0
- package/templates/frontend/tanstack/config.json +48 -49
- package/templates/modules/auth/express/src/controllers/authController.ts.hbs +62 -13
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -26
- package/templates/modules/auth/express/src/services/userStore.ts.hbs +22 -0
- package/templates/modules/docker/Dockerfile.hbs +130 -125
- package/templates/modules/docker/config.json +1 -1
- package/templates/modules/mongoose/{config.json → express/config.json} +2 -2
- package/templates/modules/mongoose/{src → express/src}/models/User.ts +48 -48
- package/templates/modules/mongoose/nextjs/config.json +16 -0
- package/templates/modules/mongoose/nextjs/lib/db.ts +36 -0
- package/templates/modules/mongoose/nextjs/models/User.ts +48 -0
- package/templates/modules/mongoose/tanstack/config.json +17 -0
- package/templates/modules/mongoose/tanstack/src/lib/db.ts +37 -0
- package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -0
- package/templates/frontend/nextjs/next.config.ts +0 -8
- package/templates/modules/mongoose/src/lib/db.ts.hbs +0 -43
- /package/templates/modules/mongoose/{src → express/src}/lib/db.ts +0 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# better-ts-stack
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> An interactive CLI that scaffolds TypeScript backend and full-stack projects.
|
|
4
4
|
|
|
5
|
-
`better-ts-stack`
|
|
5
|
+
`better-ts-stack` creates an Express, Next.js, or TanStack Start project with TypeScript, ESLint, and Prettier set up. Database, authentication, Docker, and git are optional.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -15,30 +15,29 @@ npm install -g better-ts-stack
|
|
|
15
15
|
better-ts-stack
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Run the command
|
|
18
|
+
Run the command and answer the prompts. The CLI creates the project folder.
|
|
19
19
|
|
|
20
20
|
## What It Can Scaffold
|
|
21
21
|
|
|
22
|
-
| Project type | Stack
|
|
23
|
-
| -------------- |
|
|
24
|
-
| Backend API | Express + TypeScript
|
|
25
|
-
| Full-stack app | Next.js 16 + React 19
|
|
22
|
+
| Project type | Stack | Optional setup |
|
|
23
|
+
| -------------- | --------------------------------------- | ------------------------------------------------------------------------ |
|
|
24
|
+
| Backend API | Express 5 + TypeScript | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, JWT auth, Docker, git |
|
|
25
|
+
| Full-stack app | Next.js 16 or TanStack Start + React 19 | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, Better Auth, Docker, git |
|
|
26
26
|
|
|
27
27
|
Notes:
|
|
28
28
|
|
|
29
|
-
- Express is the
|
|
30
|
-
-
|
|
31
|
-
- Better Auth is only available for full-stack projects when a database is selected.
|
|
29
|
+
- Express is the only backend framework for now. NestJS is listed in the prompt as "coming soon".
|
|
30
|
+
- Better Auth is only offered for full-stack projects that use a database.
|
|
32
31
|
|
|
33
32
|
## Interactive Prompt Flow
|
|
34
33
|
|
|
35
|
-
The CLI
|
|
36
|
-
|
|
37
|
-
You will be guided through:
|
|
34
|
+
The CLI has no subcommands or flags. It asks, in order:
|
|
38
35
|
|
|
39
36
|
1. Project name
|
|
40
37
|
2. Application type: `Backend API` or `Full-stack App`
|
|
41
|
-
3.
|
|
38
|
+
3. Framework
|
|
39
|
+
- Backend: `Express` (`NestJS` is coming soon)
|
|
40
|
+
- Full-stack: `Next.js` or `TanStack Start`
|
|
42
41
|
4. Database: `none`, `PostgreSQL`, or `MongoDB`
|
|
43
42
|
5. ORM/ODM when a database is selected
|
|
44
43
|
- PostgreSQL: `Prisma` or `Drizzle`
|
|
@@ -47,7 +46,7 @@ You will be guided through:
|
|
|
47
46
|
7. Docker setup
|
|
48
47
|
8. Authentication
|
|
49
48
|
- Express apps: JWT-based auth
|
|
50
|
-
- Next.js apps: Better Auth when a database is
|
|
49
|
+
- Next.js and TanStack Start apps: Better Auth, only when a database is selected
|
|
51
50
|
9. Git initialization
|
|
52
51
|
10. Dependency installation
|
|
53
52
|
|
|
@@ -55,14 +54,14 @@ The CLI does not ask for a port. Backend projects default to `PORT=3000`.
|
|
|
55
54
|
|
|
56
55
|
## What You Get
|
|
57
56
|
|
|
58
|
-
Every generated project includes
|
|
57
|
+
Every generated project includes:
|
|
59
58
|
|
|
60
59
|
- TypeScript with strict mode enabled
|
|
61
60
|
- ESLint and Prettier pre-configured
|
|
62
61
|
- `.env.example` and `.env` setup
|
|
63
62
|
- Ready-to-run project scripts
|
|
64
63
|
- Health check endpoint at `/health` for backend projects
|
|
65
|
-
- shadcn-compatible UI
|
|
64
|
+
- shadcn-compatible UI components for Next.js and TanStack Start projects
|
|
66
65
|
- Database wiring and schema setup when selected
|
|
67
66
|
- Authentication scaffolding when selected
|
|
68
67
|
- Docker files when selected
|
|
@@ -81,13 +80,3 @@ Every generated project includes the essentials:
|
|
|
81
80
|
## License
|
|
82
81
|
|
|
83
82
|
MIT
|
|
84
|
-
|
|
85
|
-
## Release 1.1.2
|
|
86
|
-
|
|
87
|
-
Frontend starters now allow committing `.env.example`, and Express starters allow committing lockfiles. Setup instructions follow installation, database preparation, and server startup before account creation. Git documentation now accurately describes repository initialization.
|
|
88
|
-
|
|
89
|
-
## Release 1.0.8
|
|
90
|
-
|
|
91
|
-
Requires Node.js 24+. Dependencies and templates use current stable releases, with TypeScript 6.0.3 held for typescript-eslint compatibility and Node.js types aligned to 24.x. Express and Next.js support PostgreSQL with Prisma or Drizzle and MongoDB with Mongoose. Prisma generation runs before builds; Docker uses Node.js 24 and Compose v2.
|
|
92
|
-
|
|
93
|
-
See [upgrade notes](https://better-ts-stack.abdullahtech.me/docs/upgrade-notes) for compatibility changes and maintainer publishing commands.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configGenerator.d.ts","sourceRoot":"","sources":["../../src/builder/configGenerator.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,UAAU,CAAC;AAGlB,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAGD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,eAAe,GACvB,YAAY,CAuBd;AAGD,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"configGenerator.d.ts","sourceRoot":"","sources":["../../src/builder/configGenerator.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,UAAU,CAAC;AAGlB,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAGD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,eAAe,GACvB,YAAY,CAuBd;AAGD,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,IAAI,CAAC,CAgDf;AAGD,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,CAgCf"}
|
|
@@ -60,7 +60,11 @@ async function generatePackageJson(targetDir, mergedConfig, config) {
|
|
|
60
60
|
name: config.projectName,
|
|
61
61
|
version: "1.0.0",
|
|
62
62
|
private: true,
|
|
63
|
-
|
|
63
|
+
// TanStack's srvx static serving needs Node 24.20+ on Windows (earlier
|
|
64
|
+
// 24.x reports mismatched file stats there, so every asset 404s)
|
|
65
|
+
engines: {
|
|
66
|
+
node: config.framework === "tanstack" ? ">=24.20.0" : ">=24.0.0",
|
|
67
|
+
},
|
|
64
68
|
description: `Project created with better-ts-stack using ${config.database !== "none" ? config.database : "no database"}`,
|
|
65
69
|
...(config.framework === "tanstack" ? { type: "module" } : {}),
|
|
66
70
|
...(isFullstack ? {} : { main: "dist/index.js" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configGenerator.js","sourceRoot":"","sources":["../../src/builder/configGenerator.ts"],"names":[],"mappings":";;;;;AAeA,wDAsBC;AAGD,kDA0BC;AAGD,
|
|
1
|
+
{"version":3,"file":"configGenerator.js","sourceRoot":"","sources":["../../src/builder/configGenerator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moduleSelector.d.ts","sourceRoot":"","sources":["../../src/builder/moduleSelector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa;;;YAUtB,MAAM;mBAAa,MAAM;;
|
|
1
|
+
{"version":3,"file":"moduleSelector.d.ts","sourceRoot":"","sources":["../../src/builder/moduleSelector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa;;;YAUtB,MAAM;mBAAa,MAAM;;EAiBrD"}
|
|
@@ -10,12 +10,9 @@ function selectModules(config) {
|
|
|
10
10
|
? `backend/${config.framework}`
|
|
11
11
|
: `frontend/${config.framework}`;
|
|
12
12
|
const modules = [];
|
|
13
|
-
// Add database module if selected (compound ids: prisma/express,
|
|
13
|
+
// Add database module if selected (compound ids: prisma/express, drizzle/nextjs, mongoose/tanstack, ...)
|
|
14
14
|
if (config.database !== "none") {
|
|
15
|
-
|
|
16
|
-
? "mongoose"
|
|
17
|
-
: `${config.database}/${framework}`;
|
|
18
|
-
modules.push({ id: dbId, framework });
|
|
15
|
+
modules.push({ id: `${config.database}/${framework}`, framework });
|
|
19
16
|
}
|
|
20
17
|
// Add feature modules based on configuration
|
|
21
18
|
if (config.useDocker) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moduleSelector.js","sourceRoot":"","sources":["../../src/builder/moduleSelector.ts"],"names":[],"mappings":";;AAGA,
|
|
1
|
+
{"version":3,"file":"moduleSelector.js","sourceRoot":"","sources":["../../src/builder/moduleSelector.ts"],"names":[],"mappings":";;AAGA,sCA2BC;AA5BD,mFAAmF;AACnF,SAAgB,aAAa,CAAC,MAAqB;IACjD,6CAA6C;IAC7C,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IAEnC,yCAAyC;IACzC,MAAM,IAAI,GACR,MAAM,CAAC,eAAe,KAAK,SAAS;QAClC,CAAC,CAAC,WAAW,MAAM,CAAC,SAAS,EAAE;QAC/B,CAAC,CAAC,YAAY,MAAM,CAAC,SAAS,EAAE,CAAC;IAErC,MAAM,OAAO,GAA6C,EAAE,CAAC;IAE7D,yGAAyG;IACzG,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,MAAM,CAAC,QAAQ,IAAI,SAAS,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,6CAA6C;IAC7C,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,SAAS,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nextSteps.d.ts","sourceRoot":"","sources":["../../src/output/nextSteps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"nextSteps.d.ts","sourceRoot":"","sources":["../../src/output/nextSteps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAmBzC,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,aAAa,EACrB,aAAa,EAAE,OAAO,GACrB,MAAM,EAAE,CAwEV;AAGD,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EAAE,GACd,MAAM,CAeR"}
|
package/dist/output/nextSteps.js
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateNextSteps = generateNextSteps;
|
|
4
4
|
exports.displayNextSteps = displayNextSteps;
|
|
5
|
+
const MONGODB_AUTH_SETUP_URL = "https://better-ts-stack.abdullahtech.me/docs/modules/database#mongoose-and-mongodb";
|
|
6
|
+
// Node 24 releases before 24.20 report mismatched file stats on Windows, which
|
|
7
|
+
// makes srvx (TanStack's production server) reject every static asset
|
|
8
|
+
const MIN_TANSTACK_WINDOWS_NODE = [24, 20];
|
|
9
|
+
function isNodeBelow([major, minor]) {
|
|
10
|
+
const [currentMajor, currentMinor] = process.versions.node
|
|
11
|
+
.split(".")
|
|
12
|
+
.map(Number);
|
|
13
|
+
return (currentMajor < major || (currentMajor === major && currentMinor < minor));
|
|
14
|
+
}
|
|
5
15
|
// Generates a list of instructions for the user to follow after project creation
|
|
6
16
|
function generateNextSteps(config, depsInstalled) {
|
|
7
17
|
const steps = [];
|
|
@@ -17,6 +27,12 @@ function generateNextSteps(config, depsInstalled) {
|
|
|
17
27
|
? "Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env"
|
|
18
28
|
: "Review the generated JWT_SECRET in .env");
|
|
19
29
|
}
|
|
30
|
+
if (config.framework === "tanstack" &&
|
|
31
|
+
process.platform === "win32" &&
|
|
32
|
+
isNodeBelow(MIN_TANSTACK_WINDOWS_NODE)) {
|
|
33
|
+
steps.push(`Upgrade Node.js to ${MIN_TANSTACK_WINDOWS_NODE.join(".")} or newer: on Windows, ` +
|
|
34
|
+
`Node ${process.versions.node} makes "npm start" return 404 for CSS, JS, and other static files`);
|
|
35
|
+
}
|
|
20
36
|
// Step 3: Install dependencies if not already done
|
|
21
37
|
if (!depsInstalled) {
|
|
22
38
|
steps.push(`${config.packageManager} install`);
|
|
@@ -32,6 +48,12 @@ function generateNextSteps(config, depsInstalled) {
|
|
|
32
48
|
}
|
|
33
49
|
else if (config.database === "mongoose") {
|
|
34
50
|
steps.push("Ensure MongoDB is running locally or update MONGODB_URI in .env");
|
|
51
|
+
// Better Auth's MongoDB adapter uses transactions, which need a replica set
|
|
52
|
+
if (config.useAuth && isFullstack) {
|
|
53
|
+
steps.push("Better Auth uses MongoDB transactions: run MongoDB as a replica set (or use Atlas) " +
|
|
54
|
+
'and create the "user", "session", "account", and "verification" collections before signing up. ' +
|
|
55
|
+
`See ${MONGODB_AUTH_SETUP_URL}`);
|
|
56
|
+
}
|
|
35
57
|
}
|
|
36
58
|
// Step 5: Start dev server
|
|
37
59
|
steps.push(`${config.packageManager} run dev`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nextSteps.js","sourceRoot":"","sources":["../../src/output/nextSteps.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"nextSteps.js","sourceRoot":"","sources":["../../src/output/nextSteps.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-ts-stack",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "A powerful CLI tool that generates fully configured TypeScript projects with backend, frontend, database integration, Docker support, and more--all through an interactive setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -35,10 +35,25 @@ app.use((_req: Request, res: Response) => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// Error handler
|
|
38
|
-
app.use(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
app.use(
|
|
39
|
+
(
|
|
40
|
+
err: Error & { status?: number; statusCode?: number; expose?: boolean },
|
|
41
|
+
_req: Request,
|
|
42
|
+
res: Response,
|
|
43
|
+
_next: NextFunction,
|
|
44
|
+
) => {
|
|
45
|
+
// Middleware such as express.json() sets 4xx statuses (bad JSON, body too large)
|
|
46
|
+
const status = err.status ?? err.statusCode ?? 500;
|
|
47
|
+
|
|
48
|
+
if (status >= 400 && status < 500) {
|
|
49
|
+
res.status(status).json({ error: err.expose ? err.message : 'Bad Request' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.error(err.stack);
|
|
54
|
+
res.status(500).json({ error: 'Internal Server Error' });
|
|
55
|
+
},
|
|
56
|
+
);
|
|
42
57
|
|
|
43
58
|
{{#if (eq database 'mongoose')}}
|
|
44
59
|
void awaitDatabase().catch((error: unknown) => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
{{#if useDocker}}
|
|
4
|
+
const nextConfig: NextConfig = {
|
|
5
|
+
// Standalone output is what the Docker image runs (node server.js)
|
|
6
|
+
output: "standalone",
|
|
7
|
+
};
|
|
8
|
+
{{else}}
|
|
9
|
+
const nextConfig: NextConfig = {};
|
|
10
|
+
{{/if}}
|
|
11
|
+
|
|
12
|
+
export default nextConfig;
|
|
@@ -1,49 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "tanstack",
|
|
3
|
-
"name": "TanStack Start",
|
|
4
|
-
"description": "TanStack Start with TanStack Router, React 19, and Tailwind CSS v4",
|
|
5
|
-
"type": "base",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "vite dev",
|
|
8
|
-
"build": "vite build",
|
|
9
|
-
"start": "srvx --prod -s ../client dist/server/server.js",
|
|
10
|
-
"preview": "vite preview",
|
|
11
|
-
"type:check": "tsc --noEmit",
|
|
12
|
-
"lint": "eslint src"
|
|
13
|
-
},
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"@tanstack/react-query": "^5.102.8",
|
|
16
|
-
"@tanstack/react-query-devtools": "^5.102.8",
|
|
17
|
-
"@tanstack/react-router": "^1.170.36",
|
|
18
|
-
"@tanstack/react-router-devtools": "^1.167.2",
|
|
19
|
-
"@tanstack/react-router-ssr-query": "^1.167.2",
|
|
20
|
-
"@tanstack/react-start": "^1.168.53",
|
|
21
|
-
"class-variance-authority": "^0.7.1",
|
|
22
|
-
"clsx": "^2.1.1",
|
|
23
|
-
"lucide-react": "^1.41.0",
|
|
24
|
-
"react": "^19.2.8",
|
|
25
|
-
"react-dom": "^19.2.8",
|
|
26
|
-
"srvx": "^1.0.4",
|
|
27
|
-
"tailwind-merge": "^3.6.0"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@eslint/js": "^10.0.1",
|
|
31
|
-
"@tailwindcss/vite": "^4.3.3",
|
|
32
|
-
"@types/node": "^24.13.3",
|
|
33
|
-
"@types/react": "^19.2.18",
|
|
34
|
-
"@types/react-dom": "^19.2.7",
|
|
35
|
-
"@vitejs/plugin-react": "^6.1.1",
|
|
36
|
-
"eslint": "^10.10.0",
|
|
37
|
-
"tailwindcss": "^4.3.3",
|
|
38
|
-
"typescript": "^6.0.3",
|
|
39
|
-
"typescript-eslint": "^8.69.0",
|
|
40
|
-
"vite": "^8.3.0"
|
|
41
|
-
},
|
|
42
|
-
"envVars": {
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "tanstack",
|
|
3
|
+
"name": "TanStack Start",
|
|
4
|
+
"description": "TanStack Start with TanStack Router, React 19, and Tailwind CSS v4",
|
|
5
|
+
"type": "base",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"start": "srvx --prod -s ../client dist/server/server.js",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"type:check": "tsc --noEmit",
|
|
12
|
+
"lint": "eslint src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@tanstack/react-query": "^5.102.8",
|
|
16
|
+
"@tanstack/react-query-devtools": "^5.102.8",
|
|
17
|
+
"@tanstack/react-router": "^1.170.36",
|
|
18
|
+
"@tanstack/react-router-devtools": "^1.167.2",
|
|
19
|
+
"@tanstack/react-router-ssr-query": "^1.167.2",
|
|
20
|
+
"@tanstack/react-start": "^1.168.53",
|
|
21
|
+
"class-variance-authority": "^0.7.1",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"lucide-react": "^1.41.0",
|
|
24
|
+
"react": "^19.2.8",
|
|
25
|
+
"react-dom": "^19.2.8",
|
|
26
|
+
"srvx": "^1.0.4",
|
|
27
|
+
"tailwind-merge": "^3.6.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@eslint/js": "^10.0.1",
|
|
31
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
32
|
+
"@types/node": "^24.13.3",
|
|
33
|
+
"@types/react": "^19.2.18",
|
|
34
|
+
"@types/react-dom": "^19.2.7",
|
|
35
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
36
|
+
"eslint": "^10.10.0",
|
|
37
|
+
"tailwindcss": "^4.3.3",
|
|
38
|
+
"typescript": "^6.0.3",
|
|
39
|
+
"typescript-eslint": "^8.69.0",
|
|
40
|
+
"vite": "^8.3.0"
|
|
41
|
+
},
|
|
42
|
+
"envVars": {
|
|
43
|
+
"PORT": "3000"
|
|
44
|
+
},
|
|
45
|
+
"templateFiles": [
|
|
46
|
+
"src/routes/index.tsx.hbs"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -2,32 +2,67 @@ import { Request, Response } from "express";
|
|
|
2
2
|
import bcrypt from "bcrypt";
|
|
3
3
|
import { signToken } from "../lib/jwt";
|
|
4
4
|
import { AuthenticatedRequest } from "../middleware/requireAuth";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createUser,
|
|
7
|
+
findUserByEmail,
|
|
8
|
+
isDuplicateEmailError,
|
|
9
|
+
toPublicUser,
|
|
10
|
+
} from "../services/userStore";
|
|
6
11
|
|
|
7
12
|
const SALT_ROUNDS = 10;
|
|
13
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
14
|
+
// bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
|
|
15
|
+
const MAX_PASSWORD_BYTES = 72;
|
|
8
16
|
|
|
17
|
+
// Request bodies are untrusted JSON, so fields may have any type
|
|
9
18
|
interface RegisterBody {
|
|
10
|
-
email?:
|
|
11
|
-
password?:
|
|
12
|
-
name?:
|
|
19
|
+
email?: unknown;
|
|
20
|
+
password?: unknown;
|
|
21
|
+
name?: unknown;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
interface LoginBody {
|
|
16
|
-
email?:
|
|
17
|
-
password?:
|
|
25
|
+
email?: unknown;
|
|
26
|
+
password?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Returns a trimmed, lowercased email, or null when the value is not a usable string
|
|
30
|
+
function normalizeEmail(email: unknown): string | null {
|
|
31
|
+
if (typeof email !== "string") return null;
|
|
32
|
+
const normalized = email.trim().toLowerCase();
|
|
33
|
+
return normalized || null;
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
export async function register(
|
|
21
|
-
req: Request<object, unknown, RegisterBody>,
|
|
37
|
+
req: Request<object, unknown, RegisterBody | undefined>,
|
|
22
38
|
res: Response,
|
|
23
39
|
): Promise<void> {
|
|
24
|
-
|
|
40
|
+
// Express 5 leaves req.body undefined when the request has no body
|
|
41
|
+
const body = req.body ?? {};
|
|
42
|
+
const email = normalizeEmail(body.email);
|
|
43
|
+
const password = body.password;
|
|
44
|
+
const name = body.name ?? null;
|
|
25
45
|
|
|
26
|
-
if (!email || !password) {
|
|
46
|
+
if (!email || typeof password !== "string" || !password) {
|
|
27
47
|
res.status(400).json({ error: "Email and password are required" });
|
|
28
48
|
return;
|
|
29
49
|
}
|
|
30
50
|
|
|
51
|
+
if (
|
|
52
|
+
password.length < MIN_PASSWORD_LENGTH ||
|
|
53
|
+
Buffer.byteLength(password, "utf8") > MAX_PASSWORD_BYTES
|
|
54
|
+
) {
|
|
55
|
+
res.status(400).json({
|
|
56
|
+
error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters and at most ${MAX_PASSWORD_BYTES} bytes`,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (name !== null && typeof name !== "string") {
|
|
62
|
+
res.status(400).json({ error: "Name must be a string" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
const existingUser = await findUserByEmail(email);
|
|
32
67
|
if (existingUser) {
|
|
33
68
|
res.status(409).json({ error: "User already exists" });
|
|
@@ -35,19 +70,33 @@ export async function register(
|
|
|
35
70
|
}
|
|
36
71
|
|
|
37
72
|
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
|
38
|
-
|
|
73
|
+
|
|
74
|
+
let user;
|
|
75
|
+
try {
|
|
76
|
+
user = await createUser({ email, name, password: hashedPassword });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// A concurrent request registered the same email after the check above
|
|
79
|
+
if (isDuplicateEmailError(error)) {
|
|
80
|
+
res.status(409).json({ error: "User already exists" });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
39
86
|
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
40
87
|
|
|
41
88
|
res.status(201).json({ user: toPublicUser(user), token });
|
|
42
89
|
}
|
|
43
90
|
|
|
44
91
|
export async function login(
|
|
45
|
-
req: Request<object, unknown, LoginBody>,
|
|
92
|
+
req: Request<object, unknown, LoginBody | undefined>,
|
|
46
93
|
res: Response,
|
|
47
94
|
): Promise<void> {
|
|
48
|
-
const
|
|
95
|
+
const body = req.body ?? {};
|
|
96
|
+
const email = normalizeEmail(body.email);
|
|
97
|
+
const password = body.password;
|
|
49
98
|
|
|
50
|
-
if (!email || !password) {
|
|
99
|
+
if (!email || typeof password !== "string" || !password) {
|
|
51
100
|
res.status(400).json({ error: "Email and password are required" });
|
|
52
101
|
return;
|
|
53
102
|
}
|
|
@@ -1,26 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -77,6 +77,14 @@ export {{#unless (eq database 'none')}}async {{/unless}}function createUser(data
|
|
|
77
77
|
{{/if}}
|
|
78
78
|
|
|
79
79
|
{{#if (eq database 'none')}}
|
|
80
|
+
// Enforce unique emails like a database constraint would, so a sign-up that
|
|
81
|
+
// raced past the findUserByEmail check is rejected instead of duplicated
|
|
82
|
+
if (users.some((candidate) => candidate.email === data.email)) {
|
|
83
|
+
return Promise.reject(
|
|
84
|
+
Object.assign(new Error('User already exists'), { code: 'DUPLICATE_EMAIL' }),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
const newUser: AuthUser = {
|
|
81
89
|
id: crypto.randomUUID(),
|
|
82
90
|
email: data.email,
|
|
@@ -89,6 +97,20 @@ export {{#unless (eq database 'none')}}async {{/unless}}function createUser(data
|
|
|
89
97
|
{{/if}}
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
// Unique-constraint error codes: Prisma, PostgreSQL (Drizzle wraps it in `cause`),
|
|
101
|
+
// MongoDB, and the in-memory store
|
|
102
|
+
const DUPLICATE_KEY_CODES = new Set<unknown>(['P2002', '23505', 11000, 'DUPLICATE_EMAIL']);
|
|
103
|
+
|
|
104
|
+
// True when an insert failed because the email is already taken (e.g. concurrent sign-ups)
|
|
105
|
+
export function isDuplicateEmailError(error: unknown): boolean {
|
|
106
|
+
let current = error;
|
|
107
|
+
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth++) {
|
|
108
|
+
if (DUPLICATE_KEY_CODES.has((current as { code?: unknown }).code)) return true;
|
|
109
|
+
current = (current as { cause?: unknown }).cause;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
export function toPublicUser(user: AuthUser): Omit<AuthUser, 'password'> {
|
|
93
115
|
const { id, email, name } = user;
|
|
94
116
|
return { id, email, name };
|
|
@@ -1,126 +1,131 @@
|
|
|
1
|
-
{{#if (eq framework 'nextjs')}}
|
|
2
|
-
# Base stage
|
|
3
|
-
FROM node:24-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
|
-
{{#if
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
{{#if (eq framework 'nextjs')}}
|
|
2
|
+
# Base stage
|
|
3
|
+
FROM node:24-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
|
+
{{#if useAuth}}
|
|
17
|
+
# next build loads the auth route, which initializes Better Auth. Use a
|
|
18
|
+
# build-only secret here; the real BETTER_AUTH_SECRET is supplied at runtime.
|
|
19
|
+
ENV BETTER_AUTH_SECRET="build-only-placeholder-secret-not-used-at-runtime"
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{#if (eq database 'prisma')}}
|
|
22
|
+
RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
|
|
23
|
+
{{else if (eq database 'mongoose')}}
|
|
24
|
+
RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
|
|
25
|
+
{{else}}
|
|
26
|
+
RUN npm run build
|
|
27
|
+
{{/if}}
|
|
28
|
+
|
|
29
|
+
# Production stage
|
|
30
|
+
FROM node:24-alpine AS runner
|
|
31
|
+
WORKDIR /app
|
|
32
|
+
ENV NODE_ENV=production
|
|
33
|
+
|
|
34
|
+
RUN addgroup --system --gid 1001 nodejs && \
|
|
35
|
+
adduser --system --uid 1001 nextjs
|
|
36
|
+
|
|
37
|
+
COPY --from=builder /app/public ./public
|
|
38
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
39
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
40
|
+
|
|
41
|
+
USER nextjs
|
|
42
|
+
EXPOSE {{port}}
|
|
43
|
+
ENV PORT={{port}} HOSTNAME=0.0.0.0
|
|
44
|
+
|
|
45
|
+
CMD ["node", "server.js"]
|
|
46
|
+
|
|
47
|
+
{{else if (eq framework 'tanstack')}}
|
|
48
|
+
# Build stage
|
|
49
|
+
FROM node:24-alpine AS builder
|
|
50
|
+
WORKDIR /app
|
|
51
|
+
|
|
52
|
+
COPY package*.json ./
|
|
53
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
54
|
+
|
|
55
|
+
COPY . .
|
|
56
|
+
|
|
57
|
+
# Only build if package.json has a build script
|
|
58
|
+
{{#if (eq database 'prisma')}}
|
|
59
|
+
RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
|
|
60
|
+
{{else if (eq database 'mongoose')}}
|
|
61
|
+
RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
|
|
62
|
+
{{else}}
|
|
63
|
+
RUN npm run build
|
|
64
|
+
{{/if}}
|
|
65
|
+
|
|
66
|
+
# Production stage
|
|
67
|
+
FROM node:24-alpine
|
|
68
|
+
WORKDIR /app
|
|
69
|
+
ENV NODE_ENV=production
|
|
70
|
+
|
|
71
|
+
# Create non-root user for security
|
|
72
|
+
RUN addgroup --system --gid 1001 nodejs && \
|
|
73
|
+
adduser --system --uid 1001 tanstackuser
|
|
74
|
+
|
|
75
|
+
COPY package*.json ./
|
|
76
|
+
RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
|
|
77
|
+
|
|
78
|
+
# Copy built output (dist/server/server.js + dist/client)
|
|
79
|
+
COPY --from=builder --chown=tanstackuser:nodejs /app/dist ./dist
|
|
80
|
+
|
|
81
|
+
# Include the Prisma client generated during the build stage
|
|
82
|
+
{{#if (eq database 'prisma')}}
|
|
83
|
+
COPY --from=builder --chown=tanstackuser:nodejs /app/generated ./generated
|
|
84
|
+
{{/if}}
|
|
85
|
+
|
|
86
|
+
USER tanstackuser
|
|
87
|
+
EXPOSE {{port}}
|
|
88
|
+
ENV PORT={{port}}
|
|
89
|
+
|
|
90
|
+
CMD ["node_modules/.bin/srvx", "--prod", "-s", "../client", "dist/server/server.js"]
|
|
91
|
+
|
|
92
|
+
{{else}}
|
|
93
|
+
# Build stage
|
|
94
|
+
FROM node:24-alpine AS builder
|
|
95
|
+
WORKDIR /app
|
|
96
|
+
|
|
97
|
+
COPY package*.json ./
|
|
98
|
+
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
99
|
+
|
|
100
|
+
COPY . .
|
|
101
|
+
|
|
102
|
+
# Only build if package.json has a build script
|
|
103
|
+
{{#if (eq database 'prisma')}}
|
|
104
|
+
RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
|
|
105
|
+
{{else if (eq database 'mongoose')}}
|
|
106
|
+
RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
|
|
107
|
+
{{else}}
|
|
108
|
+
RUN npm run build
|
|
109
|
+
{{/if}}
|
|
110
|
+
|
|
111
|
+
# Production stage
|
|
112
|
+
FROM node:24-alpine
|
|
113
|
+
WORKDIR /app
|
|
114
|
+
ENV NODE_ENV=production
|
|
115
|
+
|
|
116
|
+
# Create non-root user for security
|
|
117
|
+
RUN addgroup --system --gid 1001 nodejs && \
|
|
118
|
+
adduser --system --uid 1001 expressuser
|
|
119
|
+
|
|
120
|
+
COPY package*.json ./
|
|
121
|
+
RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
|
|
122
|
+
|
|
123
|
+
# Copy built files (if TypeScript) or source files (if JavaScript)
|
|
124
|
+
COPY --from=builder --chown=expressuser:nodejs /app/dist ./dist
|
|
125
|
+
|
|
126
|
+
USER expressuser
|
|
127
|
+
EXPOSE {{port}}
|
|
128
|
+
ENV PORT={{port}}
|
|
129
|
+
|
|
130
|
+
CMD ["node", "dist/index.js"]
|
|
126
131
|
{{/if}}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"devDependencies": {},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"docker:build": "docker build -t {{projectName}} .",
|
|
10
|
-
"docker:up": "docker compose up -d",
|
|
10
|
+
"docker:up": "docker compose up -d --build",
|
|
11
11
|
"docker:down": "docker compose down",
|
|
12
12
|
"docker:logs": "docker compose logs -f"
|
|
13
13
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mongoose",
|
|
3
3
|
"name": "Mongoose (MongoDB)",
|
|
4
|
-
"description": "MongoDB database integration using Mongoose ODM",
|
|
4
|
+
"description": "MongoDB database integration for Express using Mongoose ODM",
|
|
5
5
|
"type": "database",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"mongoose": "^9.9.5",
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
"envVars": {
|
|
13
13
|
"MONGODB_URI": "mongodb://localhost:27017/myapp"
|
|
14
14
|
},
|
|
15
|
-
"templateFiles": [
|
|
15
|
+
"templateFiles": []
|
|
16
16
|
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import mongoose, { type Model, Schema } from "mongoose";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* User document interface
|
|
5
|
-
*/
|
|
6
|
-
export interface IUser {
|
|
7
|
-
email: string;
|
|
8
|
-
name?: string | null;
|
|
9
|
-
password: string;
|
|
10
|
-
createdAt: Date;
|
|
11
|
-
updatedAt: Date;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* User schema definition
|
|
16
|
-
*/
|
|
17
|
-
const userSchema = new Schema<IUser>(
|
|
18
|
-
{
|
|
19
|
-
email: {
|
|
20
|
-
type: String,
|
|
21
|
-
required: true,
|
|
22
|
-
unique: true,
|
|
23
|
-
lowercase: true,
|
|
24
|
-
trim: true,
|
|
25
|
-
},
|
|
26
|
-
name: {
|
|
27
|
-
type: String,
|
|
28
|
-
trim: true,
|
|
29
|
-
},
|
|
30
|
-
password: {
|
|
31
|
-
type: String,
|
|
32
|
-
required: true,
|
|
33
|
-
minlength: 6,
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
timestamps: true,
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* User model
|
|
43
|
-
*/
|
|
44
|
-
const User =
|
|
45
|
-
(mongoose.models.User as Model<IUser> | undefined) ??
|
|
46
|
-
mongoose.model<IUser>("User", userSchema);
|
|
47
|
-
|
|
48
|
-
export default User;
|
|
1
|
+
import mongoose, { type Model, Schema } from "mongoose";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User document interface
|
|
5
|
+
*/
|
|
6
|
+
export interface IUser {
|
|
7
|
+
email: string;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
password: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* User schema definition
|
|
16
|
+
*/
|
|
17
|
+
const userSchema = new Schema<IUser>(
|
|
18
|
+
{
|
|
19
|
+
email: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: true,
|
|
22
|
+
unique: true,
|
|
23
|
+
lowercase: true,
|
|
24
|
+
trim: true,
|
|
25
|
+
},
|
|
26
|
+
name: {
|
|
27
|
+
type: String,
|
|
28
|
+
trim: true,
|
|
29
|
+
},
|
|
30
|
+
password: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: true,
|
|
33
|
+
minlength: 6,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
timestamps: true,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* User model
|
|
43
|
+
*/
|
|
44
|
+
const User =
|
|
45
|
+
(mongoose.models.User as Model<IUser> | undefined) ??
|
|
46
|
+
mongoose.model<IUser>("User", userSchema);
|
|
47
|
+
|
|
48
|
+
export default User;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "mongoose",
|
|
3
|
+
"name": "Mongoose (MongoDB)",
|
|
4
|
+
"description": "MongoDB database integration for Next.js using Mongoose ODM",
|
|
5
|
+
"type": "database",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"mongoose": "^9.9.5",
|
|
8
|
+
"mongodb": "^7.6.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {},
|
|
11
|
+
"scripts": {},
|
|
12
|
+
"envVars": {
|
|
13
|
+
"MONGODB_URI": "mongodb://localhost:27017/myapp"
|
|
14
|
+
},
|
|
15
|
+
"templateFiles": []
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
// Reuse one connection across hot reloads and concurrent requests
|
|
4
|
+
const globalForMongoose = globalThis as unknown as {
|
|
5
|
+
mongooseConnection?: Promise<typeof mongoose>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Connects Mongoose once and returns the shared connection.
|
|
10
|
+
* Call this in server code before querying Mongoose models.
|
|
11
|
+
*/
|
|
12
|
+
export async function connectDB(): Promise<typeof mongoose> {
|
|
13
|
+
const mongoUri = process.env.MONGODB_URI;
|
|
14
|
+
|
|
15
|
+
if (!mongoUri) {
|
|
16
|
+
throw new Error("MONGODB_URI environment variable is not defined");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
globalForMongoose.mongooseConnection ??= mongoose
|
|
20
|
+
.connect(mongoUri)
|
|
21
|
+
.catch((error: unknown) => {
|
|
22
|
+
// Let the next call retry instead of caching the failure
|
|
23
|
+
globalForMongoose.mongooseConnection = undefined;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return globalForMongoose.mongooseConnection;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gracefully close the MongoDB connection
|
|
32
|
+
*/
|
|
33
|
+
export async function disconnectDB(): Promise<void> {
|
|
34
|
+
await mongoose.connection.close();
|
|
35
|
+
globalForMongoose.mongooseConnection = undefined;
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import mongoose, { type Model, Schema } from "mongoose";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User document interface
|
|
5
|
+
*/
|
|
6
|
+
export interface IUser {
|
|
7
|
+
email: string;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
password: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* User schema definition
|
|
16
|
+
*/
|
|
17
|
+
const userSchema = new Schema<IUser>(
|
|
18
|
+
{
|
|
19
|
+
email: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: true,
|
|
22
|
+
unique: true,
|
|
23
|
+
lowercase: true,
|
|
24
|
+
trim: true,
|
|
25
|
+
},
|
|
26
|
+
name: {
|
|
27
|
+
type: String,
|
|
28
|
+
trim: true,
|
|
29
|
+
},
|
|
30
|
+
password: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: true,
|
|
33
|
+
minlength: 6,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
timestamps: true,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* User model
|
|
43
|
+
*/
|
|
44
|
+
const User =
|
|
45
|
+
(mongoose.models.User as Model<IUser> | undefined) ??
|
|
46
|
+
mongoose.model<IUser>("User", userSchema);
|
|
47
|
+
|
|
48
|
+
export default User;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "mongoose",
|
|
3
|
+
"name": "Mongoose (MongoDB)",
|
|
4
|
+
"description": "MongoDB database integration for TanStack Start using Mongoose ODM",
|
|
5
|
+
"type": "database",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"mongoose": "^9.9.5",
|
|
8
|
+
"mongodb": "^7.6.0",
|
|
9
|
+
"dotenv": "^17.4.2"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {},
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"envVars": {
|
|
14
|
+
"MONGODB_URI": "mongodb://localhost:27017/myapp"
|
|
15
|
+
},
|
|
16
|
+
"templateFiles": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
|
|
4
|
+
// Reuse one connection across hot reloads and concurrent requests
|
|
5
|
+
const globalForMongoose = globalThis as unknown as {
|
|
6
|
+
mongooseConnection?: Promise<typeof mongoose>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Connects Mongoose once and returns the shared connection.
|
|
11
|
+
* Call this in server code before querying Mongoose models.
|
|
12
|
+
*/
|
|
13
|
+
export async function connectDB(): Promise<typeof mongoose> {
|
|
14
|
+
const mongoUri = process.env.MONGODB_URI;
|
|
15
|
+
|
|
16
|
+
if (!mongoUri) {
|
|
17
|
+
throw new Error("MONGODB_URI environment variable is not defined");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
globalForMongoose.mongooseConnection ??= mongoose
|
|
21
|
+
.connect(mongoUri)
|
|
22
|
+
.catch((error: unknown) => {
|
|
23
|
+
// Let the next call retry instead of caching the failure
|
|
24
|
+
globalForMongoose.mongooseConnection = undefined;
|
|
25
|
+
throw error;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return globalForMongoose.mongooseConnection;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gracefully close the MongoDB connection
|
|
33
|
+
*/
|
|
34
|
+
export async function disconnectDB(): Promise<void> {
|
|
35
|
+
await mongoose.connection.close();
|
|
36
|
+
globalForMongoose.mongooseConnection = undefined;
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import mongoose, { type Model, Schema } from "mongoose";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User document interface
|
|
5
|
+
*/
|
|
6
|
+
export interface IUser {
|
|
7
|
+
email: string;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
password: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* User schema definition
|
|
16
|
+
*/
|
|
17
|
+
const userSchema = new Schema<IUser>(
|
|
18
|
+
{
|
|
19
|
+
email: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: true,
|
|
22
|
+
unique: true,
|
|
23
|
+
lowercase: true,
|
|
24
|
+
trim: true,
|
|
25
|
+
},
|
|
26
|
+
name: {
|
|
27
|
+
type: String,
|
|
28
|
+
trim: true,
|
|
29
|
+
},
|
|
30
|
+
password: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: true,
|
|
33
|
+
minlength: 6,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
timestamps: true,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* User model
|
|
43
|
+
*/
|
|
44
|
+
const User =
|
|
45
|
+
(mongoose.models.User as Model<IUser> | undefined) ??
|
|
46
|
+
mongoose.model<IUser>("User", userSchema);
|
|
47
|
+
|
|
48
|
+
export default User;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import mongoose from "mongoose";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MongoDB connection configuration
|
|
5
|
-
*/
|
|
6
|
-
const connectDB = async (): Promise<void> => {
|
|
7
|
-
try {
|
|
8
|
-
const mongoUri = process.env.MONGODB_URI;
|
|
9
|
-
|
|
10
|
-
if (!mongoUri) {
|
|
11
|
-
throw new Error("MONGODB_URI environment variable is not defined");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
await mongoose.connect(mongoUri);
|
|
15
|
-
|
|
16
|
-
console.log("MongoDB connected successfully");
|
|
17
|
-
|
|
18
|
-
mongoose.connection.on("error", (error) => {
|
|
19
|
-
console.error("MongoDB connection error:", error);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
mongoose.connection.on("disconnected", () => {
|
|
23
|
-
console.log("MongoDB disconnected");
|
|
24
|
-
});
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.error("Failed to connect to MongoDB:", error);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Gracefully close MongoDB connection
|
|
33
|
-
*/
|
|
34
|
-
const disconnectDB = async (): Promise<void> => {
|
|
35
|
-
try {
|
|
36
|
-
await mongoose.connection.close();
|
|
37
|
-
console.log("MongoDB connection closed");
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error("Error closing MongoDB connection:", error);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export { connectDB, disconnectDB };
|
|
File without changes
|