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.
- package/package.json +1 -1
- package/src/analyzer.js +410 -99
- package/src/generators/dotnet.js +1 -1
- package/src/generators/java.js +154 -97
- package/src/generators/node.js +293 -232
- package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +29 -7
- package/src/templates/java-spring/partials/AuthController.java.ejs +45 -14
- package/src/templates/java-spring/partials/Controller.java.ejs +25 -11
- package/src/templates/java-spring/partials/Dockerfile.ejs +25 -3
- package/src/templates/java-spring/partials/Entity.java.ejs +28 -3
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +41 -7
- package/src/templates/java-spring/partials/JwtService.java.ejs +47 -12
- package/src/templates/java-spring/partials/Repository.java.ejs +8 -1
- package/src/templates/java-spring/partials/Service.java.ejs +30 -6
- package/src/templates/java-spring/partials/User.java.ejs +26 -3
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
- package/src/templates/java-spring/partials/UserRepository.java.ejs +6 -0
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +27 -5
- package/src/templates/node-ts-express/base/server.ts +63 -9
- package/src/templates/node-ts-express/base/tsconfig.json +19 -4
- package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +24 -9
- package/src/templates/node-ts-express/partials/App.test.ts.ejs +47 -27
- package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +68 -45
- package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +45 -14
- package/src/templates/node-ts-express/partials/Auth.routes.ts.ejs +44 -5
- package/src/templates/node-ts-express/partials/Controller.ts.ejs +30 -16
- package/src/templates/node-ts-express/partials/Dockerfile.ejs +33 -11
- package/src/templates/node-ts-express/partials/Model.cs.ejs +38 -5
- package/src/templates/node-ts-express/partials/Model.ts.ejs +42 -12
- package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +57 -23
- package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +33 -10
- package/src/templates/node-ts-express/partials/README.md.ejs +8 -10
- package/src/templates/node-ts-express/partials/Seeder.ts.ejs +99 -56
- package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +30 -3
- package/src/templates/node-ts-express/partials/package.json.ejs +12 -7
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
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
|
|
29
|
-
if (
|
|
30
|
-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() %>
|
|
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
|
-
|
|
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
|
|
23
|
-
return service.findById(id)
|
|
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
|
|
38
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(m));
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
@PutMapping("/{id}")
|
|
32
|
-
public ResponseEntity<<%= controllerName %>> update(@PathVariable
|
|
33
|
-
return service.update(id, m)
|
|
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
|
|
38
|
-
return service.delete(id)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
|
|
14
|
+
@Id
|
|
15
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
11
16
|
private Long id;
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
.
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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)
|
|
25
|
-
|
|
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
|
-
|
|
26
|
+
db:
|
|
27
|
+
condition: service_healthy
|
|
18
28
|
environment:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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:
|