create-backlist 5.0.7 → 6.0.2

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.
Files changed (58) hide show
  1. package/bin/backlist.js +227 -0
  2. package/package.json +10 -4
  3. package/src/analyzer.js +210 -89
  4. package/src/db/prisma.ts +4 -0
  5. package/src/generators/dotnet.js +120 -94
  6. package/src/generators/java.js +205 -75
  7. package/src/generators/node.js +262 -85
  8. package/src/generators/python.js +54 -25
  9. package/src/generators/template.js +38 -2
  10. package/src/scanner/index.js +99 -0
  11. package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
  12. package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
  13. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +30 -0
  14. package/src/templates/java-spring/partials/AuthController.java.ejs +62 -0
  15. package/src/templates/java-spring/partials/Controller.java.ejs +40 -50
  16. package/src/templates/java-spring/partials/Dockerfile.ejs +16 -0
  17. package/src/templates/java-spring/partials/Entity.java.ejs +16 -15
  18. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +66 -0
  19. package/src/templates/java-spring/partials/JwtService.java.ejs +58 -0
  20. package/src/templates/java-spring/partials/Repository.java.ejs +9 -3
  21. package/src/templates/java-spring/partials/SecurityConfig.java.ejs +44 -0
  22. package/src/templates/java-spring/partials/Service.java.ejs +69 -0
  23. package/src/templates/java-spring/partials/User.java.ejs +33 -0
  24. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +33 -0
  25. package/src/templates/java-spring/partials/UserRepository.java.ejs +20 -0
  26. package/src/templates/java-spring/partials/docker-compose.yml.ejs +35 -0
  27. package/src/templates/node-ts-express/base/server.ts +12 -5
  28. package/src/templates/node-ts-express/base/tsconfig.json +13 -3
  29. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
  30. package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
  31. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
  32. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
  33. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  34. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  35. package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
  36. package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
  37. package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
  38. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
  39. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
  40. package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
  41. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
  42. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
  43. package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
  44. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
  45. package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
  46. package/src/templates/python-fastapi/Dockerfile.ejs +8 -0
  47. package/src/templates/python-fastapi/app/core/config.py.ejs +8 -0
  48. package/src/templates/python-fastapi/app/core/security.py.ejs +8 -0
  49. package/src/templates/python-fastapi/app/db.py.ejs +7 -0
  50. package/src/templates/python-fastapi/app/main.py.ejs +24 -0
  51. package/src/templates/python-fastapi/app/models/user.py.ejs +9 -0
  52. package/src/templates/python-fastapi/app/routers/auth.py.ejs +33 -0
  53. package/src/templates/python-fastapi/app/routers/model_routes.py.ejs +72 -0
  54. package/src/templates/python-fastapi/app/schemas/user.py.ejs +16 -0
  55. package/src/templates/python-fastapi/docker-compose.yml.ejs +19 -0
  56. package/src/templates/python-fastapi/requirements.txt.ejs +5 -1
  57. package/src/utils.js +19 -4
  58. package/bin/index.js +0 -141
