create-backlist 7.0.1 → 7.3.1
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 +1 -10
- package/bin/index.js +242 -275
- package/package.json +3 -2
- package/src/ai-agent.js +171 -171
- package/src/analyzer.js +750 -495
- package/src/env-resolver.js +70 -0
- package/src/generators/dotnet.js +134 -133
- package/src/generators/java.js +248 -233
- package/src/generators/js.js +346 -0
- package/src/generators/nestjs.js +278 -0
- package/src/generators/node.js +404 -404
- package/src/generators/python.js +86 -104
- package/src/generators/template.js +22 -22
- package/src/project-detector.js +131 -0
- package/src/templates/dotnet/partials/Dockerfile.ejs +27 -0
- package/src/templates/dotnet/partials/docker-compose.yml.ejs +33 -0
- package/src/templates/java-spring/partials/Controller.java.ejs +3 -3
- package/src/templates/js-express/base/server.js +59 -0
- package/src/templates/js-express/partials/Dockerfile.ejs +12 -0
- package/src/templates/js-express/partials/auth.controller.js.ejs +66 -0
- package/src/templates/js-express/partials/auth.middleware.js.ejs +19 -0
- package/src/templates/js-express/partials/auth.routes.js.ejs +9 -0
- package/src/templates/js-express/partials/controller.js.ejs +53 -0
- package/src/templates/js-express/partials/db.js.ejs +19 -0
- package/src/templates/js-express/partials/docker-compose.yml.ejs +46 -0
- package/src/templates/js-express/partials/model.js.ejs +18 -0
- package/src/templates/js-express/partials/package.json.ejs +17 -0
- package/src/templates/js-express/partials/prisma.schema.ejs +21 -0
- package/src/templates/js-express/partials/routes.js.ejs +19 -0
- package/src/templates/js-express/partials/seeder.js.ejs +103 -0
- package/src/templates/js-express/partials/service.js.ejs +51 -0
- package/src/templates/js-express/partials/swagger.js.ejs +30 -0
- package/src/templates/js-express/partials/test.js.ejs +46 -0
- package/src/templates/nestjs/base/app.module.ts +9 -0
- package/src/templates/nestjs/base/main.ts +23 -0
- package/src/templates/nestjs/base/tsconfig.json +21 -0
- package/src/templates/nestjs/partials/auth.controller.ts.ejs +17 -0
- package/src/templates/nestjs/partials/auth.module.ts.ejs +17 -0
- package/src/templates/nestjs/partials/auth.service.ts.ejs +70 -0
- package/src/templates/nestjs/partials/controller.ts.ejs +34 -0
- package/src/templates/nestjs/partials/create-dto.ts.ejs +22 -0
- package/src/templates/nestjs/partials/jwt-guard.ts.ejs +24 -0
- package/src/templates/nestjs/partials/module.ts.ejs +10 -0
- package/src/templates/nestjs/partials/package.json.ejs +27 -0
- package/src/templates/nestjs/partials/prisma.service.ts.ejs +13 -0
- package/src/templates/nestjs/partials/schema.ts.ejs +19 -0
- package/src/templates/nestjs/partials/service.ts.ejs +67 -0
- package/src/templates/nestjs/partials/update-dto.ts.ejs +4 -0
- package/src/templates/node-ts-express/partials/HexController.ts.ejs +56 -56
- package/src/templates/node-ts-express/partials/HexRepository.ts.ejs +26 -26
- package/src/templates/node-ts-express/partials/HexService.ts.ejs +27 -27
- package/src/utils.js +11 -11
- /package/src/templates/{node-ts-express → dotnet}/partials/DbContext.cs.ejs +0 -0
- /package/src/templates/{node-ts-express → dotnet}/partials/Model.cs.ejs +0 -0
package/src/generators/python.js
CHANGED
|
@@ -1,104 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
async function generatePythonProject(options) {
|
|
9
|
-
const { projectDir, projectName, frontendSrcDir } = options;
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await fs.ensureDir(
|
|
37
|
-
await fs.ensureDir(
|
|
38
|
-
await fs.ensureDir(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await renderAndWrite(getTemplatePath('python-fastapi/app/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
console.log(chalk.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.log(chalk.green(' -> Python (FastAPI) backend generation is complete!'));
|
|
90
|
-
console.log(chalk.yellow('\nTo run your new Python backend with Docker:'));
|
|
91
|
-
console.log(chalk.cyan(' 1. Make sure Docker Desktop is running.'));
|
|
92
|
-
console.log(chalk.cyan(' 2. Run: `docker-compose up --build`'));
|
|
93
|
-
console.log(chalk.cyan(' 3. API will be available at http://localhost:8000 and docs at http://localhost:8000/docs'));
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} catch (error) {
|
|
97
|
-
if (error.code === 'ENOENT') {
|
|
98
|
-
throw new Error(`'${error.command}' command not found. Please ensure Python and venv are installed and in your system's PATH.`);
|
|
99
|
-
}
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = { generatePythonProject };
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { analyzeFrontend } from '../analyzer.js';
|
|
6
|
+
import { renderAndWrite, getTemplatePath } from './template.js';
|
|
7
|
+
|
|
8
|
+
export async function generatePythonProject(options) {
|
|
9
|
+
const { projectDir, projectName, frontendSrcDir } = options;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
console.log(chalk.blue(' -> Analyzing frontend for Python (FastAPI) backend...'));
|
|
13
|
+
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
14
|
+
const modelsToGenerate = new Map();
|
|
15
|
+
endpoints.forEach(ep => {
|
|
16
|
+
if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
17
|
+
modelsToGenerate.set(ep.controllerName, { name: ep.controllerName, fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type })) });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!modelsToGenerate.has('User')) {
|
|
22
|
+
modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String' }] });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(chalk.blue(' -> Scaffolding Python (FastAPI) project structure...'));
|
|
26
|
+
const appDir = path.join(projectDir, 'app');
|
|
27
|
+
const coreDir = path.join(appDir, 'core');
|
|
28
|
+
const dbDir = path.join(appDir, 'db');
|
|
29
|
+
const modelsDir = path.join(appDir, 'models');
|
|
30
|
+
const schemasDir = path.join(appDir, 'schemas');
|
|
31
|
+
const routesDir = path.join(appDir, 'routers');
|
|
32
|
+
|
|
33
|
+
await fs.ensureDir(appDir);
|
|
34
|
+
await fs.ensureDir(coreDir);
|
|
35
|
+
await fs.ensureDir(dbDir);
|
|
36
|
+
await fs.ensureDir(modelsDir);
|
|
37
|
+
await fs.ensureDir(schemasDir);
|
|
38
|
+
await fs.ensureDir(routesDir);
|
|
39
|
+
|
|
40
|
+
const controllers = Array.from(modelsToGenerate.keys());
|
|
41
|
+
|
|
42
|
+
await renderAndWrite(getTemplatePath('python-fastapi/main.py.ejs'), path.join(projectDir, 'app', 'main.py'), { projectName, controllers });
|
|
43
|
+
await renderAndWrite(getTemplatePath('python-fastapi/requirements.txt.ejs'), path.join(projectDir, 'requirements.txt'), {});
|
|
44
|
+
|
|
45
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/core/config.py.ejs'), path.join(coreDir, 'config.py'), { projectName });
|
|
46
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/core/security.py.ejs'), path.join(coreDir, 'security.py'), {});
|
|
47
|
+
|
|
48
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/db.py.ejs'), path.join(appDir, 'db.py'), {});
|
|
49
|
+
|
|
50
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/models/user.py.ejs'), path.join(modelsDir, 'user.py'), {});
|
|
51
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/schemas/user.py.ejs'), path.join(schemasDir, 'user.py'), {});
|
|
52
|
+
|
|
53
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/routers/auth.py.ejs'), path.join(routesDir, 'auth.py'), {});
|
|
54
|
+
|
|
55
|
+
for (const [modelName, modelData] of modelsToGenerate.entries()) {
|
|
56
|
+
if (modelName.toLowerCase() !== 'user') {
|
|
57
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/routers/model_routes.py.ejs'), path.join(routesDir, `${modelName.toLowerCase()}_routes.py`), { modelName, schema: modelData });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.magenta(' -> Setting up virtual environment and installing dependencies...'));
|
|
62
|
+
await execa('python', ['-m', 'venv', 'venv'], { cwd: projectDir });
|
|
63
|
+
|
|
64
|
+
const pipPath = process.platform === 'win32' ? path.join('venv', 'Scripts', 'pip') : path.join('venv', 'bin', 'pip');
|
|
65
|
+
await execa(path.join(projectDir, pipPath), ['install', '-r', 'requirements.txt'], { cwd: projectDir });
|
|
66
|
+
|
|
67
|
+
await renderAndWrite(getTemplatePath('python-fastapi/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), {});
|
|
68
|
+
await renderAndWrite(getTemplatePath('python-fastapi/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName });
|
|
69
|
+
|
|
70
|
+
const envContent = `DATABASE_URL="postgresql://postgres:password@db:5432/${projectName}"\nJWT_SECRET="a_very_secret_key_change_this"`;
|
|
71
|
+
await fs.writeFile(path.join(projectDir, '.env'), envContent);
|
|
72
|
+
await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
|
|
73
|
+
|
|
74
|
+
console.log(chalk.green(' -> Python (FastAPI) backend generation is complete!'));
|
|
75
|
+
console.log(chalk.yellow('\nTo run your new Python backend with Docker:'));
|
|
76
|
+
console.log(chalk.cyan(' 1. Make sure Docker Desktop is running.'));
|
|
77
|
+
console.log(chalk.cyan(' 2. Run: `docker-compose up --build`'));
|
|
78
|
+
console.log(chalk.cyan(' 3. API will be available at http://localhost:8000 and docs at http://localhost:8000/docs'));
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.code === 'ENOENT') {
|
|
82
|
+
throw new Error(`'${error.command}' command not found. Please ensure Python and venv are installed and in your system's PATH.`);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import ejs from 'ejs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
export async function renderAndWrite(templatePath, outPath, data) {
|
|
10
|
-
try {
|
|
11
|
-
const tpl = await fs.readFile(templatePath, 'utf-8');
|
|
12
|
-
const code = ejs.render(tpl, data || {}, { filename: templatePath }); // filename helps with EJS errors
|
|
13
|
-
await fs.outputFile(outPath, code);
|
|
14
|
-
} catch (err) {
|
|
15
|
-
console.error('EJS render failed for:', templatePath);
|
|
16
|
-
console.error('Data keys:', Object.keys(data || {}));
|
|
17
|
-
throw err;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getTemplatePath(subpath) {
|
|
22
|
-
return path.join(__dirname, '..', 'templates', subpath);
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import ejs from 'ejs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export async function renderAndWrite(templatePath, outPath, data) {
|
|
10
|
+
try {
|
|
11
|
+
const tpl = await fs.readFile(templatePath, 'utf-8');
|
|
12
|
+
const code = ejs.render(tpl, data || {}, { filename: templatePath }); // filename helps with EJS errors
|
|
13
|
+
await fs.outputFile(outPath, code);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.error('EJS render failed for:', templatePath);
|
|
16
|
+
console.error('Data keys:', Object.keys(data || {}));
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getTemplatePath(subpath) {
|
|
22
|
+
return path.join(__dirname, '..', 'templates', subpath);
|
|
23
23
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const NEXTJS_SCAN_DIRS = [
|
|
5
|
+
"app",
|
|
6
|
+
"pages",
|
|
7
|
+
"src",
|
|
8
|
+
"components",
|
|
9
|
+
"lib",
|
|
10
|
+
"hooks",
|
|
11
|
+
"services",
|
|
12
|
+
"utils",
|
|
13
|
+
"features",
|
|
14
|
+
"modules",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const REACT_SCAN_DIRS = ["src"];
|
|
18
|
+
|
|
19
|
+
export async function detectProjectType(projectRoot) {
|
|
20
|
+
const result = {
|
|
21
|
+
type: "unknown",
|
|
22
|
+
scanDirs: [],
|
|
23
|
+
apiRouteDirs: [],
|
|
24
|
+
framework: "unknown",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
28
|
+
let pkg = null;
|
|
29
|
+
if (await fs.pathExists(pkgPath)) {
|
|
30
|
+
try {
|
|
31
|
+
pkg = await fs.readJson(pkgPath);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const deps = pkg
|
|
36
|
+
? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
|
37
|
+
: {};
|
|
38
|
+
|
|
39
|
+
const hasNext = !!deps.next || (await hasNextConfig(projectRoot));
|
|
40
|
+
const hasReact = !!deps.react;
|
|
41
|
+
const hasVite = !!deps.vite;
|
|
42
|
+
|
|
43
|
+
const hasAppDir = await fs.pathExists(path.join(projectRoot, "app"));
|
|
44
|
+
const hasPagesDir = await fs.pathExists(path.join(projectRoot, "pages"));
|
|
45
|
+
const hasSrcApp = await fs.pathExists(path.join(projectRoot, "src", "app"));
|
|
46
|
+
const hasSrcPages = await fs.pathExists(
|
|
47
|
+
path.join(projectRoot, "src", "pages")
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (hasNext) {
|
|
51
|
+
result.framework = "next";
|
|
52
|
+
|
|
53
|
+
if ((hasAppDir || hasSrcApp) && (hasPagesDir || hasSrcPages)) {
|
|
54
|
+
result.type = "nextjs-hybrid";
|
|
55
|
+
} else if (hasAppDir || hasSrcApp) {
|
|
56
|
+
result.type = "nextjs-app";
|
|
57
|
+
} else if (hasPagesDir || hasSrcPages) {
|
|
58
|
+
result.type = "nextjs-pages";
|
|
59
|
+
} else {
|
|
60
|
+
result.type = "nextjs-app";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const candidateDirs = [...NEXTJS_SCAN_DIRS];
|
|
64
|
+
for (const dir of candidateDirs) {
|
|
65
|
+
const abs = path.join(projectRoot, dir);
|
|
66
|
+
if (await fs.pathExists(abs)) {
|
|
67
|
+
result.scanDirs.push(abs);
|
|
68
|
+
}
|
|
69
|
+
const srcNested = path.join(projectRoot, "src", dir);
|
|
70
|
+
if (dir !== "src" && (await fs.pathExists(srcNested))) {
|
|
71
|
+
result.scanDirs.push(srcNested);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (hasAppDir) {
|
|
76
|
+
const appApiDir = path.join(projectRoot, "app", "api");
|
|
77
|
+
if (await fs.pathExists(appApiDir)) result.apiRouteDirs.push(appApiDir);
|
|
78
|
+
result.apiRouteDirs.push(path.join(projectRoot, "app"));
|
|
79
|
+
}
|
|
80
|
+
if (hasSrcApp) {
|
|
81
|
+
const srcAppApiDir = path.join(projectRoot, "src", "app", "api");
|
|
82
|
+
if (await fs.pathExists(srcAppApiDir))
|
|
83
|
+
result.apiRouteDirs.push(srcAppApiDir);
|
|
84
|
+
result.apiRouteDirs.push(path.join(projectRoot, "src", "app"));
|
|
85
|
+
}
|
|
86
|
+
if (hasPagesDir) {
|
|
87
|
+
const pagesApiDir = path.join(projectRoot, "pages", "api");
|
|
88
|
+
if (await fs.pathExists(pagesApiDir))
|
|
89
|
+
result.apiRouteDirs.push(pagesApiDir);
|
|
90
|
+
}
|
|
91
|
+
if (hasSrcPages) {
|
|
92
|
+
const srcPagesApiDir = path.join(projectRoot, "src", "pages", "api");
|
|
93
|
+
if (await fs.pathExists(srcPagesApiDir))
|
|
94
|
+
result.apiRouteDirs.push(srcPagesApiDir);
|
|
95
|
+
}
|
|
96
|
+
} else if (hasReact) {
|
|
97
|
+
result.framework = hasVite ? "vite" : "react";
|
|
98
|
+
result.type = hasVite ? "vite-react" : "react";
|
|
99
|
+
|
|
100
|
+
for (const dir of REACT_SCAN_DIRS) {
|
|
101
|
+
const abs = path.join(projectRoot, dir);
|
|
102
|
+
if (await fs.pathExists(abs)) {
|
|
103
|
+
result.scanDirs.push(abs);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result.scanDirs.length === 0) {
|
|
109
|
+
const fallback = path.join(projectRoot, "src");
|
|
110
|
+
if (await fs.pathExists(fallback)) {
|
|
111
|
+
result.scanDirs.push(fallback);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
result.scanDirs = [...new Set(result.scanDirs)];
|
|
116
|
+
result.apiRouteDirs = [...new Set(result.apiRouteDirs)];
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function hasNextConfig(projectRoot) {
|
|
122
|
+
const configs = [
|
|
123
|
+
"next.config.js",
|
|
124
|
+
"next.config.mjs",
|
|
125
|
+
"next.config.ts",
|
|
126
|
+
];
|
|
127
|
+
for (const cfg of configs) {
|
|
128
|
+
if (await fs.pathExists(path.join(projectRoot, cfg))) return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Auto-generated by create-backlist (.NET Core)
|
|
2
|
+
|
|
3
|
+
# ---- Build Stage ----
|
|
4
|
+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
|
5
|
+
WORKDIR /src
|
|
6
|
+
|
|
7
|
+
COPY *.csproj .
|
|
8
|
+
RUN dotnet restore
|
|
9
|
+
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN dotnet publish -c Release -o /app/publish --no-restore
|
|
12
|
+
|
|
13
|
+
# ---- Runtime Stage ----
|
|
14
|
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
|
15
|
+
WORKDIR /app
|
|
16
|
+
|
|
17
|
+
ENV ASPNETCORE_URLS=http://+:5000
|
|
18
|
+
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
19
|
+
|
|
20
|
+
COPY --from=build /app/publish .
|
|
21
|
+
|
|
22
|
+
EXPOSE 5000
|
|
23
|
+
|
|
24
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
25
|
+
CMD curl -f http://localhost:5000/health || exit 1
|
|
26
|
+
|
|
27
|
+
ENTRYPOINT ["dotnet", "<%= projectName %>.dll"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Auto-generated by create-backlist (.NET Core)
|
|
2
|
+
version: '3.8'
|
|
3
|
+
|
|
4
|
+
services:
|
|
5
|
+
app:
|
|
6
|
+
build: .
|
|
7
|
+
ports:
|
|
8
|
+
- "5000:5000"
|
|
9
|
+
environment:
|
|
10
|
+
- ASPNETCORE_ENVIRONMENT=Development
|
|
11
|
+
- ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database=<%= projectName %>;Username=postgres;Password=postgres
|
|
12
|
+
depends_on:
|
|
13
|
+
db:
|
|
14
|
+
condition: service_healthy
|
|
15
|
+
|
|
16
|
+
db:
|
|
17
|
+
image: postgres:16-alpine
|
|
18
|
+
environment:
|
|
19
|
+
POSTGRES_USER: postgres
|
|
20
|
+
POSTGRES_PASSWORD: postgres
|
|
21
|
+
POSTGRES_DB: <%= projectName %>
|
|
22
|
+
ports:
|
|
23
|
+
- "5432:5432"
|
|
24
|
+
volumes:
|
|
25
|
+
- pgdata:/var/lib/postgresql/data
|
|
26
|
+
healthcheck:
|
|
27
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
28
|
+
interval: 5s
|
|
29
|
+
timeout: 3s
|
|
30
|
+
retries: 5
|
|
31
|
+
|
|
32
|
+
volumes:
|
|
33
|
+
pgdata:
|
|
@@ -27,7 +27,7 @@ public class <%= controllerName %>Controller {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
@GetMapping("/{id}")
|
|
30
|
-
public ResponseEntity<<%= controllerName %>> one(@PathVariable
|
|
30
|
+
public ResponseEntity<<%= controllerName %>> one(@PathVariable Long id) {
|
|
31
31
|
return service.findById(id)
|
|
32
32
|
.map(ResponseEntity::ok)
|
|
33
33
|
.orElse(ResponseEntity.notFound().build());
|
|
@@ -39,14 +39,14 @@ public class <%= controllerName %>Controller {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
@PutMapping("/{id}")
|
|
42
|
-
public ResponseEntity<<%= controllerName %>> update(@PathVariable
|
|
42
|
+
public ResponseEntity<<%= controllerName %>> update(@PathVariable Long id, @RequestBody <%= controllerName %> m) {
|
|
43
43
|
return service.update(id, m)
|
|
44
44
|
.map(ResponseEntity::ok)
|
|
45
45
|
.orElse(ResponseEntity.notFound().build());
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
@DeleteMapping("/{id}")
|
|
49
|
-
public ResponseEntity<Void> delete(@PathVariable
|
|
49
|
+
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
|
50
50
|
return service.delete(id)
|
|
51
51
|
? ResponseEntity.noContent().build()
|
|
52
52
|
: ResponseEntity.notFound().build();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import helmet from 'helmet';
|
|
5
|
+
import morgan from 'morgan';
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
const app = express();
|
|
10
|
+
|
|
11
|
+
app.use(helmet());
|
|
12
|
+
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
|
13
|
+
|
|
14
|
+
app.use(cors({
|
|
15
|
+
origin: process.env.CORS_ORIGIN
|
|
16
|
+
? process.env.CORS_ORIGIN.split(',').map(s => s.trim())
|
|
17
|
+
: ['http://localhost:3000', 'http://localhost:5173'],
|
|
18
|
+
credentials: true,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
app.use(express.json({ limit: '1mb' }));
|
|
22
|
+
|
|
23
|
+
app.get('/api/health', (req, res) => {
|
|
24
|
+
res.status(200).json({ status: 'ok' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.get('/', (req, res) => {
|
|
28
|
+
res.send('Backend is running! Generated by create-backlist.');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// INJECT:ROUTES
|
|
32
|
+
|
|
33
|
+
app.use((req, res) => {
|
|
34
|
+
res.status(404).json({ message: 'Route not found' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.use((err, req, res, _next) => {
|
|
38
|
+
console.error(err);
|
|
39
|
+
res.status(err?.statusCode || 500).json({
|
|
40
|
+
message: err?.message || 'Internal Server Error',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const PORT = process.env.PORT || 8000;
|
|
45
|
+
|
|
46
|
+
const server = app.listen(PORT, () => {
|
|
47
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function shutdown(signal) {
|
|
51
|
+
console.log(`Received ${signal}. Shutting down...`);
|
|
52
|
+
server.close(() => {
|
|
53
|
+
console.log('HTTP server closed.');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
59
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<% if (dbType === 'mongoose') { %>
|
|
2
|
+
import User from '../models/User.model.js';
|
|
3
|
+
<% } else { %>
|
|
4
|
+
import { prisma } from '../db.js';
|
|
5
|
+
<% } %>
|
|
6
|
+
import bcrypt from 'bcryptjs';
|
|
7
|
+
import jwt from 'jsonwebtoken';
|
|
8
|
+
|
|
9
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'changeme';
|
|
10
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '5h';
|
|
11
|
+
|
|
12
|
+
export class AuthController {
|
|
13
|
+
|
|
14
|
+
static async register(req, res) {
|
|
15
|
+
try {
|
|
16
|
+
const { name, email, password } = req.body;
|
|
17
|
+
if (!email || !password) {
|
|
18
|
+
return res.status(400).json({ message: 'Email and password are required' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
22
|
+
|
|
23
|
+
<% if (dbType === 'mongoose') { %>
|
|
24
|
+
const existing = await User.findOne({ email });
|
|
25
|
+
if (existing) return res.status(409).json({ message: 'Email already in use' });
|
|
26
|
+
|
|
27
|
+
const user = await User.create({ name, email, password: hashedPassword });
|
|
28
|
+
const token = jwt.sign({ id: user._id, email: user.email }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
29
|
+
<% } else { %>
|
|
30
|
+
const existing = await prisma.user.findUnique({ where: { email } });
|
|
31
|
+
if (existing) return res.status(409).json({ message: 'Email already in use' });
|
|
32
|
+
|
|
33
|
+
const user = await prisma.user.create({ data: { name, email, password: hashedPassword } });
|
|
34
|
+
const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
35
|
+
<% } %>
|
|
36
|
+
|
|
37
|
+
res.status(201).json({ token, user: { id: user.id || user._id, name: user.name, email: user.email } });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
res.status(500).json({ message: error.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async login(req, res) {
|
|
44
|
+
try {
|
|
45
|
+
const { email, password } = req.body;
|
|
46
|
+
if (!email || !password) {
|
|
47
|
+
return res.status(400).json({ message: 'Email and password are required' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
<% if (dbType === 'mongoose') { %>
|
|
51
|
+
const user = await User.findOne({ email });
|
|
52
|
+
<% } else { %>
|
|
53
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
54
|
+
<% } %>
|
|
55
|
+
if (!user) return res.status(401).json({ message: 'Invalid credentials' });
|
|
56
|
+
|
|
57
|
+
const isMatch = await bcrypt.compare(password, user.password);
|
|
58
|
+
if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });
|
|
59
|
+
|
|
60
|
+
const token = jwt.sign({ id: user.id || user._id, email: user.email }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
61
|
+
res.status(200).json({ token, user: { id: user.id || user._id, name: user.name, email: user.email } });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
res.status(500).json({ message: error.message });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
|
|
3
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'changeme';
|
|
4
|
+
|
|
5
|
+
export function authMiddleware(req, res, next) {
|
|
6
|
+
const authHeader = req.headers.authorization;
|
|
7
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
8
|
+
return res.status(401).json({ message: 'Access denied. No token provided.' });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const token = authHeader.split(' ')[1];
|
|
12
|
+
try {
|
|
13
|
+
const decoded = jwt.verify(token, JWT_SECRET);
|
|
14
|
+
req.user = decoded;
|
|
15
|
+
next();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return res.status(401).json({ message: 'Invalid or expired token.' });
|
|
18
|
+
}
|
|
19
|
+
}
|