create-backlist 6.1.6 → 6.1.8

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/analyzer.js +410 -99
  3. package/src/generators/dotnet.js +1 -1
  4. package/src/generators/java.js +154 -97
  5. package/src/generators/node.js +293 -232
  6. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +29 -7
  7. package/src/templates/java-spring/partials/AuthController.java.ejs +45 -14
  8. package/src/templates/java-spring/partials/Controller.java.ejs +25 -11
  9. package/src/templates/java-spring/partials/Dockerfile.ejs +25 -3
  10. package/src/templates/java-spring/partials/Entity.java.ejs +28 -3
  11. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +41 -7
  12. package/src/templates/java-spring/partials/JwtService.java.ejs +47 -12
  13. package/src/templates/java-spring/partials/Repository.java.ejs +8 -1
  14. package/src/templates/java-spring/partials/Service.java.ejs +30 -6
  15. package/src/templates/java-spring/partials/User.java.ejs +26 -3
  16. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
  17. package/src/templates/java-spring/partials/UserRepository.java.ejs +6 -0
  18. package/src/templates/java-spring/partials/docker-compose.yml.ejs +27 -5
  19. package/src/templates/node-ts-express/base/server.ts +63 -9
  20. package/src/templates/node-ts-express/base/tsconfig.json +19 -4
  21. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +24 -9
  22. package/src/templates/node-ts-express/partials/App.test.ts.ejs +47 -27
  23. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +68 -45
  24. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +45 -14
  25. package/src/templates/node-ts-express/partials/Auth.routes.ts.ejs +44 -5
  26. package/src/templates/node-ts-express/partials/Controller.ts.ejs +30 -16
  27. package/src/templates/node-ts-express/partials/Dockerfile.ejs +33 -11
  28. package/src/templates/node-ts-express/partials/Model.cs.ejs +38 -5
  29. package/src/templates/node-ts-express/partials/Model.ts.ejs +42 -12
  30. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +57 -23
  31. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +33 -10
  32. package/src/templates/node-ts-express/partials/README.md.ejs +8 -10
  33. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +99 -56
  34. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +30 -3
  35. package/src/templates/node-ts-express/partials/package.json.ejs +12 -7
  36. package/src/templates/node-ts-express/partials/routes.ts.ejs +31 -18
@@ -4,21 +4,43 @@ package <%= group %>.<%= projectName %>;
4
4
  import org.springframework.boot.CommandLineRunner;
5
5
  import org.springframework.context.annotation.Bean;
6
6
  import org.springframework.context.annotation.Configuration;
7
+ import org.springframework.core.env.Environment;
8
+
7
9
  import <%= group %>.<%= projectName %>.model.User;
8
10
  import <%= group %>.<%= projectName %>.repository.UserRepository;
11
+
9
12
  import org.springframework.security.crypto.password.PasswordEncoder;
10
13
 
11
14
  @Configuration
12
15
  public class ApplicationSeeder {
16
+
13
17
  @Bean
14
- CommandLineRunner seed(UserRepository userRepository, PasswordEncoder encoder) {
18
+ CommandLineRunner seed(UserRepository userRepository, PasswordEncoder encoder, Environment env) {
15
19
  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);
20
+
21
+ // Enable/disable seeding via env (default: false)
22
+ boolean enabled = Boolean.parseBoolean(env.getProperty("SEED_ENABLED", "false"));
23
+ if (!enabled) return;
24
+
25
+ String email = env.getProperty("ADMIN_EMAIL", "admin@example.com");
26
+ String password = env.getProperty("ADMIN_PASSWORD", "admin123");
27
+ String name = env.getProperty("ADMIN_NAME", "Admin");
28
+
29
+ try {
30
+ if (userRepository.findByEmail(email).isEmpty()) {
31
+ User admin = new User();
32
+ admin.setName(name);
33
+ admin.setEmail(email);
34
+ admin.setPassword(encoder.encode(password));
35
+
36
+ userRepository.save(admin);
37
+
38
+ System.out.println("[Seeder] Admin user created: " + email);
39
+ } else {
40
+ System.out.println("[Seeder] Admin already exists: " + email);
41
+ }
42
+ } catch (Exception e) {
43
+ System.out.println("[Seeder] Seeding failed: " + e.getMessage());
22
44
  }
23
45
  };
24
46
  }
@@ -4,6 +4,7 @@ package <%= group %>.<%= projectName %>.controller;
4
4
  import <%= group %>.<%= projectName %>.model.User;