@@ -0,0 +1,99 @@
1
+ const path = require('path');
2
+ const fg = require('fast-glob');
3
+ const { Project, SyntaxKind } = require('ts-morph');
4
+ const fs = require('fs-extra');
5
+
6
+ function normalizeMethod(name) {
7
+ const m = String(name).toUpperCase();
8
+ return ['GET','POST','PUT','PATCH','DELETE'].includes(m) ? m : null;
9
+ }
10
+
11
+ // Very first-pass extractor: axios.<method>('url') OR fetch('url', { method: 'POST' })
12
+ async function scanFrontend({ frontendSrcDir }) {
13
+ const patterns = [
14
+ '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'
15
+ ];
16
+
17
+ const files = await fg(patterns, {
18
+ cwd: frontendSrcDir,
19
+ absolute: true,
20
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**']
21
+ });
22
+
23
+ const project = new Project({
24
+ tsConfigFilePath: fs.existsSync(path.join(frontendSrcDir, '../tsconfig.json'))
25
+ ? path.join(frontendSrcDir, '../tsconfig.json')
26
+ : undefined,
27
+ skipAddingFilesFromTsConfig: true
28
+ });
29
+
30
+ files.forEach(f => project.addSourceFileAtPathIfExists(f));
31
+
32
+ const endpoints = [];
33
+
34
+ for (const sf of project.getSourceFiles()) {
35
+ // axios.get('/x') | axios.post('/x')
36
+ const callExprs = sf.getDescendantsOfKind(SyntaxKind.CallExpression);
37
+
38
+ for (const call of callExprs) {
39
+ const expr = call.getExpression();
40
+
41
+ // axios.<method>(...)
42
+ if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
43
+ const pae = expr;
44
+ const method = normalizeMethod(pae.getName());
45
+ const target = pae.getExpression().getText(); // axios / api / client etc (basic)
46
+ const args = call.getArguments();
47
+ if (method && args.length >= 1 && args[0].getKind() === SyntaxKind.StringLiteral) {
48
+ const url = args[0].getText().slice(1, -1);
49
+ endpoints.push({
50
+ source: sf.getFilePath(),
51
+ kind: 'axios',
52
+ client: target,
53
+ method,
54
+ url
55
+ });
56
+ }
57
+ }
58
+
59
+ // fetch('/x', { method: 'POST' })
60
+ if (expr.getText() === 'fetch') {
61
+ const args = call.getArguments();
62
+ if (args.length >= 1 && args[0].getKind() === SyntaxKind.StringLiteral) {
63
+ const url = args[0].getText().slice(1, -1);
64
+ let method = 'GET';
65
+ if (args[1] && args[1].getKind() === SyntaxKind.ObjectLiteralExpression) {
66
+ const obj = args[1];
67
+ const methodProp = obj.getProperty('method');
68
+ if (methodProp && methodProp.getKind() === SyntaxKind.PropertyAssignment) {
69
+ const init = methodProp.getInitializer();
70
+ if (init && init.getKind() === SyntaxKind.StringLiteral) {
71
+ method = init.getText().slice(1, -1).toUpperCase();
72
+ }
73
+ }
74
+ }
75
+ endpoints.push({
76
+ source: sf.getFilePath(),
77
+ kind: 'fetch',
78
+ method,
79
+ url
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ return {
87
+ version: 1,
88
+ generatedAt: new Date().toISOString(),
89
+ frontendSrcDir,
90
+ endpoints
91
+ };
92
+ }
93
+
94
+ async function writeContracts(outFile, contracts) {
95
+ await fs.ensureDir(path.dirname(outFile));
96
+ await fs.writeJson(outFile, contracts, { spaces: 2 });
97
+ }
98
+
99
+ module.exports = { scanFrontend, writeContracts };
@@ -3,22 +3,15 @@ using Microsoft.AspNetCore.Mvc;
3
3
  namespace <%= projectName %>.Controllers;
4
4
 
5
5
  [ApiController]
6
- [Route("api/[controller]")]
6
+ [Route("[controller]")]
7
7
  public class <%= controllerName %>Controller : ControllerBase
8
8
  {
9
- // Endpoints for <%= controllerName %> auto-generated by Backlist
10
-
11
- <% endpoints.forEach(endpoint => { %>
12
- <%# Convert /api/users/{id} to just {id} for the route attribute %>
13
- <% const routePath = endpoint.path.replace(`/api/${controllerName.toLowerCase()}`, '').substring(1); %>
14
- /**
15
- * <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
16
- */
17
- [Http<%= endpoint.method.charAt(0) + endpoint.method.slice(1).toLowerCase() %>("<%- routePath %>")]
18
- public IActionResult AutoGenerated_<%= endpoint.method %>_<%= routePath.replace(/{|}/g, 'By_').replace(/[^a-zA-Z0-9_]/g, '') || 'Index' %>()
9
+ <% endpoints.forEach(ep => { -%>
10
+ [Http<%= ep.method.charAt(0) + ep.method.slice(1).toLowerCase() %>("<%= ep.route.replace(`/${controllerName.toLowerCase()}`, "") %>")]
11
+ public IActionResult <%= ep.actionName || (ep.method.toLowerCase() + controllerName) %>()
19
12
  {
20
- // TODO: Implement logic here. You can access route parameters like: public IActionResult Get(int id)
21
- return Ok(new { message = "Auto-generated response for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>" });
13
+ return Ok(new { message = "TODO: Implement <%= ep.method %> <%= ep.route %>" });
22
14
  }
23
- <% }); %>
15
+
16
+ <% }) -%>
24
17
  }
@@ -0,0 +1,8 @@
1
+ namespace <%= projectName %>.Models.DTOs;
2
+
3
+ public class <%= model.name %>
4
+ {
5
+ <% for (const [name, type] of Object.entries(model.fields || {})) { -%>
6
+ public <%= mapCSharpType(type) %> <%= pascal(name) %> { get; set; }
7
+ <% } -%>
8
+ }
@@ -0,0 +1,30 @@
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
+
8
+ import <%= group %>.<%= projectName %>.model.User;
9
+ import <%= group %>.<%= projectName %>.repository.UserRepository;
10
+
11
+ import org.springframework.security.crypto.password.PasswordEncoder;
12
+
13
+ @Configuration
14
+ public class ApplicationSeeder {
15
+
16
+ @Bean
17
+ CommandLineRunner seed(UserRepository userRepository, PasswordEncoder encoder) {
18
+ return args -> {
19
+ userRepository.findByEmail("admin@example.com").ifPresentOrElse(u -> {
20
+ // already exists
21
+ }, () -> {
22
+ User admin = new User();
23
+ admin.setName("Admin");
24
+ admin.setEmail("admin@example.com");
25
+ admin.setPassword(encoder.encode("admin123"));
26
+ userRepository.save(admin);
27
+ });
28
+ };
29
+ }
30
+ }
@@ -0,0 +1,62 @@
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
+
8
+ import org.springframework.http.ResponseEntity;
9
+ import org.springframework.http.HttpStatus;
10
+ import org.springframework.security.crypto.password.PasswordEncoder;
11
+ import org.springframework.web.bind.annotation.*;
12
+
13
+ import java.util.Optional;
14
+
15
+ @RestController
16
+ @RequestMapping("/api/auth")
17
+ @CrossOrigin(origins = "*")
18
+ public class AuthController {
19
+
20
+ private final UserRepository repo;
21
+ private final PasswordEncoder encoder;
22
+ private final JwtService jwt;
23
+
24
+ public AuthController(UserRepository repo, PasswordEncoder encoder, JwtService jwt) {
25
+ this.repo = repo;
26
+ this.encoder = encoder;
27
+ this.jwt = jwt;
28
+ }
29
+
30
+ @PostMapping("/register")
31
+ public ResponseEntity<?> register(@RequestBody AuthRequest req) {
32
+ if (repo.findByEmail(req.email()).isPresent()) {
33
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("User already exists");
34
+ }
35
+
36
+ User user = new User();
37
+ user.setName(req.name());
38
+ user.setEmail(req.email());
39
+ user.setPassword(encoder.encode(req.password()));
40
+ repo.save(user);
41
+
42
+ String token = jwt.generateToken(user.getEmail());
43
+ return ResponseEntity.status(HttpStatus.CREATED).body(new TokenResponse(token));
44
+ }
45
+
46
+ @PostMapping("/login")
47
+ public ResponseEntity<?> login(@RequestBody LoginRequest req) {
48
+ Optional<User> current = repo.findByEmail(req.email());
49
+ if (current.isEmpty()) return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid credentials");
50
+
51
+ if (!encoder.matches(req.password(), current.get().getPassword())) {
52
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid credentials");
53
+ }
54
+
55
+ String token = jwt.generateToken(current.get().getEmail());
56
+ return ResponseEntity.ok(new TokenResponse(token));
57
+ }
58
+
59
+ public record AuthRequest(String name, String email, String password) {}
60
+ public record LoginRequest(String email, String password) {}
61
+ public record TokenResponse(String token) {}
62
+ }
@@ -1,61 +1,51 @@
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
7
 
11
8
  import java.util.List;
12
- import java.util.Optional;
9
+
10
+ import <%= group %>.<%= projectName %>.service.<%= controllerName %>Service;
11
+ import <%= group %>.<%= projectName %>.model.<%= controllerName %>;
13
12
 
14
13
  @RestController
15
- @CrossOrigin(origins = "*") // Allow all origins for development
16
- @RequestMapping("/api/<%= controllerName.toLowerCase() %>s")
14
+ @RequestMapping("<%= basePath %>")
15
+ @CrossOrigin(origins = "*")
17
16
  public class <%= controllerName %>Controller {
18
-
19
- @Autowired
20
- private <%= controllerName %>Repository repository;
21
-
22
- @GetMapping
23
- public List<<%= controllerName %>> getAll<%= controllerName %>s() {
24
- return repository.findAll();
25
- }
26
-
27
- @GetMapping("/{id}")
28
- public ResponseEntity<<%= controllerName %>> get<%= controllerName %>ById(@PathVariable Long id) {
29
- Optional<<%= controllerName %>> item = repository.findById(id);
30
- return item.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
31
- }
32
-
33
- @PostMapping
34
- public ResponseEntity<<%= controllerName %>> create<%= controllerName %>(@RequestBody <%= controllerName %> newItem) {
35
- <%= controllerName %> savedItem = repository.save(newItem);
36
- return new ResponseEntity<>(savedItem, HttpStatus.CREATED);
37
- }
38
-
39
- @PutMapping("/{id}")
40
- public ResponseEntity<<%= controllerName %>> update<%= controllerName %>(@PathVariable Long id, @RequestBody <%= controllerName %> updatedItem) {
41
- return repository.findById(id)
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
- }
17
+ private final <%= controllerName %>Service service;
18
+
19
+ public <%= controllerName %>Controller(<%= controllerName %>Service service) {
20
+ this.service = service;
21
+ }
22
+
23
+ @GetMapping
24
+ public List<<%= controllerName %>> all() {
25
+ return service.findAll();
26
+ }
27
+
28
+ @GetMapping("/{id}")
29
+ public ResponseEntity<<%= controllerName %>> one(@PathVariable Long id) {
30
+ return service.findById(id)
31
+ .map(ResponseEntity::ok)
32
+ .orElse(ResponseEntity.notFound().build());
33
+ }
34
+
35
+ @PostMapping
36
+ public ResponseEntity<<%= controllerName %>> create(@RequestBody <%= controllerName %> m) {
37
+ return new ResponseEntity<>(service.create(m), HttpStatus.CREATED);
38
+ }
39
+
40
+ @PutMapping("/{id}")
41
+ public ResponseEntity<<%= controllerName %>> update(@PathVariable Long id, @RequestBody <%= controllerName %> m) {
42
+ return service.update(id, m)
43
+ .map(ResponseEntity::ok)
44
+ .orElse(ResponseEntity.notFound().build());
45
+ }
46
+
47
+ @DeleteMapping("/{id}")
48
+ public ResponseEntity<Void> delete(@PathVariable Long id) {
49
+ return service.delete(id) ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
50
+ }
61
51
  }
@@ -0,0 +1,16 @@
1
+ # Auto-generated by create-backlist
2
+ FROM eclipse-temurin:21-jdk AS build
3
+ WORKDIR /app
4
+
5
+ COPY .mvn/ .mvn/
6
+ COPY mvnw pom.xml ./
7
+ RUN chmod +x mvnw && ./mvnw -q -DskipTests dependency:go-offline
8
+
9
+ COPY src ./src
10
+ RUN ./mvnw -q -DskipTests package
11
+
12
+ FROM eclipse-temurin:21-jre
13
+ WORKDIR /app
14
+ COPY --from=build /app/target/*.jar app.jar
15
+ EXPOSE 8080
16
+ ENTRYPOINT ["java", "-jar", "app.jar"]
@@ -1,24 +1,25 @@
1
- // Auto-generated by create-backlist v6.0
1
+ // Auto-generated by create-backlist
2
2
  package <%= group %>.<%= projectName %>.model;
3
3
 
4
- import jakarta.persistence.Entity;
5
- import jakarta.persistence.Id;
6
- import jakarta.persistence.GeneratedValue;
7
- import jakarta.persistence.GenerationType;
8
- import lombok.Data;
4
+ import jakarta.persistence.*;
5
+ import lombok.*;
9
6
 
10
7
  @Data
8
+ @NoArgsConstructor
9
+ @AllArgsConstructor
11
10
  @Entity
11
+ @Table(name = "<%= modelName.toLowerCase() %>")
12
12
  public class <%= modelName %> {
13
13
 
14
- @Id
15
- @GeneratedValue(strategy = GenerationType.AUTO)
16
- private Long id;
14
+ @Id
15
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
16
+ private Long id;
17
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
- <% }); %>
18
+ <% model.fields.forEach(f => { -%>
19
+ private <%- (f.type === 'number' || f.type === 'Number') ? 'Double'
20
+ : (f.type === 'int' || f.type === 'Integer') ? 'Integer'
21
+ : (f.type === 'boolean' || f.type === 'Boolean') ? 'Boolean'
22
+ : (f.type === 'date' || f.type === 'Date') ? 'java.time.Instant'
23
+ : 'String' %> <%= f.name %>;
24
+ <% }) -%>
24
25
  }
@@ -0,0 +1,66 @@
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
+
9
+ import io.jsonwebtoken.JwtException;
10
+
11
+ import org.springframework.security.core.userdetails.UserDetailsService;
12
+ import org.springframework.security.core.userdetails.UserDetails;
13
+ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
14
+ import org.springframework.security.core.context.SecurityContextHolder;
15
+ import org.springframework.stereotype.Component;
16
+ import org.springframework.web.filter.OncePerRequestFilter;
17
+
18
+ import java.io.IOException;
19
+
20
+ @Component
21
+ public class JwtAuthFilter extends OncePerRequestFilter {
22
+
23
+ private final JwtService jwtService;
24
+ private final UserDetailsService uds;
25
+
26
+ public JwtAuthFilter(JwtService jwtService, UserDetailsService uds) {
27
+ this.jwtService = jwtService;
28
+ this.uds = uds;
29
+ }
30
+
31
+ @Override
32
+ protected boolean shouldNotFilter(HttpServletRequest request) {
33
+ String path = request.getRequestURI();
34
+ return path != null && path.startsWith("/api/auth");
35
+ }
36
+
37
+ @Override
38
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
39
+ throws ServletException, IOException {
40
+
41
+ // If already authenticated, skip
42
+ if (SecurityContextHolder.getContext().getAuthentication() != null) {
43
+ chain.doFilter(request, response);
44
+ return;
45
+ }
46
+
47
+ String header = request.getHeader("Authorization");
48
+
49
+ if (header != null && header.startsWith("Bearer ")) {
50
+ String token = header.substring(7).trim();
51
+ if (!token.isEmpty()) {
52
+ try {
53
+ String subject = jwtService.validateAndGetSubject(token);
54
+ UserDetails ud = uds.loadUserByUsername(subject);
55
+
56
+ var auth = new UsernamePasswordAuthenticationToken(ud, null, ud.getAuthorities());
57
+ SecurityContextHolder.getContext().setAuthentication(auth);
58
+ } catch (JwtException ex) {
59
+ // Invalid/expired token: continue without authentication
60
+ }
61
+ }
62
+ }
63
+
64
+ chain.doFilter(request, response);
65
+ }
66
+ }
@@ -0,0 +1,58 @@
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.nio.charset.StandardCharsets;
9
+ import java.security.Key;
10
+ import java.util.Date;
11
+
12
+ @Service
13
+ public class JwtService {
14
+
15
+ private final Key key;
16
+ private final long expirationMs;
17
+
18
+ public JwtService() {
19
+ String secret = System.getenv("JWT_SECRET");
20
+ if (secret == null || secret.trim().length() < 32) {
21
+ // Must be >= 32 chars for HS256 keys (jjwt enforces minimum)
22
+ secret = "change_this_dev_secret_change_this_dev_secret";
23
+ }
24
+ this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
25
+
26
+ String exp = System.getenv("JWT_EXPIRES_MS");
27
+ long fallback = 1000L * 60 * 60 * 5; // 5 hours
28
+ this.expirationMs = (exp != null) ? parseLongSafe(exp, fallback) : fallback;
29
+ }
30
+
31
+ public String generateToken(String subject) {
32
+ Date now = new Date();
33
+ Date exp = new Date(now.getTime() + expirationMs);
34
+
35
+ return Jwts.builder()
36
+ .setSubject(subject)
37
+ .setIssuedAt(now)
38
+ .setExpiration(exp)
39
+ .signWith(key, SignatureAlgorithm.HS256)
40
+ .compact();
41
+ }
42
+
43
+ /**
44
+ * @throws JwtException if token invalid/expired
45
+ */
46
+ public String validateAndGetSubject(String token) throws JwtException {
47
+ return Jwts.parserBuilder()
48
+ .setSigningKey(key)
49
+ .build()
50
+ .parseClaimsJws(token)
51
+ .getBody()
52
+ .getSubject();
53
+ }
54
+
55
+ private long parseLongSafe(String v, long fallback) {
56
+ try { return Long.parseLong(v); } catch (Exception e) { return fallback; }
57
+ }
58
+ }
@@ -1,12 +1,18 @@
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;
7
6
 
7
+ import <%= group %>.<%= projectName %>.model.<%= modelName %>;
8
+
8
9
  @Repository
9
10
  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.
11
+
12
+ <%
13
+ // If generator passes `extraFinders: [{name:'Email', type:'String'}]`
14
+ (extraFinders || []).forEach(f => {
15
+ -%>
16
+ java.util.Optional<<%= modelName %>> findBy<%= f.name %>(<%= f.type %> <%= f.name.charAt(0).toLowerCase() + f.name.slice(1) %>);
17
+ <% }) -%>
12
18
  }
@@ -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,69 @@
1
+ // Auto-generated by create-backlist
2
+ package <%= group %>.<%= projectName %>.service;
3
+
4
+ import org.springframework.stereotype.Service;
5
+
6
+ import java.util.List;
7
+ import java.util.Optional;
8
+
9
+ import <%= group %>.<%= projectName %>.repository.<%= modelName %>Repository;
10
+ import <%= group %>.<%= projectName %>.model.<%= modelName %>;
11
+
12
+ @Service
13
+ public class <%= modelName %>Service {
14
+
15
+ private final <%= modelName %>Repository repo;
16
+
17
+ public <%= modelName %>Service(<%= modelName %>Repository repo) {
18
+ this.repo = repo;
19
+ }
20
+
21
+ public List<<%= modelName %>> findAll() {
22
+ return repo.findAll();
23
+ }
24
+
25
+ public Optional<<%= modelName %>> findById(Long id) {
26
+ return repo.findById(id);
27
+ }
28
+
29
+ public <%= modelName %> create(<%= modelName %> m) {
30
+ // Ensure id is not forced by client
31
+ m.setId(null);
32
+ return repo.save(m);
33
+ }
34
+
35
+ public Optional<<%= modelName %>> update(Long id, <%= modelName %> m) {
36
+ return repo.findById(id).map(existing -> {
37
+ <% model.fields.forEach(f => {
38
+ const cap = f.name.charAt(0).toUpperCase() + f.name.slice(1);
39
+ -%>
40
+ // Update <%= f.name %>
41
+ existing.set<%= cap %>(m.get<%= cap %>());
42
+ <% }) -%>
43
+ return repo.save(existing);
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Partial update: only set non-null values.
49
+ * Useful if your frontend sends partial payloads.
50
+ */
51
+ public Optional<<%= modelName %>> patch(Long id, <%= modelName %> m) {
52
+ return repo.findById(id).map(existing -> {
53
+ <% model.fields.forEach(f => {
54
+ const cap = f.name.charAt(0).toUpperCase() + f.name.slice(1);
55
+ -%>
56
+ if (m.get<%= cap %>() != null) {
57
+ existing.set<%= cap %>(m.get<%= cap %>());
58
+ }
59
+ <% }) -%>
60
+ return repo.save(existing);
61
+ });
62
+ }
63
+
64
+ public boolean delete(Long id) {
65
+ if (!repo.existsById(id)) return false;
66
+ repo.deleteById(id);
67
+ return true;
68
+ }
69
+ }