create-backlist 5.0.6 → 6.0.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/package.json +1 -1
- package/src/generators/java.js +136 -54
- package/src/generators/python.js +54 -25
- package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +25 -0
- package/src/templates/java-spring/partials/AuthController.java.ejs +49 -0
- package/src/templates/java-spring/partials/Controller.java.ejs +28 -49
- package/src/templates/java-spring/partials/Dockerfile.ejs +11 -0
- package/src/templates/java-spring/partials/Entity.java.ejs +7 -16
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +43 -0
- package/src/templates/java-spring/partials/JwtService.java.ejs +30 -0
- package/src/templates/java-spring/partials/Repository.java.ejs +2 -5
- package/src/templates/java-spring/partials/SecurityConfig.java.ejs +44 -0
- package/src/templates/java-spring/partials/Service.java.ejs +31 -0
- package/src/templates/java-spring/partials/User.java.ejs +20 -0
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +27 -0
- package/src/templates/java-spring/partials/UserRepository.java.ejs +12 -0
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +27 -0
- package/src/templates/node-ts-express/partials/routes.ts.ejs +45 -51
- package/src/templates/python-fastapi/Dockerfile.ejs +8 -0
- package/src/templates/python-fastapi/app/core/config.py.ejs +8 -0
- package/src/templates/python-fastapi/app/core/security.py.ejs +8 -0
- package/src/templates/python-fastapi/app/db.py.ejs +7 -0
- package/src/templates/python-fastapi/app/main.py.ejs +24 -0
- package/src/templates/python-fastapi/app/models/user.py.ejs +9 -0
- package/src/templates/python-fastapi/app/routers/auth.py.ejs +33 -0
- package/src/templates/python-fastapi/app/routers/model_routes.py.ejs +72 -0
- package/src/templates/python-fastapi/app/schemas/user.py.ejs +16 -0
- package/src/templates/python-fastapi/docker-compose.yml.ejs +19 -0
- package/src/templates/python-fastapi/requirements.txt.ejs +5 -1
package/package.json
CHANGED
package/src/generators/java.js
CHANGED
|
@@ -2,95 +2,177 @@ const chalk = require('chalk');
|
|
|
2
2
|
const { execa } = require('execa');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const axios = require('axios');
|
|
6
|
-
const unzipper = require('unzipper');
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const unzipper = require('unzipper');
|
|
7
7
|
const { analyzeFrontend } = require('../analyzer');
|
|
8
8
|
const { renderAndWrite, getTemplatePath } = require('./template');
|
|
9
9
|
|
|
10
|
+
function sanitizeArtifactId(name) {
|
|
11
|
+
// Lowercase, keep letters, numbers and dashes; replace others with dashes
|
|
12
|
+
return String(name || 'backend').toLowerCase().replace(/[^a-z0-9\-]/g, '-').replace(/-+/g, '-');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
|
|
16
|
+
const params = new URLSearchParams({
|
|
17
|
+
type: 'maven-project',
|
|
18
|
+
language: 'java',
|
|
19
|
+
groupId,
|
|
20
|
+
artifactId,
|
|
21
|
+
name,
|
|
22
|
+
packageName: `${groupId}.${artifactId.replace(/-/g, '')}`, // com.example.myapp
|
|
23
|
+
dependencies: dependencies.join(','),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (bootVersion) params.set('bootVersion', bootVersion);
|
|
27
|
+
|
|
28
|
+
const url = `https://start.spring.io/starter.zip?${params.toString()}`;
|
|
29
|
+
|
|
30
|
+
const res = await axios.get(url, {
|
|
31
|
+
responseType: 'stream',
|
|
32
|
+
headers: { Accept: 'application/zip' }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return res;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function extractZipStream(stream, dest) {
|
|
39
|
+
await new Promise((resolve, reject) => {
|
|
40
|
+
const out = stream.pipe(unzipper.Extract({ path: dest }));
|
|
41
|
+
out.on('close', resolve);
|
|
42
|
+
out.on('finish', resolve);
|
|
43
|
+
out.on('error', reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
10
47
|
async function generateJavaProject(options) {
|
|
11
48
|
const { projectDir, projectName, frontendSrcDir } = options;
|
|
12
|
-
const
|
|
49
|
+
const groupId = 'com.backlist.generated';
|
|
50
|
+
const artifactId = sanitizeArtifactId(projectName || 'backend');
|
|
51
|
+
const name = projectName || 'backend';
|
|
13
52
|
|
|
14
53
|
try {
|
|
15
|
-
// --- Step 1: Download Base Project from Spring Initializr ---
|
|
16
54
|
console.log(chalk.blue(' -> Contacting Spring Initializr to download a base Spring Boot project...'));
|
|
17
|
-
|
|
18
|
-
// Define standard dependencies for a web API
|
|
19
|
-
const dependencies = 'web,data-jpa,lombok,postgresql'; // Using PostgreSQL as an example DB driver
|
|
20
|
-
const springInitializrUrl = `https://start.spring.io/starter.zip?type=maven-project&language=java&bootVersion=3.2.0&groupId=${group}&artifactId=${projectName}&name=${projectName}&dependencies=${dependencies}`;
|
|
21
|
-
|
|
22
|
-
const response = await axios({
|
|
23
|
-
url: springInitializrUrl,
|
|
24
|
-
method: 'GET',
|
|
25
|
-
responseType: 'stream'
|
|
26
|
-
});
|
|
27
55
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
// Primary attempt: current stable Boot version. If this fails, we’ll retry without bootVersion.
|
|
57
|
+
const deps = ['web', 'data-jpa', 'lombok', 'postgresql']; // valid Initializr ids
|
|
58
|
+
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await downloadInitializrZip({
|
|
62
|
+
groupId,
|
|
63
|
+
artifactId,
|
|
64
|
+
name,
|
|
65
|
+
bootVersion: '3.3.4', // current stable; adjust as needed
|
|
66
|
+
dependencies: deps
|
|
35
67
|
});
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// Fallback – remove bootVersion and also try smaller dependency set if needed
|
|
70
|
+
const fallbackDeps = ['web', 'data-jpa', 'lombok'];
|
|
71
|
+
try {
|
|
72
|
+
console.log(chalk.yellow(' -> Initial attempt failed. Retrying with default Boot version...'));
|
|
73
|
+
response = await downloadInitializrZip({
|
|
74
|
+
groupId,
|
|
75
|
+
artifactId,
|
|
76
|
+
name,
|
|
77
|
+
bootVersion: '', // let Initializr pick latest
|
|
78
|
+
dependencies: deps
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
console.log(chalk.yellow(' -> Second attempt failed. Retrying with minimal dependencies...'));
|
|
82
|
+
response = await downloadInitializrZip({
|
|
83
|
+
groupId,
|
|
84
|
+
artifactId,
|
|
85
|
+
name,
|
|
86
|
+
bootVersion: '',
|
|
87
|
+
dependencies: fallbackDeps
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
38
91
|
|
|
39
|
-
|
|
92
|
+
console.log(chalk.blue(' -> Unzipping the Spring Boot project...'));
|
|
93
|
+
await extractZipStream(response.data, projectDir);
|
|
94
|
+
|
|
95
|
+
// Analyze frontend and plan entities/controllers
|
|
40
96
|
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
41
97
|
const modelsToGenerate = new Map();
|
|
42
|
-
endpoints.forEach(ep => {
|
|
43
|
-
if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
44
|
-
modelsToGenerate.set(ep.controllerName, {
|
|
98
|
+
(Array.isArray(endpoints) ? endpoints : []).forEach(ep => {
|
|
99
|
+
if (ep && ep.schemaFields && ep.controllerName && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
100
|
+
modelsToGenerate.set(ep.controllerName, {
|
|
101
|
+
name: ep.controllerName,
|
|
102
|
+
fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type }))
|
|
103
|
+
});
|
|
45
104
|
}
|
|
46
105
|
});
|
|
47
106
|
|
|
48
|
-
//
|
|
107
|
+
// Generate Entities/Repositories/Controllers (basic)
|
|
49
108
|
if (modelsToGenerate.size > 0) {
|
|
50
|
-
console.log(chalk.blue(' -> Generating Java entities and controllers...'));
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
109
|
+
console.log(chalk.blue(' -> Generating Java entities, repositories, and controllers...'));
|
|
110
|
+
|
|
111
|
+
// Spring Initializr zips project as <artifactId> root folder; if you extracted to projectDir,
|
|
112
|
+
// files are already in the right place. Compute Java src path:
|
|
113
|
+
const javaSrcRoot = path.join(projectDir, 'src', 'main', 'java', ...groupId.split('.'), artifactId.replace(/-/g, ''));
|
|
114
|
+
const entityDir = path.join(javaSrcRoot, 'model');
|
|
115
|
+
const repoDir = path.join(javaSrcRoot, 'repository');
|
|
116
|
+
const controllerDir = path.join(javaSrcRoot, 'controller');
|
|
117
|
+
|
|
55
118
|
await fs.ensureDir(entityDir);
|
|
119
|
+
await fs.ensureDir(repoDir);
|
|
56
120
|
await fs.ensureDir(controllerDir);
|
|
57
121
|
|
|
58
122
|
for (const [modelName, modelData] of modelsToGenerate.entries()) {
|
|
59
123
|
await renderAndWrite(
|
|
60
124
|
getTemplatePath('java-spring/partials/Entity.java.ejs'),
|
|
61
125
|
path.join(entityDir, `${modelName}.java`),
|
|
62
|
-
{ group, projectName, modelName, model: modelData }
|
|
126
|
+
{ group: groupId, projectName: artifactId.replace(/-/g, ''), modelName, model: modelData }
|
|
127
|
+
);
|
|
128
|
+
await renderAndWrite(
|
|
129
|
+
getTemplatePath('java-spring/partials/Repository.java.ejs'),
|
|
130
|
+
path.join(repoDir, `${modelName}Repository.java`),
|
|
131
|
+
{ group: groupId, projectName: artifactId.replace(/-/g, ''), modelName }
|
|
63
132
|
);
|
|
64
133
|
await renderAndWrite(
|
|
65
134
|
getTemplatePath('java-spring/partials/Controller.java.ejs'),
|
|
66
135
|
path.join(controllerDir, `${modelName}Controller.java`),
|
|
67
|
-
{ group, projectName, controllerName: modelName }
|
|
136
|
+
{ group: groupId, projectName: artifactId.replace(/-/g, ''), controllerName: modelName, model: modelData }
|
|
68
137
|
);
|
|
69
138
|
}
|
|
70
139
|
}
|
|
71
140
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
console.log(chalk.yellow('\nTo run your new Java backend:'));
|
|
88
|
-
console.log(chalk.cyan(' 1. Open the project in a Java IDE (like IntelliJ IDEA or VS Code).'));
|
|
89
|
-
console.log(chalk.cyan(' 2. Run the main application file.'));
|
|
141
|
+
// Append DB config (PostgreSQL) to application.properties (non-fatal if fails)
|
|
142
|
+
try {
|
|
143
|
+
const propsPath = path.join(projectDir, 'src', 'main', 'resources', 'application.properties');
|
|
144
|
+
const dbProps = [
|
|
145
|
+
`\n\n# --- Auto-generated by create-backlist ---`,
|
|
146
|
+
`spring.datasource.url=jdbc:postgresql://localhost:5432/${artifactId}`,
|
|
147
|
+
`spring.datasource.username=postgres`,
|
|
148
|
+
`spring.datasource.password=password`,
|
|
149
|
+
`spring.jpa.hibernate.ddl-auto=update`,
|
|
150
|
+
`spring.jpa.show-sql=true`,
|
|
151
|
+
].join('\n');
|
|
152
|
+
await fs.appendFile(propsPath, dbProps);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.log(chalk.yellow(' -> Could not update application.properties (continuing).'));
|
|
155
|
+
}
|
|
90
156
|
|
|
157
|
+
console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
|
|
158
|
+
console.log(chalk.yellow('\nNext steps:'));
|
|
159
|
+
console.log(chalk.cyan(` cd ${path.basename(projectDir)}`));
|
|
160
|
+
console.log(chalk.cyan(' ./mvnw spring-boot:run # or use your IDE to run Application class'));
|
|
91
161
|
|
|
92
162
|
} catch (error) {
|
|
93
|
-
if (error.response) {
|
|
163
|
+
if (error.response && error.response.status) {
|
|
164
|
+
console.error(chalk.red(` -> Initializr error status: ${error.response.status}`));
|
|
165
|
+
if (error.response.data) {
|
|
166
|
+
try {
|
|
167
|
+
// Try read error text body for hints
|
|
168
|
+
const text = (await (async () => {
|
|
169
|
+
let buf = '';
|
|
170
|
+
for await (const chunk of error.response.data) buf += chunk.toString();
|
|
171
|
+
return buf;
|
|
172
|
+
})());
|
|
173
|
+
console.error(chalk.yellow(' -> Initializr response body:'), text);
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
94
176
|
throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
|
|
95
177
|
}
|
|
96
178
|
throw error;
|
package/src/generators/python.js
CHANGED
|
@@ -19,52 +19,81 @@ async function generatePythonProject(options) {
|
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
22
|
+
// Add a default User model if none was detected but auth might be added later
|
|
23
|
+
if (!modelsToGenerate.has('User')) {
|
|
24
|
+
modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String'}] });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Step 2: Scaffold Base Python Project Directories ---
|
|
28
|
+
console.log(chalk.blue(' -> Scaffolding Python (FastAPI) project structure...'));
|
|
24
29
|
const appDir = path.join(projectDir, 'app');
|
|
25
|
-
const
|
|
30
|
+
const coreDir = path.join(appDir, 'core');
|
|
31
|
+
const dbDir = path.join(appDir, 'db'); // For DB connection
|
|
32
|
+
const modelsDir = path.join(appDir, 'models');
|
|
33
|
+
const schemasDir = path.join(appDir, 'schemas');
|
|
34
|
+
const routesDir = path.join(appDir, 'routers');
|
|
35
|
+
|
|
26
36
|
await fs.ensureDir(appDir);
|
|
37
|
+
await fs.ensureDir(coreDir);
|
|
38
|
+
await fs.ensureDir(dbDir);
|
|
39
|
+
await fs.ensureDir(modelsDir);
|
|
40
|
+
await fs.ensureDir(schemasDir);
|
|
27
41
|
await fs.ensureDir(routesDir);
|
|
28
42
|
|
|
29
|
-
// --- Step 3: Generate Files from Templates ---
|
|
43
|
+
// --- Step 3: Generate All Python Files from Templates ---
|
|
30
44
|
const controllers = Array.from(modelsToGenerate.keys());
|
|
31
45
|
|
|
32
|
-
// Generate main
|
|
46
|
+
// Generate main application file
|
|
33
47
|
await renderAndWrite(getTemplatePath('python-fastapi/main.py.ejs'), path.join(projectDir, 'app', 'main.py'), { projectName, controllers });
|
|
34
|
-
|
|
35
|
-
// Generate requirements.txt
|
|
48
|
+
// Generate dependency file
|
|
36
49
|
await renderAndWrite(getTemplatePath('python-fastapi/requirements.txt.ejs'), path.join(projectDir, 'requirements.txt'), {});
|
|
37
50
|
|
|
38
|
-
// Generate
|
|
51
|
+
// Generate core files (config, security)
|
|
52
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/core/config.py.ejs'), path.join(coreDir, 'config.py'), { projectName });
|
|
53
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/core/security.py.ejs'), path.join(coreDir, 'security.py'), {});
|
|
54
|
+
|
|
55
|
+
// Generate DB connection and base model
|
|
56
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/db.py.ejs'), path.join(appDir, 'db.py'), {});
|
|
57
|
+
|
|
58
|
+
// Generate model and schema files for User (for auth)
|
|
59
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/models/user.py.ejs'), path.join(modelsDir, 'user.py'), {});
|
|
60
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/schemas/user.py.ejs'), path.join(schemasDir, 'user.py'), {});
|
|
61
|
+
|
|
62
|
+
// Generate router for auth
|
|
63
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/routers/auth.py.ejs'), path.join(routesDir, 'auth.py'), {});
|
|
64
|
+
|
|
65
|
+
// Generate router for each detected model
|
|
39
66
|
for (const [modelName, modelData] of modelsToGenerate.entries()) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
67
|
+
if(modelName.toLowerCase() !== 'user') { // User model is handled separately
|
|
68
|
+
// In a full implementation, you'd have generic model/schema templates too
|
|
69
|
+
}
|
|
70
|
+
await renderAndWrite(getTemplatePath('python-fastapi/app/routers/model_routes.py.ejs'), path.join(routesDir, `${modelName.toLowerCase()}_routes.py`), { modelName, schema: modelData });
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
// --- Step 4: Setup Virtual Environment and Install Dependencies ---
|
|
48
74
|
console.log(chalk.magenta(' -> Setting up virtual environment and installing dependencies...'));
|
|
49
|
-
// Create a virtual environment
|
|
50
75
|
await execa('python', ['-m', 'venv', 'venv'], { cwd: projectDir });
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
const pipPath = process.platform === 'win32'
|
|
54
|
-
? path.join('venv', 'Scripts', 'pip')
|
|
55
|
-
: path.join('venv', 'bin', 'pip');
|
|
56
|
-
|
|
57
|
-
// Install dependencies using the virtual environment's pip
|
|
77
|
+
const pipPath = process.platform === 'win32' ? path.join('venv', 'Scripts', 'pip') : path.join('venv', 'bin', 'pip');
|
|
58
78
|
await execa(path.join(projectDir, pipPath), ['install', '-r', 'requirements.txt'], { cwd: projectDir });
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
// --- Step 5: Generate Docker and .env files ---
|
|
81
|
+
await renderAndWrite(getTemplatePath('python-fastapi/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), {});
|
|
82
|
+
await renderAndWrite(getTemplatePath('python-fastapi/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName });
|
|
83
|
+
|
|
84
|
+
const envContent = `DATABASE_URL="postgresql://postgres:password@db:5432/${projectName}"\nJWT_SECRET="a_very_secret_key_change_this"`;
|
|
85
|
+
await fs.writeFile(path.join(projectDir, '.env'), envContent);
|
|
86
|
+
await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
|
|
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'));
|
|
64
94
|
|
|
65
95
|
|
|
66
96
|
} catch (error) {
|
|
67
|
-
// Improve error message for command not found
|
|
68
97
|
if (error.code === 'ENOENT') {
|
|
69
98
|
throw new Error(`'${error.command}' command not found. Please ensure Python and venv are installed and in your system's PATH.`);
|
|
70
99
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>;
|
|
3
|
+
|
|
4
|
+
import org.springframework.boot.CommandLineRunner;
|
|
5
|
+
import org.springframework.context.annotation.Bean;
|
|
6
|
+
import org.springframework.context.annotation.Configuration;
|
|
7
|
+
import <%= group %>.<%= projectName %>.model.User;
|
|
8
|
+
import <%= group %>.<%= projectName %>.repository.UserRepository;
|
|
9
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
10
|
+
|
|
11
|
+
@Configuration
|
|
12
|
+
public class ApplicationSeeder {
|
|
13
|
+
@Bean
|
|
14
|
+
CommandLineRunner seed(UserRepository userRepository, PasswordEncoder encoder) {
|
|
15
|
+
return args -> {
|
|
16
|
+
if (userRepository.findByEmail("admin@example.com").isEmpty()) {
|
|
17
|
+
User admin = new User();
|
|
18
|
+
admin.setName("Admin");
|
|
19
|
+
admin.setEmail("admin@example.com");
|
|
20
|
+
admin.setPassword(encoder.encode("admin123"));
|
|
21
|
+
userRepository.save(admin);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.controller;
|
|
3
|
+
|
|
4
|
+
import <%= group %>.<%= projectName %>.model.User;
|
|
5
|
+
import <%= group %>.<%= projectName %>.repository.UserRepository;
|
|
6
|
+
import <%= group %>.<%= projectName %>.security.JwtService;
|
|
7
|
+
import org.springframework.http.ResponseEntity;
|
|
8
|
+
import org.springframework.http.HttpStatus;
|
|
9
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
10
|
+
import org.springframework.web.bind.annotation.*;
|
|
11
|
+
|
|
12
|
+
import java.util.Optional;
|
|
13
|
+
|
|
14
|
+
@RestController
|
|
15
|
+
@RequestMapping("/api/auth")
|
|
16
|
+
@CrossOrigin(origins = "*")
|
|
17
|
+
public class AuthController {
|
|
18
|
+
|
|
19
|
+
private final UserRepository repo;
|
|
20
|
+
private final PasswordEncoder encoder;
|
|
21
|
+
private final JwtService jwt;
|
|
22
|
+
|
|
23
|
+
public AuthController(UserRepository repo, PasswordEncoder encoder, JwtService jwt) {
|
|
24
|
+
this.repo = repo; this.encoder = encoder; this.jwt = jwt;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@PostMapping("/register")
|
|
28
|
+
public ResponseEntity<?> register(@RequestBody User req) {
|
|
29
|
+
if (repo.findByEmail(req.getEmail()).isPresent()) {
|
|
30
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("User already exists");
|
|
31
|
+
}
|
|
32
|
+
req.setPassword(encoder.encode(req.getPassword()));
|
|
33
|
+
repo.save(req);
|
|
34
|
+
String token = jwt.generateToken(req.getEmail());
|
|
35
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(new TokenResponse(token));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@PostMapping("/login")
|
|
39
|
+
public ResponseEntity<?> login(@RequestBody User req) {
|
|
40
|
+
Optional<User> current = repo.findByEmail(req.getEmail());
|
|
41
|
+
if (current.isEmpty()) return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid credentials");
|
|
42
|
+
if (!encoder.matches(req.getPassword(), current.get().getPassword()))
|
|
43
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid credentials");
|
|
44
|
+
String token = jwt.generateToken(req.getEmail());
|
|
45
|
+
return ResponseEntity.ok(new TokenResponse(token));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public record TokenResponse(String token) {}
|
|
49
|
+
}
|
|
@@ -1,61 +1,40 @@
|
|
|
1
1
|
// Auto-generated by create-backlist
|
|
2
2
|
package <%= group %>.<%= projectName %>.controller;
|
|
3
3
|
|
|
4
|
-
import <%= group %>.<%= projectName %>.model.<%= controllerName %>;
|
|
5
|
-
import <%= group %>.<%= projectName %>.repository.<%= controllerName %>Repository;
|
|
6
|
-
import org.springframework.beans.factory.annotation.Autowired;
|
|
7
|
-
import org.springframework.http.HttpStatus;
|
|
8
4
|
import org.springframework.http.ResponseEntity;
|
|
5
|
+
import org.springframework.http.HttpStatus;
|
|
9
6
|
import org.springframework.web.bind.annotation.*;
|
|
10
|
-
|
|
11
7
|
import java.util.List;
|
|
12
8
|
import java.util.Optional;
|
|
9
|
+
import <%= group %>.<%= projectName %>.service.<%= controllerName %>Service;
|
|
10
|
+
import <%= group %>.<%= projectName %>.model.<%= controllerName %>;
|
|
13
11
|
|
|
14
12
|
@RestController
|
|
15
|
-
@CrossOrigin(origins = "*") // Allow all origins for development
|
|
16
13
|
@RequestMapping("/api/<%= controllerName.toLowerCase() %>s")
|
|
14
|
+
@CrossOrigin(origins = "*")
|
|
17
15
|
public class <%= controllerName %>Controller {
|
|
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
|
-
.map(item -> {
|
|
43
|
-
// Manually map fields to update. For simplicity, we assume all fields are updatable.
|
|
44
|
-
<% model.fields.forEach(field => { %>
|
|
45
|
-
item.set<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>(updatedItem.get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>());
|
|
46
|
-
<% }); %>
|
|
47
|
-
<%= controllerName %> savedItem = repository.save(item);
|
|
48
|
-
return ResponseEntity.ok(savedItem);
|
|
49
|
-
})
|
|
50
|
-
.orElseGet(() -> ResponseEntity.notFound().build());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@DeleteMapping("/{id}")
|
|
54
|
-
public ResponseEntity<Void> delete<%= controllerName %>(@PathVariable Long id) {
|
|
55
|
-
if (!repository.existsById(id)) {
|
|
56
|
-
return ResponseEntity.notFound().build();
|
|
57
|
-
}
|
|
58
|
-
repository.deleteById(id);
|
|
59
|
-
return ResponseEntity.noContent().build();
|
|
60
|
-
}
|
|
16
|
+
private final <%= controllerName %>Service service;
|
|
17
|
+
public <%= controllerName %>Controller(<%= controllerName %>Service service) { this.service = service; }
|
|
18
|
+
|
|
19
|
+
@GetMapping public List<<%= controllerName %>> all() { return service.findAll(); }
|
|
20
|
+
|
|
21
|
+
@GetMapping("/{id}")
|
|
22
|
+
public ResponseEntity<<%= controllerName %>> one(@PathVariable Long id) {
|
|
23
|
+
return service.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@PostMapping
|
|
27
|
+
public ResponseEntity<<%= controllerName %>> create(@RequestBody <%= controllerName %> m) {
|
|
28
|
+
return new ResponseEntity<>(service.create(m), HttpStatus.CREATED);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@PutMapping("/{id}")
|
|
32
|
+
public ResponseEntity<<%= controllerName %>> update(@PathVariable Long id, @RequestBody <%= controllerName %> m) {
|
|
33
|
+
return service.update(id, m).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@DeleteMapping("/{id}")
|
|
37
|
+
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
|
38
|
+
return service.delete(id) ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
|
|
39
|
+
}
|
|
61
40
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Auto-generated by create-backlist
|
|
2
|
+
FROM eclipse-temurin:21-jdk AS build
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN ./mvnw -q -DskipTests package
|
|
6
|
+
|
|
7
|
+
FROM eclipse-temurin:21-jre
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
COPY --from=build /app/target/*.jar app.jar
|
|
10
|
+
EXPOSE 8080
|
|
11
|
+
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
// Auto-generated by create-backlist
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
2
|
package <%= group %>.<%= projectName %>.model;
|
|
3
3
|
|
|
4
|
-
import jakarta.persistence
|
|
5
|
-
import jakarta.persistence.Id;
|
|
6
|
-
import jakarta.persistence.GeneratedValue;
|
|
7
|
-
import jakarta.persistence.GenerationType;
|
|
4
|
+
import jakarta.persistence.*;
|
|
8
5
|
import lombok.Data;
|
|
9
6
|
|
|
10
7
|
@Data
|
|
11
8
|
@Entity
|
|
12
9
|
public class <%= modelName %> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<% model.fields.forEach(field => { %>
|
|
19
|
-
<% let javaType = 'String'; %>
|
|
20
|
-
<% if (field.type === 'Number') javaType = 'Integer'; %>
|
|
21
|
-
<% if (field.type === 'Boolean') javaType = 'boolean'; %>
|
|
22
|
-
private <%= javaType %> <%= field.name %>;
|
|
23
|
-
<% }); %>
|
|
10
|
+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
11
|
+
private Long id;
|
|
12
|
+
<% model.fields.forEach(f => { %>
|
|
13
|
+
private <%= f.type === 'Number' ? 'Integer' : (f.type === 'Boolean' ? 'Boolean' : 'String') %> <%= f.name %>;
|
|
14
|
+
<% }) %>
|
|
24
15
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.security;
|
|
3
|
+
|
|
4
|
+
import jakarta.servlet.FilterChain;
|
|
5
|
+
import jakarta.servlet.ServletException;
|
|
6
|
+
import jakarta.servlet.http.HttpServletRequest;
|
|
7
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
8
|
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
9
|
+
import org.springframework.security.core.userdetails.UserDetails;
|
|
10
|
+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
11
|
+
import org.springframework.security.core.context.SecurityContextHolder;
|
|
12
|
+
import org.springframework.stereotype.Component;
|
|
13
|
+
import org.springframework.web.filter.OncePerRequestFilter;
|
|
14
|
+
|
|
15
|
+
import java.io.IOException;
|
|
16
|
+
|
|
17
|
+
@Component
|
|
18
|
+
public class JwtAuthFilter extends OncePerRequestFilter {
|
|
19
|
+
|
|
20
|
+
private final JwtService jwtService;
|
|
21
|
+
private final UserDetailsService uds;
|
|
22
|
+
|
|
23
|
+
public JwtAuthFilter(JwtService jwtService, UserDetailsService uds) {
|
|
24
|
+
this.jwtService = jwtService;
|
|
25
|
+
this.uds = uds;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Override
|
|
29
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
|
30
|
+
throws ServletException, IOException {
|
|
31
|
+
String header = request.getHeader("Authorization");
|
|
32
|
+
if (header != null && header.startsWith("Bearer ")) {
|
|
33
|
+
String token = header.substring(7);
|
|
34
|
+
try {
|
|
35
|
+
String subject = jwtService.validateAndGetSubject(token);
|
|
36
|
+
UserDetails ud = uds.loadUserByUsername(subject);
|
|
37
|
+
var auth = new UsernamePasswordAuthenticationToken(ud, null, ud.getAuthorities());
|
|
38
|
+
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
39
|
+
} catch (Exception ignored) {}
|
|
40
|
+
}
|
|
41
|
+
chain.doFilter(request, response);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.security;
|
|
3
|
+
|
|
4
|
+
import io.jsonwebtoken.*;
|
|
5
|
+
import io.jsonwebtoken.security.Keys;
|
|
6
|
+
import org.springframework.stereotype.Service;
|
|
7
|
+
|
|
8
|
+
import java.security.Key;
|
|
9
|
+
import java.util.Date;
|
|
10
|
+
|
|
11
|
+
@Service
|
|
12
|
+
public class JwtService {
|
|
13
|
+
private final Key key = Keys.hmacShaKeyFor(System.getenv("JWT_SECRET") != null
|
|
14
|
+
? System.getenv("JWT_SECRET").getBytes()
|
|
15
|
+
: "change_this_dev_secret_change_this_dev_secret".getBytes());
|
|
16
|
+
|
|
17
|
+
public String generateToken(String userId) {
|
|
18
|
+
return Jwts.builder()
|
|
19
|
+
.setSubject(userId)
|
|
20
|
+
.setIssuedAt(new Date())
|
|
21
|
+
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 5))
|
|
22
|
+
.signWith(key, SignatureAlgorithm.HS256)
|
|
23
|
+
.compact();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public String validateAndGetSubject(String token) {
|
|
27
|
+
return Jwts.parserBuilder().setSigningKey(key).build()
|
|
28
|
+
.parseClaimsJws(token).getBody().getSubject();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
// Auto-generated by create-backlist
|
|
2
2
|
package <%= group %>.<%= projectName %>.repository;
|
|
3
3
|
|
|
4
|
-
import <%= group %>.<%= projectName %>.model.<%= modelName %>;
|
|
5
4
|
import org.springframework.data.jpa.repository.JpaRepository;
|
|
6
5
|
import org.springframework.stereotype.Repository;
|
|
6
|
+
import <%= group %>.<%= projectName %>.model.<%= modelName %>;
|
|
7
7
|
|
|
8
8
|
@Repository
|
|
9
|
-
public interface <%= modelName %>Repository extends JpaRepository<<%= modelName %>, Long> {
|
|
10
|
-
// Spring Data JPA automatically provides CRUD methods like findAll(), findById(), save(), deleteById()
|
|
11
|
-
// You can add custom query methods here if needed.
|
|
12
|
-
}
|
|
9
|
+
public interface <%= modelName %>Repository extends JpaRepository<<%= modelName %>, Long> {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.config;
|
|
3
|
+
|
|
4
|
+
import <%= group %>.<%= projectName %>.security.JwtAuthFilter;
|
|
5
|
+
import org.springframework.context.annotation.Bean;
|
|
6
|
+
import org.springframework.context.annotation.Configuration;
|
|
7
|
+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
8
|
+
import org.springframework.security.config.Customizer;
|
|
9
|
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
10
|
+
import org.springframework.security.web.SecurityFilterChain;
|
|
11
|
+
import org.springframework.security.authentication.AuthenticationManager;
|
|
12
|
+
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
|
13
|
+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
|
14
|
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
15
|
+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
16
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
17
|
+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
18
|
+
|
|
19
|
+
@Configuration
|
|
20
|
+
@EnableWebSecurity
|
|
21
|
+
public class SecurityConfig {
|
|
22
|
+
|
|
23
|
+
private final JwtAuthFilter jwtAuthFilter;
|
|
24
|
+
private final UserDetailsService userDetailsService;
|
|
25
|
+
|
|
26
|
+
public SecurityConfig(JwtAuthFilter jwtAuthFilter, UserDetailsService uds) {
|
|
27
|
+
this.jwtAuthFilter = jwtAuthFilter;
|
|
28
|
+
this.userDetailsService = uds;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Bean
|
|
32
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
33
|
+
http.csrf(csrf -> csrf.disable())
|
|
34
|
+
.authorizeHttpRequests(auth -> auth
|
|
35
|
+
.requestMatchers("/api/auth/**", "/actuator/**").permitAll()
|
|
36
|
+
.anyRequest().authenticated()
|
|
37
|
+
)
|
|
38
|
+
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
|
39
|
+
.httpBasic(Customizer.withDefaults());
|
|
40
|
+
return http.build();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.service;
|
|
3
|
+
|
|
4
|
+
import org.springframework.stereotype.Service;
|
|
5
|
+
import java.util.List;
|
|
6
|
+
import java.util.Optional;
|
|
7
|
+
import <%= group %>.<%= projectName %>.repository.<%= modelName %>Repository;
|
|
8
|
+
import <%= group %>.<%= projectName %>.model.<%= modelName %>;
|
|
9
|
+
|
|
10
|
+
@Service
|
|
11
|
+
public class <%= modelName %>Service {
|
|
12
|
+
private final <%= modelName %>Repository repo;
|
|
13
|
+
public <%= modelName %>Service(<%= modelName %>Repository repo) { this.repo = repo; }
|
|
14
|
+
|
|
15
|
+
public List<<%= modelName %>> findAll() { return repo.findAll(); }
|
|
16
|
+
public Optional<<%= modelName %>> findById(Long id) { return repo.findById(id); }
|
|
17
|
+
public <%= modelName %> create(<%= modelName %> m) { return repo.save(m); }
|
|
18
|
+
public Optional<<%= modelName %>> update(Long id, <%= modelName %> m) {
|
|
19
|
+
return repo.findById(id).map(existing -> {
|
|
20
|
+
<% model.fields.forEach(f => { %>
|
|
21
|
+
existing.set<%= f.name.charAt(0).toUpperCase() + f.name.slice(1) %>(m.get<%= f.name.charAt(0).toUpperCase() + f.name.slice(1) %>());
|
|
22
|
+
<% }) %>
|
|
23
|
+
return repo.save(existing);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
public boolean delete(Long id) {
|
|
27
|
+
if (!repo.existsById(id)) return false;
|
|
28
|
+
repo.deleteById(id);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.model;
|
|
3
|
+
|
|
4
|
+
import jakarta.persistence.*;
|
|
5
|
+
import lombok.Data;
|
|
6
|
+
|
|
7
|
+
@Data
|
|
8
|
+
@Entity
|
|
9
|
+
@Table(name="users")
|
|
10
|
+
public class User {
|
|
11
|
+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
12
|
+
private Long id;
|
|
13
|
+
|
|
14
|
+
private String name;
|
|
15
|
+
|
|
16
|
+
@Column(unique = true)
|
|
17
|
+
private String email;
|
|
18
|
+
|
|
19
|
+
private String password;
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.security;
|
|
3
|
+
|
|
4
|
+
import <%= group %>.<%= projectName %>.model.User;
|
|
5
|
+
import <%= group %>.<%= projectName %>.repository.UserRepository;
|
|
6
|
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
7
|
+
import org.springframework.security.core.userdetails.UserDetails;
|
|
8
|
+
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
9
|
+
import org.springframework.stereotype.Service;
|
|
10
|
+
import org.springframework.security.core.userdetails.User.UserBuilder;
|
|
11
|
+
import org.springframework.security.core.userdetails.User.*;
|
|
12
|
+
|
|
13
|
+
@Service
|
|
14
|
+
public class UserDetailsServiceImpl implements UserDetailsService {
|
|
15
|
+
|
|
16
|
+
private final UserRepository repo;
|
|
17
|
+
|
|
18
|
+
public UserDetailsServiceImpl(UserRepository repo) {
|
|
19
|
+
this.repo = repo;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Override
|
|
23
|
+
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
|
|
24
|
+
User u = repo.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("Not found"));
|
|
25
|
+
return withUsername(u.getEmail()).password(u.getPassword()).authorities("USER").build();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Auto-generated by create-backlist
|
|
2
|
+
package <%= group %>.<%= projectName %>.repository;
|
|
3
|
+
|
|
4
|
+
import java.util.Optional;
|
|
5
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
6
|
+
import org.springframework.stereotype.Repository;
|
|
7
|
+
import <%= group %>.<%= projectName %>.model.User;
|
|
8
|
+
|
|
9
|
+
@Repository
|
|
10
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
11
|
+
Optional<User> findByEmail(String email);
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
db:
|
|
4
|
+
image: postgres:16-alpine
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: ${DB_USER:-postgres}
|
|
7
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
|
|
8
|
+
POSTGRES_DB: ${DB_NAME:-<%= projectName %>}
|
|
9
|
+
ports:
|
|
10
|
+
- "5432:5432"
|
|
11
|
+
volumes:
|
|
12
|
+
- pgdata:/var/lib/postgresql/data
|
|
13
|
+
|
|
14
|
+
app:
|
|
15
|
+
build: .
|
|
16
|
+
depends_on:
|
|
17
|
+
- db
|
|
18
|
+
environment:
|
|
19
|
+
- JWT_SECRET=${JWT_SECRET:-change_me_long_secret}
|
|
20
|
+
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/${DB_NAME:-<%= projectName %>}
|
|
21
|
+
- SPRING_DATASOURCE_USERNAME=${DB_USER:-postgres}
|
|
22
|
+
- SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD:-password}
|
|
23
|
+
ports:
|
|
24
|
+
- "8080:8080"
|
|
25
|
+
|
|
26
|
+
volumes:
|
|
27
|
+
pgdata:
|
|
@@ -1,64 +1,58 @@
|
|
|
1
|
-
// Auto-generated by create-backlist
|
|
1
|
+
// Auto-generated by create-backlist on <%= new Date().toISOString() %>
|
|
2
2
|
import { Router, Request, Response } from 'express';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
<%
|
|
4
|
+
// Build unique controller list safely
|
|
5
|
+
const controllers = [];
|
|
6
|
+
if (Array.isArray(endpoints)) {
|
|
7
|
+
endpoints.forEach((ep) => {
|
|
8
|
+
if (ep && ep.controllerName && ep.controllerName !== 'Default' && !controllers.includes(ep.controllerName)) {
|
|
9
|
+
controllers.push(ep.controllerName);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
%>
|
|
14
|
+
<% controllers.forEach((ctrl) => { %>
|
|
15
|
+
import * as <%= ctrl %>Controller from './controllers/<%= ctrl %>.controller';
|
|
16
|
+
<% }) %>
|
|
10
17
|
|
|
11
|
-
<%# Import the protect middleware only if authentication is enabled %>
|
|
12
18
|
<% if (addAuth) { %>
|
|
13
|
-
import { protect } from '
|
|
19
|
+
import { protect } from './middleware/Auth.middleware';
|
|
14
20
|
<% } %>
|
|
15
21
|
|
|
16
22
|
const router = Router();
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
<%
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let handlerFunction;
|
|
24
|
+
// If no endpoints detected, emit a basic route so file is valid
|
|
25
|
+
<% if (!Array.isArray(endpoints) || endpoints.length === 0) { %>
|
|
26
|
+
router.get('/health', (_req: Request, res: Response) => {
|
|
27
|
+
res.status(200).json({ ok: true, message: 'Auto-generated routes alive' });
|
|
28
|
+
});
|
|
29
|
+
<% } %>
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
<%
|
|
32
|
+
if (Array.isArray(endpoints)) {
|
|
33
|
+
endpoints.forEach((ep) => {
|
|
34
|
+
const rawPath = (ep && ep.path) ? ep.path : '/';
|
|
35
|
+
const expressPath = (rawPath.replace(/^\/api/, '') || '/').replace(/{(\w+)}/g, ':$1');
|
|
36
|
+
const method = ((ep && ep.method) ? ep.method : 'GET').toLowerCase();
|
|
37
|
+
const ctrl = (ep && ep.controllerName) ? ep.controllerName : 'Default';
|
|
38
|
+
const hasId = expressPath.includes(':');
|
|
39
|
+
let handler = '';
|
|
40
|
+
|
|
41
|
+
if (ctrl !== 'Default') {
|
|
42
|
+
if (method === 'post' && !hasId) handler = `${ctrl}Controller.create${ctrl}`;
|
|
43
|
+
else if (method === 'get' && !hasId) handler = `${ctrl}Controller.getAll${ctrl}s`;
|
|
44
|
+
else if (method === 'get' && hasId) handler = `${ctrl}Controller.get${ctrl}ById`;
|
|
45
|
+
else if (method === 'put' && hasId) handler = `${ctrl}Controller.update${ctrl}ById`;
|
|
46
|
+
else if (method === 'delete' && hasId) handler = `${ctrl}Controller.delete${ctrl}ById`;
|
|
38
47
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// If no specific CRUD function matches, create a placeholder handler.
|
|
42
|
-
if (!handlerFunction) {
|
|
43
|
-
handlerFunction = `(req: Request, res: Response) => {
|
|
44
|
-
res.status(501).json({ message: 'Handler not implemented for <%= endpoint.method %> <%= expressPath %>' });
|
|
45
|
-
}`;
|
|
46
|
-
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const needsProtect = !!addAuth && (method === 'post' || method === 'put' || method === 'delete');
|
|
50
|
+
const middleware = needsProtect ? 'protect, ' : '';
|
|
51
|
+
%>
|
|
52
|
+
router.<%= method %>('<%- expressPath || "/" %>', <%- middleware %><%- handler || '(req: Request, res: Response) => res.status(501).json({ message: "Not Implemented" })' %>);
|
|
53
|
+
<%
|
|
54
|
+
});
|
|
55
|
+
}
|
|
54
56
|
%>
|
|
55
|
-
/**
|
|
56
|
-
* Route for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
|
|
57
|
-
* Mapped to: <%- handlerFunction.includes('=>') ? 'Inline Handler' : handlerFunction %>
|
|
58
|
-
* Protected: <%= middleware ? 'Yes' : 'No' %>
|
|
59
|
-
*/
|
|
60
|
-
router.<%= endpoint.method.toLowerCase() %>('<%- expressPath %>', <%- middleware %><%- handlerFunction %>);
|
|
61
|
-
|
|
62
|
-
<% }); %>
|
|
63
57
|
|
|
64
58
|
export default router;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from jose import jwt
|
|
3
|
+
from app.core.config import settings
|
|
4
|
+
|
|
5
|
+
def create_token(sub: str):
|
|
6
|
+
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
|
|
7
|
+
payload = {"sub": sub, "exp": expire}
|
|
8
|
+
return jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from sqlalchemy import create_engine
|
|
2
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
3
|
+
from app.core.config import settings
|
|
4
|
+
|
|
5
|
+
engine = create_engine(settings.DB_URL)
|
|
6
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
7
|
+
Base = declarative_base()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
+
from app.db import Base, engine
|
|
4
|
+
from app.models.user import User
|
|
5
|
+
from app.routers import auth as auth_routes
|
|
6
|
+
<% (controllers || []).forEach(c => { %>from app.routers import <%= c.toLowerCase() %>_routes
|
|
7
|
+
<% }) %>
|
|
8
|
+
|
|
9
|
+
app = FastAPI(title="<%= projectName %>")
|
|
10
|
+
|
|
11
|
+
app.add_middleware(
|
|
12
|
+
CORSMiddleware,
|
|
13
|
+
allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
Base.metadata.create_all(bind=engine)
|
|
17
|
+
|
|
18
|
+
@app.get("/")
|
|
19
|
+
def root():
|
|
20
|
+
return {"ok": True, "message": "FastAPI backend generated by create-backlist"}
|
|
21
|
+
|
|
22
|
+
app.include_router(auth_routes.router)
|
|
23
|
+
<% (controllers || []).forEach(c => { %>app.include_router(<%= c.toLowerCase() %>_routes.router)
|
|
24
|
+
<% }) %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String
|
|
2
|
+
from app.db import Base
|
|
3
|
+
|
|
4
|
+
class User(Base):
|
|
5
|
+
__tablename__ = "users"
|
|
6
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
7
|
+
name = Column(String)
|
|
8
|
+
email = Column(String, unique=True, index=True)
|
|
9
|
+
password = Column(String)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
from passlib.hash import bcrypt
|
|
4
|
+
from app.db import SessionLocal
|
|
5
|
+
from app.models.user import User
|
|
6
|
+
from app.schemas.user import UserCreate, Token
|
|
7
|
+
from app.core.security import create_token
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/api/auth", tags=["Auth"])
|
|
10
|
+
|
|
11
|
+
def get_db():
|
|
12
|
+
db = SessionLocal()
|
|
13
|
+
try:
|
|
14
|
+
yield db
|
|
15
|
+
finally:
|
|
16
|
+
db.close()
|
|
17
|
+
|
|
18
|
+
@router.post("/register", response_model=Token, status_code=status.HTTP_201_CREATED)
|
|
19
|
+
def register(payload: UserCreate, db: Session = Depends(get_db)):
|
|
20
|
+
if db.query(User).filter(User.email == payload.email).first():
|
|
21
|
+
raise HTTPException(status_code=400, detail="User already exists")
|
|
22
|
+
u = User(name=payload.name, email=payload.email, password=bcrypt.hash(payload.password))
|
|
23
|
+
db.add(u)
|
|
24
|
+
db.commit()
|
|
25
|
+
db.refresh(u)
|
|
26
|
+
return {"token": create_token(u.email)}
|
|
27
|
+
|
|
28
|
+
@router.post("/login", response_model=Token)
|
|
29
|
+
def login(payload: UserCreate, db: Session = Depends(get_db)):
|
|
30
|
+
u = db.query(User).filter(User.email == payload.email).first()
|
|
31
|
+
if not u or not bcrypt.verify(payload.password, u.password):
|
|
32
|
+
raise HTTPException(status_code=400, detail="Invalid credentials")
|
|
33
|
+
return {"token": create_token(u.email)}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
2
|
+
from typing import List
|
|
3
|
+
from sqlalchemy.orm import Session
|
|
4
|
+
from app.db import SessionLocal
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
# Build schema from fields
|
|
8
|
+
<% if (schema && schema.fields) { %>
|
|
9
|
+
class <%= modelName %>Base(BaseModel):
|
|
10
|
+
<% schema.fields.forEach(f => { %>
|
|
11
|
+
<%= f.name %>: <%= f.type === 'Number' ? 'int' : 'str' %>
|
|
12
|
+
<% }) %>
|
|
13
|
+
|
|
14
|
+
class <%= modelName %>Create(<%= modelName %>Base):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class <%= modelName %>(<%= modelName %>Base):
|
|
18
|
+
id: int
|
|
19
|
+
class Config:
|
|
20
|
+
orm_mode = True
|
|
21
|
+
<% } %>
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/api/<%= modelName.toLowerCase() %>s", tags=["<%= modelName %>s"])
|
|
24
|
+
|
|
25
|
+
def get_db():
|
|
26
|
+
db = SessionLocal()
|
|
27
|
+
try:
|
|
28
|
+
yield db
|
|
29
|
+
finally:
|
|
30
|
+
db.close()
|
|
31
|
+
|
|
32
|
+
# NOTE: For brevity this uses a generic in-memory table replacement.
|
|
33
|
+
# In a full implementation you'd generate SQLAlchemy model files per entity.
|
|
34
|
+
|
|
35
|
+
_db = []
|
|
36
|
+
_counter = 0
|
|
37
|
+
|
|
38
|
+
@router.get("/", response_model=List[<%= modelName %>])
|
|
39
|
+
def list_items():
|
|
40
|
+
return _db
|
|
41
|
+
|
|
42
|
+
@router.post("/", response_model=<%= modelName %>, status_code=status.HTTP_201_CREATED)
|
|
43
|
+
def create_item(payload: <%= modelName %>Create):
|
|
44
|
+
global _counter
|
|
45
|
+
_counter += 1
|
|
46
|
+
item = {"id": _counter, **payload.dict()}
|
|
47
|
+
_db.append(item)
|
|
48
|
+
return item
|
|
49
|
+
|
|
50
|
+
@router.get("/{item_id}", response_model=<%= modelName %>)
|
|
51
|
+
def get_item(item_id: int):
|
|
52
|
+
for it in _db:
|
|
53
|
+
if it["id"] == item_id:
|
|
54
|
+
return it
|
|
55
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
56
|
+
|
|
57
|
+
@router.put("/{item_id}", response_model=<%= modelName %>)
|
|
58
|
+
def update_item(item_id: int, payload: <%= modelName %>Create):
|
|
59
|
+
for idx, it in enumerate(_db):
|
|
60
|
+
if it["id"] == item_id:
|
|
61
|
+
new_it = {"id": item_id, **payload.dict()}
|
|
62
|
+
_db[idx] = new_it
|
|
63
|
+
return new_it
|
|
64
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
65
|
+
|
|
66
|
+
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
67
|
+
def delete_item(item_id: int):
|
|
68
|
+
for idx, it in enumerate(_db):
|
|
69
|
+
if it["id"] == item_id:
|
|
70
|
+
_db.pop(idx)
|
|
71
|
+
return
|
|
72
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
class UserBase(BaseModel):
|
|
4
|
+
name: str
|
|
5
|
+
email: str
|
|
6
|
+
|
|
7
|
+
class UserCreate(UserBase):
|
|
8
|
+
password: str
|
|
9
|
+
|
|
10
|
+
class UserOut(UserBase):
|
|
11
|
+
id: int
|
|
12
|
+
class Config:
|
|
13
|
+
orm_mode = True
|
|
14
|
+
|
|
15
|
+
class Token(BaseModel):
|
|
16
|
+
token: str
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
db:
|
|
4
|
+
image: postgres:16-alpine
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: ${DB_USER:-postgres}
|
|
7
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
|
|
8
|
+
POSTGRES_DB: ${DB_NAME:-<%= projectName %>}
|
|
9
|
+
ports: ["5432:5432"]
|
|
10
|
+
volumes: [pgdata:/var/lib/postgresql/data]
|
|
11
|
+
api:
|
|
12
|
+
build: .
|
|
13
|
+
environment:
|
|
14
|
+
- DB_URL=postgresql://postgres:password@db:5432/<%= projectName %>
|
|
15
|
+
- JWT_SECRET=change_me_super_secret
|
|
16
|
+
ports: ["8000:8000"]
|
|
17
|
+
depends_on: [db]
|
|
18
|
+
volumes:
|
|
19
|
+
pgdata:
|