5
5
  import <%= group %>.<%= projectName %>.repository.UserRepository;
6
6
  import <%= group %>.<%= projectName %>.security.JwtService;
7
+
7
8
  import org.springframework.http.ResponseEntity;
8
9
  import org.springframework.http.HttpStatus;
9
10
  import org.springframework.security.crypto.password.PasswordEncoder;
@@ -21,29 +22,59 @@ public class AuthController {
21
22
  private final JwtService jwt;
22
23
 
23
24
  public AuthController(UserRepository repo, PasswordEncoder encoder, JwtService jwt) {
24
- this.repo = repo; this.encoder = encoder; this.jwt = jwt;
25
+ this.repo = repo;
26
+ this.encoder = encoder;
27
+ this.jwt = jwt;
25
28
  }
26
29
 
30
+ public record RegisterRequest(String name, String email, String password) {}
31
+ public record LoginRequest(String email, String password) {}
32
+ public record TokenResponse(String token) {}
33
+ public record ErrorResponse(String message) {}
34
+
27
35
  @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");
36
+ public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
37
+ if (req == null || isBlank(req.email()) || isBlank(req.password()) || isBlank(req.name())) {
38
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("name, email, password are required"));
39
+ }
40
+
41
+ if (repo.findByEmail(req.email()).isPresent()) {
42
+ // better than 400: conflict
43
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse("User already exists"));
31
44
  }
32
- req.setPassword(encoder.encode(req.getPassword()));
33
- repo.save(req);
34
- String token = jwt.generateToken(req.getEmail());
45
+
46
+ User user = new User();
47
+ user.setName(req.name());
48
+ user.setEmail(req.email());
49
+ user.setPassword(encoder.encode(req.password()));
50
+
51
+ repo.save(user);
52
+
53
+ String token = jwt.generateToken(user.getEmail());
35
54
  return ResponseEntity.status(HttpStatus.CREATED).body(new TokenResponse(token));
36
55
  }
37
56
 
38
57
  @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());
58
+ public ResponseEntity<?> login(@RequestBody LoginRequest req) {
59
+ if (req == null || isBlank(req.email()) || isBlank(req.password())) {
60
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("email and password are required"));
61
+ }
62
+
63
+ Optional<User> current = repo.findByEmail(req.email());
64
+ if (current.isEmpty()) {
65
+ // better than 400: unauthorized
66
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Invalid credentials"));
67
+ }
68
+
69
+ if (!encoder.matches(req.password(), current.get().getPassword())) {
70
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Invalid credentials"));
71
+ }
72
+
73
+ String token = jwt.generateToken(current.get().getEmail());
45
74
  return ResponseEntity.ok(new TokenResponse(token));
46
75
  }
47
76
 
48
- public record TokenResponse(String token) {}
77
+ private boolean isBlank(String s) {
78
+ return s == null || s.trim().isEmpty();
79
+ }
49
80
  }
@@ -4,37 +4,51 @@ package <%= group %>.<%= projectName %>.controller;
4
4
  import org.springframework.http.ResponseEntity;
5
5
  import org.springframework.http.HttpStatus;
6
6
  import org.springframework.web.bind.annotation.*;
7
+
7
8
  import java.util.List;
8
- import java.util.Optional;
9
+
9
10
  import <%= group %>.<%= projectName %>.service.<%= controllerName %>Service;
10
11
  import <%= group %>.<%= projectName %>.model.<%= controllerName %>;
11
12
 
12
13
  @RestController
13
- @RequestMapping("/api/<%= controllerName.toLowerCase() %>s")
14
+ @RequestMapping("/api/<%= controllerName.toLowerCase() %>")
14
15
  @CrossOrigin(origins = "*")
15
16
  public class <%= controllerName %>Controller {
17
+
16
18
  private final <%= controllerName %>Service service;
17
- public <%= controllerName %>Controller(<%= controllerName %>Service service) { this.service = service; }
18
19
 
19
- @GetMapping public List<<%= controllerName %>> all() { return service.findAll(); }
20
+ public <%= controllerName %>Controller(<%= controllerName %>Service service) {
21
+ this.service = service;
22
+ }
23
+
24
+ @GetMapping
25
+ public ResponseEntity<List<<%= controllerName %>>> all() {
26
+ return ResponseEntity.ok(service.findAll());
27
+ }
20
28
 
21
29
  @GetMapping("/{id}")
22
- public ResponseEntity<<%= controllerName %>> one(@PathVariable Long id) {
23
- return service.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
30
+ public ResponseEntity<<%= controllerName %>> one(@PathVariable String id) {
31
+ return service.findById(id)
32
+ .map(ResponseEntity::ok)
33
+ .orElse(ResponseEntity.notFound().build());
24
34
  }
25
35
 
26
36
  @PostMapping
27
37
  public ResponseEntity<<%= controllerName %>> create(@RequestBody <%= controllerName %> m) {
28
- return new ResponseEntity<>(service.create(m), HttpStatus.CREATED);
38
+ return ResponseEntity.status(HttpStatus.CREATED).body(service.create(m));
29
39
  }
30
40
 
31
41
  @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());
42
+ public ResponseEntity<<%= controllerName %>> update(@PathVariable String id, @RequestBody <%= controllerName %> m) {
43
+ return service.update(id, m)
44
+ .map(ResponseEntity::ok)
45
+ .orElse(ResponseEntity.notFound().build());
34
46
  }
35
47
 
36
48
  @DeleteMapping("/{id}")
37
- public ResponseEntity<Void> delete(@PathVariable Long id) {
38
- return service.delete(id) ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
49
+ public ResponseEntity<Void> delete(@PathVariable String id) {
50
+ return service.delete(id)
51
+ ? ResponseEntity.noContent().build()
52
+ : ResponseEntity.notFound().build();
39
53
  }
40
54
  }
@@ -1,11 +1,33 @@
1
- # Auto-generated by create-backlist
1
+ # Auto-generated by create-backlist (Spring Boot)
2
+
3
+ # ---- Build Stage ----
2
4
  FROM eclipse-temurin:21-jdk AS build
3
5
  WORKDIR /app
4
- COPY . .
6
+
7
+ # Copy Maven wrapper + pom first for better layer caching
8
+ COPY mvnw .
9
+ COPY .mvn .mvn
10
+ COPY pom.xml .
11
+
12
+ # Download deps (cached)
13
+ RUN ./mvnw -q -DskipTests dependency:go-offline
14
+
15
+ # Copy source and build
16
+ COPY src src
5
17
  RUN ./mvnw -q -DskipTests package
6
18
 
19
+ # ---- Runtime Stage ----
7
20
  FROM eclipse-temurin:21-jre
8
21
  WORKDIR /app
22
+
23
+ ENV JAVA_OPTS=""
24
+ ENV SPRING_PROFILES_ACTIVE=default
25
+
9
26
  COPY --from=build /app/target/*.jar app.jar
27
+
10
28
  EXPOSE 8080
11
- ENTRYPOINT ["java", "-jar", "app.jar"]
29
+
30
+ # Optional healthcheck (requires wget/curl; omit if base image lacks it)
31
+ # HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 CMD wget -qO- http://localhost:8080/actuator/health || exit 1
32
+
33
+ ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
@@ -4,12 +4,37 @@ package <%= group %>.<%= projectName %>.model;
4
4
  import jakarta.persistence.*;
5
5
  import lombok.Data;
6
6
 
7
+ import java.time.Instant;
8
+
7
9
  @Data
8
10
  @Entity
11
+ @Table(name = "<%= modelName.toLowerCase() %>")
9
12
  public class <%= modelName %> {
10
- @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
13
+
14
+ @Id
15
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
11
16
  private Long id;
12
- <% model.fields.forEach(f => { %>
13
- private <%= f.type === 'Number' ? 'Integer' : (f.type === 'Boolean' ? 'Boolean' : 'String') %> <%= f.name %>;
17
+
18
+ <% model.fields.forEach(f => {
19
+ const n = String(f.name || '').toLowerCase();
20
+ let t = 'String';
21
+ if (f.type === 'Number') t = 'Integer';
22
+ if (f.type === 'Boolean') t = 'Boolean';
23
+ // heuristic: money-like fields -> BigDecimal
24
+ if (f.type === 'Number' && (n.includes('price') || n.includes('amount') || n.includes('total'))) t = 'java.math.BigDecimal';
25
+ %>
26
+ @Column(name = "<%= f.name %>"<% if (f.isUnique) { %>, unique = true<% } %><% if (n.includes('email')) { %>, nullable = false<% } %>)
27
+ private <%= t %> <%= f.name %>;
14
28
  <% }) %>
29
+
30
+ @Column(nullable = false, updatable = false)
31
+ private Instant createdAt = Instant.now();
32
+
33
+ @Column(nullable = false)
34
+ private Instant updatedAt = Instant.now();
35
+
36
+ @PreUpdate
37
+ public void onUpdate() {
38
+ this.updatedAt = Instant.now();
39
+ }
15
40
  }
@@ -5,10 +5,12 @@ import jakarta.servlet.FilterChain;
5
5
  import jakarta.servlet.ServletException;
6
6
  import jakarta.servlet.http.HttpServletRequest;
7
7
  import jakarta.servlet.http.HttpServletResponse;
8
+
8
9
  import org.springframework.security.core.userdetails.UserDetailsService;
9
10
  import org.springframework.security.core.userdetails.UserDetails;
10
11
  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
11
12
  import org.springframework.security.core.context.SecurityContextHolder;
13
+
12
14
  import org.springframework.stereotype.Component;
13
15
  import org.springframework.web.filter.OncePerRequestFilter;
14
16
 
@@ -28,16 +30,48 @@ public class JwtAuthFilter extends OncePerRequestFilter {
28
30
  @Override
29
31
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
30
32
  throws ServletException, IOException {
33
+
34
+ // Skip if already authenticated
35
+ if (SecurityContextHolder.getContext().getAuthentication() != null) {
36
+ chain.doFilter(request, response);
37
+ return;
38
+ }
39
+
31
40
  String header = request.getHeader("Authorization");
41
+
32
42
  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) {}
43
+ String token = header.substring(7).trim();
44
+
45
+ if (!token.isEmpty()) {
46
+ try {
47
+ String subject = jwtService.validateAndGetSubject(token); // usually email/username
48
+
49
+ UserDetails ud = uds.loadUserByUsername(subject);
50
+
51
+ var auth = new UsernamePasswordAuthenticationToken(
52
+ ud,
53
+ null,
54
+ ud.getAuthorities()
55
+ );
56
+ auth.setDetails(new org.springframework.security.web.authentication.WebAuthenticationDetailsSource()
57
+ .buildDetails(request));
58
+
59
+ SecurityContextHolder.getContext().setAuthentication(auth);
60
+
61
+ } catch (Exception ex) {
62
+ // Invalid token -> do not authenticate; proceed as anonymous
63
+ // (Optionally you could log here)
64
+ }
65
+ }
40
66
  }
67
+
41
68
  chain.doFilter(request, response);
42
69
  }
70
+
71
+ @Override
72
+ protected boolean shouldNotFilter(HttpServletRequest request) {
73
+ // Don't run filter for auth endpoints (optional)
74
+ String path = request.getServletPath();
75
+ return path != null && path.startsWith("/api/auth");
76
+ }
43
77
  }
@@ -1,30 +1,65 @@
1
1
  // Auto-generated by create-backlist
2
2
  package <%= group %>.<%= projectName %>.security;
3
3
 
4
- import io.jsonwebtoken.*;
4
+ import io.jsonwebtoken.Jwts;
5
+ import io.jsonwebtoken.JwtException;
6
+ import io.jsonwebtoken.SignatureAlgorithm;
5
7
  import io.jsonwebtoken.security.Keys;
8
+
9
+ import org.springframework.beans.factory.annotation.Value;
6
10
  import org.springframework.stereotype.Service;
7
11
 
12
+ import java.nio.charset.StandardCharsets;
8
13
  import java.security.Key;
9
14
  import java.util.Date;
10
15
 
11
16
  @Service
12
17
  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
18
 
17
- public String generateToken(String userId) {
19
+ private final Key key;
20
+ private final long expiresInMs;
21
+
22
+ public JwtService(
23
+ @Value("${JWT_SECRET:change_this_dev_secret_change_this_dev_secret}") String secret,
24
+ @Value("${JWT_EXPIRES_IN_MS:18000000}") long expiresInMs // default 5h
25
+ ) {
26
+ // HS256 requires a sufficiently long secret (>= 32 bytes recommended)
27
+ byte[] bytes = secret.getBytes(StandardCharsets.UTF_8);
28
+ if (bytes.length < 32) {
29
+ // pad (dev fallback). Better: set a proper secret in env.
30
+ byte[] padded = new byte[32];
31
+ System.arraycopy(bytes, 0, padded, 0, bytes.length);
32
+ for (int i = bytes.length; i < 32; i++) padded[i] = (byte) 'x';
33
+ bytes = padded;
34
+ }
35
+ this.key = Keys.hmacShaKeyFor(bytes);
36
+ this.expiresInMs = expiresInMs;
37
+ }
38
+
39
+ /**
40
+ * subject should be a stable identifier (email or userId)
41
+ */
42
+ public String generateToken(String subject) {
43
+ long now = System.currentTimeMillis();
18
44
  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();
45
+ .setSubject(subject)
46
+ .setIssuedAt(new Date(now))
47
+ .setExpiration(new Date(now + expiresInMs))
48
+ .signWith(key, SignatureAlgorithm.HS256)
49
+ .compact();
24
50
  }
25
51
 
26
52
  public String validateAndGetSubject(String token) {
27
- return Jwts.parserBuilder().setSigningKey(key).build()
28
- .parseClaimsJws(token).getBody().getSubject();
53
+ try {
54
+ return Jwts.parserBuilder()
55
+ .setSigningKey(key)
56
+ .build()
57
+ .parseClaimsJws(token)
58
+ .getBody()
59
+ .getSubject();
60
+ } catch (JwtException ex) {
61
+ // signature invalid / expired / malformed
62
+ throw new IllegalArgumentException("Invalid JWT token");
63
+ }
29
64
  }
30
65
  }
@@ -3,7 +3,14 @@ package <%= group %>.<%= projectName %>.repository;
3
3
 
4
4
  import org.springframework.data.jpa.repository.JpaRepository;
5
5
  import org.springframework.stereotype.Repository;
6
+
6
7
  import <%= group %>.<%= projectName %>.model.<%= modelName %>;
7
8
 
8
9
  @Repository
9
- public interface <%= modelName %>Repository extends JpaRepository<<%= modelName %>, Long> {}
10
+ public interface <%= modelName %>Repository extends JpaRepository<<%= modelName %>, Long> {
11
+
12
+ // Add common query methods here if needed.
13
+ // Example:
14
+ // Optional<<%= modelName %>> findByEmail(String email);
15
+
16
+ }
@@ -2,27 +2,51 @@
2
2
  package <%= group %>.<%= projectName %>.service;
3
3
 
4
4
  import org.springframework.stereotype.Service;
5
+ import org.springframework.transaction.annotation.Transactional;
6
+
5
7
  import java.util.List;
6
8
  import java.util.Optional;
9
+
7
10
  import <%= group %>.<%= projectName %>.repository.<%= modelName %>Repository;
8
11
  import <%= group %>.<%= projectName %>.model.<%= modelName %>;
9
12
 
10
13
  @Service
14
+ @Transactional
11
15
  public class <%= modelName %>Service {
16
+
12
17
  private final <%= modelName %>Repository repo;
13
- public <%= modelName %>Service(<%= modelName %>Repository repo) { this.repo = repo; }
14
18
 
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); }
19
+ public <%= modelName %>Service(<%= modelName %>Repository repo) {
20
+ this.repo = repo;
21
+ }
22
+
23
+ @Transactional(readOnly = true)
24
+ public List<<%= modelName %>> findAll() {
25
+ return repo.findAll();
26
+ }
27
+
28
+ @Transactional(readOnly = true)
29
+ public Optional<<%= modelName %>> findById(Long id) {
30
+ return repo.findById(id);
31
+ }
32
+
33
+ public <%= modelName %> create(<%= modelName %> m) {
34
+ // Ensure ID is not set by client
35
+ m.setId(null);
36
+ return repo.save(m);
37
+ }
38
+
18
39
  public Optional<<%= modelName %>> update(Long id, <%= modelName %> m) {
19
40
  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) %>());
41
+ <% (model.fields || []).forEach(f => {
42
+ const cap = f.name.charAt(0).toUpperCase() + f.name.slice(1);
43
+ %>
44
+ existing.set<%= cap %>(m.get<%= cap %>());
22
45
  <% }) %>
23
46
  return repo.save(existing);
24
47
  });
25
48
  }
49
+
26
50
  public boolean delete(Long id) {
27
51
  if (!repo.existsById(id)) return false;
28
52
  repo.deleteById(id);
@@ -4,17 +4,40 @@ package <%= group %>.<%= projectName %>.model;
4
4
  import jakarta.persistence.*;
5
5
  import lombok.Data;
6
6
 
7
+ import java.time.Instant;
8
+
7
9
  @Data
8
10
  @Entity
9
- @Table(name="users")
11
+ @Table(
12
+ name = "users",
13
+ indexes = {
14
+ @Index(name = "idx_users_email", columnList = "email", unique = true)
15
+ }
16
+ )
10
17
  public class User {
11
- @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
18
+
19
+ @Id
20
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
12
21
  private Long id;
13
22
 
23
+ @Column(nullable = false)
14
24
  private String name;
15
25
 
16
- @Column(unique = true)
26
+ @Column(nullable = false, unique = true)
17
27
  private String email;
18
28
 
29
+ // Never expose this field in API responses
30
+ @Column(nullable = false)
19
31
  private String password;
32
+
33
+ @Column(nullable = false, updatable = false)
34
+ private Instant createdAt = Instant.now();
35
+
36
+ @Column(nullable = false)
37
+ private Instant updatedAt = Instant.now();
38
+
39
+ @PreUpdate
40
+ public void onUpdate() {
41
+ this.updatedAt = Instant.now();
42
+ }
20
43
  }
@@ -3,12 +3,12 @@ package <%= group %>.<%= projectName %>.security;
3
3
 
4
4
  import <%= group %>.<%= projectName %>.model.User;
5
5
  import <%= group %>.<%= projectName %>.repository.UserRepository;
6
+
6
7
  import org.springframework.security.core.userdetails.UserDetailsService;
7
8
  import org.springframework.security.core.userdetails.UserDetails;
8
9
  import org.springframework.security.core.userdetails.UsernameNotFoundException;
10
+
9
11
  import org.springframework.stereotype.Service;
10
- import org.springframework.security.core.userdetails.User.UserBuilder;
11
- import org.springframework.security.core.userdetails.User.*;
12
12
 
13
13
  @Service
14
14
  public class UserDetailsServiceImpl implements UserDetailsService {
@@ -21,7 +21,13 @@ public class UserDetailsServiceImpl implements UserDetailsService {
21
21
 
22
22
  @Override
23
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();
24
+ User u = repo.findByEmail(email)
25
+ .orElseThrow(() -> new UsernameNotFoundException("User not found"));
26
+
27
+ return org.springframework.security.core.userdetails.User
28
+ .withUsername(u.getEmail())
29
+ .password(u.getPassword())
30
+ .authorities("ROLE_USER")
31
+ .build();
26
32
  }
27
33
  }
@@ -2,11 +2,17 @@
2
2
  package <%= group %>.<%= projectName %>.repository;
3
3
 
4
4
  import java.util.Optional;
5
+
5
6
  import org.springframework.data.jpa.repository.JpaRepository;
6
7
  import org.springframework.stereotype.Repository;
8
+
7
9
  import <%= group %>.<%= projectName %>.model.User;
8
10
 
9
11
  @Repository
10
12
  public interface UserRepository extends JpaRepository<User, Long> {
13
+
11
14
  Optional<User> findByEmail(String email);
15
+
16
+ boolean existsByEmail(String email);
17
+
12
18
  }
@@ -1,7 +1,9 @@
1
1
  version: '3.8'
2
+
2
3
  services:
3
4
  db:
4
5
  image: postgres:16-alpine
6
+ container_name: <%= projectName %>-db
5
7
  environment:
6
8
  POSTGRES_USER: ${DB_USER:-postgres}
7
9
  POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
@@ -10,18 +12,38 @@ services:
10
12
  - "5432:5432"
11
13
  volumes:
12
14
  - pgdata:/var/lib/postgresql/data
15
+ healthcheck:
16
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-<%= projectName %>}"]
17
+ interval: 10s
18
+ timeout: 5s
19
+ retries: 10
20
+ restart: unless-stopped
13
21
 
14
22
  app:
15
23
  build: .
24
+ container_name: <%= projectName %>-app
16
25
  depends_on:
17
- - db
26
+ db:
27
+ condition: service_healthy
18
28
  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}
29
+ # Spring datasource
30
+ SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/${DB_NAME:-<%= projectName %>}
31
+ SPRING_DATASOURCE_USERNAME: ${DB_USER:-postgres}
32
+ SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-password}
33
+
34
+ # JPA defaults (optional but useful)
35
+ SPRING_JPA_HIBERNATE_DDL_AUTO: update
36
+ SPRING_JPA_SHOW_SQL: "true"
37
+
38
+ # JWT (only needed if auth is enabled)
39
+ JWT_SECRET: ${JWT_SECRET:-change_me_long_secret}
40
+ JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-5h}
41
+
42
+ # CORS (optional)
43
+ CORS_ORIGIN: ${CORS_ORIGIN:-*}
23
44
  ports:
24
45
  - "8080:8080"
46
+ restart: unless-stopped
25
47
 
26
48
  volumes:
27
49
  pgdata: