sprygen 1.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/README.md +80 -0
- package/dist/cli.js +55 -0
- package/package.json +53 -0
- package/templates/auth/AuthController.java.ejs +40 -0
- package/templates/auth/JwtAuthFilter.java.ejs +62 -0
- package/templates/auth/JwtService.java.ejs +81 -0
- package/templates/auth/SecurityConfig.java.ejs +65 -0
- package/templates/auth/UserDetailsServiceImpl.java.ejs +24 -0
- package/templates/entity/Entity.java.ejs +40 -0
- package/templates/entity/EntityController.java.ejs +92 -0
- package/templates/entity/EntityControllerTest.java.ejs +24 -0
- package/templates/entity/EntityDto.java.ejs +32 -0
- package/templates/entity/EntityRepository.java.ejs +9 -0
- package/templates/entity/EntityService.java.ejs +32 -0
- package/templates/project/java/config/CorsConfig.java.ejs +24 -0
- package/templates/project/java/config/SecurityConfig.java.ejs +76 -0
- package/templates/project/java/config/SecurityConfigSession.java.ejs +73 -0
- package/templates/project/java/config/SwaggerConfig.java.ejs +31 -0
- package/templates/project/java/controller/AdminController.java.ejs +82 -0
- package/templates/project/java/controller/AuthController.java.ejs +86 -0
- package/templates/project/java/controller/HomeController.java.ejs +63 -0
- package/templates/project/java/controller/ProfileController.java.ejs +65 -0
- package/templates/project/java/controller/UserController.java.ejs +35 -0
- package/templates/project/java/dto/AuthRequest.java.ejs +15 -0
- package/templates/project/java/dto/AuthResponse.java.ejs +18 -0
- package/templates/project/java/dto/ProfileUpdateRequest.java.ejs +20 -0
- package/templates/project/java/dto/RegisterRequest.java.ejs +30 -0
- package/templates/project/java/dto/UserDto.java.ejs +17 -0
- package/templates/project/java/entity/Role.java.ejs +6 -0
- package/templates/project/java/entity/User.java.ejs +97 -0
- package/templates/project/java/repository/UserRepository.java.ejs +11 -0
- package/templates/project/java/security/JwtAuthFilter.java.ejs +62 -0
- package/templates/project/java/security/UserDetailsServiceImpl.java.ejs +21 -0
- package/templates/project/java/service/JwtService.java.ejs +81 -0
- package/templates/project/java/service/UserService.java.ejs +32 -0
- package/templates/project/resources/application.yml.ejs +50 -0
- package/templates/project/resources/logback-spring.xml.ejs +41 -0
- package/templates/project/static/admin.html.ejs +163 -0
- package/templates/project/static/assets/app.js.ejs +340 -0
- package/templates/project/static/assets/style.css +533 -0
- package/templates/project/static/css/style.css +595 -0
- package/templates/project/static/dashboard.html.ejs +119 -0
- package/templates/project/static/index.html.ejs +96 -0
- package/templates/project/static/js/api.js +30 -0
- package/templates/project/static/js/auth.js +44 -0
- package/templates/project/static/js/nav.js.ejs +82 -0
- package/templates/project/static/js/ui.js +57 -0
- package/templates/project/static/login.html.ejs +71 -0
- package/templates/project/static/profile.html.ejs +163 -0
- package/templates/project/static/register.html.ejs +82 -0
- package/templates/project/thymeleaf/admin/users.html.ejs +111 -0
- package/templates/project/thymeleaf/dashboard.html.ejs +109 -0
- package/templates/project/thymeleaf/layout.html.ejs +75 -0
- package/templates/project/thymeleaf/login.html.ejs +56 -0
- package/templates/project/thymeleaf/profile.html.ejs +133 -0
- package/templates/project/thymeleaf/register.html.ejs +56 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package <%= packageName %>.controller;
|
|
2
|
+
|
|
3
|
+
import org.junit.jupiter.api.Test;
|
|
4
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
5
|
+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
|
6
|
+
import org.springframework.boot.test.context.SpringBootTest;
|
|
7
|
+
import org.springframework.test.web.servlet.MockMvc;
|
|
8
|
+
|
|
9
|
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
10
|
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
11
|
+
|
|
12
|
+
@SpringBootTest
|
|
13
|
+
@AutoConfigureMockMvc
|
|
14
|
+
class <%= entityName %>ControllerTest {
|
|
15
|
+
|
|
16
|
+
@Autowired
|
|
17
|
+
private MockMvc mockMvc;
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
void testGetAll() throws Exception {
|
|
21
|
+
mockMvc.perform(get("/api/v1/<%= entityNameLower %>s"))
|
|
22
|
+
.andExpect(status().isOk());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package <%= packageName %>.dto;
|
|
2
|
+
|
|
3
|
+
import lombok.AllArgsConstructor;
|
|
4
|
+
import lombok.Builder;
|
|
5
|
+
import lombok.Data;
|
|
6
|
+
import lombok.NoArgsConstructor;
|
|
7
|
+
|
|
8
|
+
<%_
|
|
9
|
+
const hasLocalDate = fields.some(f => f.type === 'LocalDate');
|
|
10
|
+
const hasLocalDateTime = fields.some(f => f.type === 'LocalDateTime');
|
|
11
|
+
|
|
12
|
+
if (hasLocalDate) { _%>
|
|
13
|
+
import java.time.LocalDate;
|
|
14
|
+
<%_ }
|
|
15
|
+
if (hasLocalDateTime) { _%>
|
|
16
|
+
import java.time.LocalDateTime;
|
|
17
|
+
<%_ } _%>
|
|
18
|
+
|
|
19
|
+
@Data
|
|
20
|
+
@Builder
|
|
21
|
+
@NoArgsConstructor
|
|
22
|
+
@AllArgsConstructor
|
|
23
|
+
public class <%= entityName %>Dto {
|
|
24
|
+
|
|
25
|
+
private Long id;
|
|
26
|
+
|
|
27
|
+
<%_ for (let i = 0; i < fields.length; i++) {
|
|
28
|
+
let field = fields[i]; _%>
|
|
29
|
+
private <%= field.type %> <%= field.name %>;
|
|
30
|
+
|
|
31
|
+
<%_ } _%>
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
package <%= packageName %>.repository;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.entity.<%= entityName %>;
|
|
4
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
5
|
+
import org.springframework.stereotype.Repository;
|
|
6
|
+
|
|
7
|
+
@Repository
|
|
8
|
+
public interface <%= entityName %>Repository extends JpaRepository<<%= entityName %>, Long> {
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package <%= packageName %>.service;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.entity.<%= entityName %>;
|
|
4
|
+
import <%= packageName %>.repository.<%= entityName %>Repository;
|
|
5
|
+
import lombok.RequiredArgsConstructor;
|
|
6
|
+
import org.springframework.stereotype.Service;
|
|
7
|
+
|
|
8
|
+
import java.util.List;
|
|
9
|
+
import java.util.Optional;
|
|
10
|
+
|
|
11
|
+
@Service
|
|
12
|
+
@RequiredArgsConstructor
|
|
13
|
+
public class <%= entityName %>Service {
|
|
14
|
+
|
|
15
|
+
private final <%= entityName %>Repository repository;
|
|
16
|
+
|
|
17
|
+
public List<<%= entityName %>> findAll() {
|
|
18
|
+
return repository.findAll();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public Optional<<%= entityName %>> findById(Long id) {
|
|
22
|
+
return repository.findById(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public <%= entityName %> save(<%= entityName %> entity) {
|
|
26
|
+
return repository.save(entity);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public void deleteById(Long id) {
|
|
30
|
+
repository.deleteById(id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package <%= packageName %>.config;
|
|
2
|
+
|
|
3
|
+
import org.springframework.context.annotation.Bean;
|
|
4
|
+
import org.springframework.context.annotation.Configuration;
|
|
5
|
+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|
6
|
+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
7
|
+
|
|
8
|
+
@Configuration
|
|
9
|
+
public class CorsConfig {
|
|
10
|
+
|
|
11
|
+
@Bean
|
|
12
|
+
public WebMvcConfigurer corsConfigurer() {
|
|
13
|
+
return new WebMvcConfigurer() {
|
|
14
|
+
@Override
|
|
15
|
+
public void addCorsMappings(CorsRegistry registry) {
|
|
16
|
+
registry.addMapping("/**")
|
|
17
|
+
.allowedOrigins("*") // In production, restrict this to specific domains
|
|
18
|
+
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
|
19
|
+
.allowedHeaders("*")
|
|
20
|
+
.maxAge(3600);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package <%= packageName %>.config;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.security.JwtAuthFilter;
|
|
4
|
+
import lombok.RequiredArgsConstructor;
|
|
5
|
+
import org.springframework.context.annotation.Bean;
|
|
6
|
+
import org.springframework.context.annotation.Configuration;
|
|
7
|
+
import org.springframework.security.authentication.AuthenticationManager;
|
|
8
|
+
import org.springframework.security.authentication.AuthenticationProvider;
|
|
9
|
+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
|
10
|
+
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
|
11
|
+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
|
12
|
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
13
|
+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
14
|
+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
|
15
|
+
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
16
|
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
17
|
+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
18
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
19
|
+
import org.springframework.security.web.SecurityFilterChain;
|
|
20
|
+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
21
|
+
|
|
22
|
+
@Configuration
|
|
23
|
+
@EnableWebSecurity
|
|
24
|
+
@EnableMethodSecurity
|
|
25
|
+
@RequiredArgsConstructor
|
|
26
|
+
public class SecurityConfig {
|
|
27
|
+
|
|
28
|
+
private final JwtAuthFilter jwtAuthFilter;
|
|
29
|
+
private final UserDetailsService userDetailsService;
|
|
30
|
+
|
|
31
|
+
@Bean
|
|
32
|
+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
|
33
|
+
http
|
|
34
|
+
.csrf(AbstractHttpConfigurer::disable)
|
|
35
|
+
.authorizeHttpRequests(authorize -> authorize
|
|
36
|
+
// Public auth endpoints
|
|
37
|
+
.requestMatchers("/api/v1/auth/**").permitAll()
|
|
38
|
+
// Public home / static assets (individual pages + assets)
|
|
39
|
+
.requestMatchers("/", "/index.html", "/login.html", "/register.html").permitAll()
|
|
40
|
+
.requestMatchers("/dashboard.html", "/profile.html", "/admin.html").permitAll()
|
|
41
|
+
.requestMatchers("/assets/**", "/css/**", "/js/**", "/favicon.ico").permitAll()
|
|
42
|
+
<%_ if (hasSwagger) { _%>
|
|
43
|
+
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
|
|
44
|
+
<%_ } _%>
|
|
45
|
+
.requestMatchers("/actuator/**").permitAll()
|
|
46
|
+
// Admin endpoints — ROLE_ADMIN enforced via @PreAuthorize too
|
|
47
|
+
.requestMatchers("/api/v1/admin/**").hasAuthority("ROLE_ADMIN")
|
|
48
|
+
.anyRequest().authenticated()
|
|
49
|
+
)
|
|
50
|
+
.sessionManagement(session -> session
|
|
51
|
+
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
52
|
+
)
|
|
53
|
+
.authenticationProvider(authenticationProvider())
|
|
54
|
+
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
|
55
|
+
|
|
56
|
+
return http.build();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Bean
|
|
60
|
+
public AuthenticationProvider authenticationProvider() {
|
|
61
|
+
// Spring Security 6.x: UserDetailsService must be passed to the constructor
|
|
62
|
+
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
|
|
63
|
+
authProvider.setPasswordEncoder(passwordEncoder());
|
|
64
|
+
return authProvider;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Bean
|
|
68
|
+
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
|
69
|
+
return config.getAuthenticationManager();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Bean
|
|
73
|
+
public PasswordEncoder passwordEncoder() {
|
|
74
|
+
return new BCryptPasswordEncoder();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package <%= packageName %>.config;
|
|
2
|
+
|
|
3
|
+
import lombok.RequiredArgsConstructor;
|
|
4
|
+
import org.springframework.context.annotation.Bean;
|
|
5
|
+
import org.springframework.context.annotation.Configuration;
|
|
6
|
+
import org.springframework.security.authentication.AuthenticationManager;
|
|
7
|
+
import org.springframework.security.authentication.AuthenticationProvider;
|
|
8
|
+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
|
9
|
+
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
|
10
|
+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
|
11
|
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
12
|
+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
13
|
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
14
|
+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
15
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
16
|
+
import org.springframework.security.web.SecurityFilterChain;
|
|
17
|
+
|
|
18
|
+
@Configuration
|
|
19
|
+
@EnableWebSecurity
|
|
20
|
+
@EnableMethodSecurity
|
|
21
|
+
@RequiredArgsConstructor
|
|
22
|
+
public class SecurityConfig {
|
|
23
|
+
|
|
24
|
+
private final UserDetailsService userDetailsService;
|
|
25
|
+
|
|
26
|
+
@Bean
|
|
27
|
+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
|
28
|
+
http
|
|
29
|
+
.authorizeHttpRequests(authorize -> authorize
|
|
30
|
+
.requestMatchers("/", "/login", "/register", "/css/**", "/js/**", "/assets/**", "/favicon.ico").permitAll()
|
|
31
|
+
<%_ if (hasSwagger) { _%>
|
|
32
|
+
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
|
|
33
|
+
<%_ } _%>
|
|
34
|
+
.requestMatchers("/actuator/**").permitAll()
|
|
35
|
+
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
|
|
36
|
+
.anyRequest().authenticated()
|
|
37
|
+
)
|
|
38
|
+
.formLogin(form -> form
|
|
39
|
+
.loginPage("/login")
|
|
40
|
+
.loginProcessingUrl("/login")
|
|
41
|
+
.defaultSuccessUrl("/dashboard", true)
|
|
42
|
+
.failureUrl("/login?error=true")
|
|
43
|
+
.permitAll()
|
|
44
|
+
)
|
|
45
|
+
.logout(logout -> logout
|
|
46
|
+
.logoutUrl("/logout")
|
|
47
|
+
.logoutSuccessUrl("/")
|
|
48
|
+
.invalidateHttpSession(true)
|
|
49
|
+
.deleteCookies("JSESSIONID")
|
|
50
|
+
.permitAll()
|
|
51
|
+
)
|
|
52
|
+
.authenticationProvider(authenticationProvider());
|
|
53
|
+
|
|
54
|
+
return http.build();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Bean
|
|
58
|
+
public AuthenticationProvider authenticationProvider() {
|
|
59
|
+
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
|
|
60
|
+
authProvider.setPasswordEncoder(passwordEncoder());
|
|
61
|
+
return authProvider;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Bean
|
|
65
|
+
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
|
66
|
+
return config.getAuthenticationManager();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Bean
|
|
70
|
+
public PasswordEncoder passwordEncoder() {
|
|
71
|
+
return new BCryptPasswordEncoder();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package <%= packageName %>.config;
|
|
2
|
+
|
|
3
|
+
import io.swagger.v3.oas.models.Components;
|
|
4
|
+
import io.swagger.v3.oas.models.OpenAPI;
|
|
5
|
+
import io.swagger.v3.oas.models.info.Info;
|
|
6
|
+
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
|
7
|
+
import io.swagger.v3.oas.models.security.SecurityScheme;
|
|
8
|
+
import org.springframework.context.annotation.Bean;
|
|
9
|
+
import org.springframework.context.annotation.Configuration;
|
|
10
|
+
|
|
11
|
+
@Configuration
|
|
12
|
+
public class SwaggerConfig {
|
|
13
|
+
|
|
14
|
+
@Bean
|
|
15
|
+
public OpenAPI customOpenAPI() {
|
|
16
|
+
final String securitySchemeName = "bearerAuth";
|
|
17
|
+
return new OpenAPI()
|
|
18
|
+
.info(new Info().title("<%= projectName %> API").version("1.0.0").description("<%= description %>"))
|
|
19
|
+
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
|
|
20
|
+
.components(
|
|
21
|
+
new Components()
|
|
22
|
+
.addSecuritySchemes(securitySchemeName,
|
|
23
|
+
new SecurityScheme()
|
|
24
|
+
.name(securitySchemeName)
|
|
25
|
+
.type(SecurityScheme.Type.HTTP)
|
|
26
|
+
.scheme("bearer")
|
|
27
|
+
.bearerFormat("JWT")
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package <%= packageName %>.controller;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.entity.Role;
|
|
4
|
+
import <%= packageName %>.entity.User;
|
|
5
|
+
import <%= packageName %>.repository.UserRepository;
|
|
6
|
+
import lombok.RequiredArgsConstructor;
|
|
7
|
+
import org.springframework.http.ResponseEntity;
|
|
8
|
+
import org.springframework.security.access.prepost.PreAuthorize;
|
|
9
|
+
import org.springframework.web.bind.annotation.*;
|
|
10
|
+
|
|
11
|
+
import java.util.List;
|
|
12
|
+
import java.util.Map;
|
|
13
|
+
import java.util.Set;
|
|
14
|
+
|
|
15
|
+
@RestController
|
|
16
|
+
@RequestMapping("/api/v1/admin")
|
|
17
|
+
@RequiredArgsConstructor
|
|
18
|
+
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
|
19
|
+
public class AdminController {
|
|
20
|
+
|
|
21
|
+
private final UserRepository userRepository;
|
|
22
|
+
|
|
23
|
+
/** GET /api/v1/admin/users — list all users */
|
|
24
|
+
@GetMapping("/users")
|
|
25
|
+
public ResponseEntity<List<User>> listUsers() {
|
|
26
|
+
return ResponseEntity.ok(userRepository.findAll());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** GET /api/v1/admin/users/{id} — get single user */
|
|
30
|
+
@GetMapping("/users/{id}")
|
|
31
|
+
public ResponseEntity<User> getUser(@PathVariable Long id) {
|
|
32
|
+
return userRepository.findById(id)
|
|
33
|
+
.map(ResponseEntity::ok)
|
|
34
|
+
.orElse(ResponseEntity.notFound().build());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** PUT /api/v1/admin/users/{id}/role — assign role */
|
|
38
|
+
@PutMapping("/users/{id}/role")
|
|
39
|
+
public ResponseEntity<Map<String, String>> assignRole(
|
|
40
|
+
@PathVariable Long id,
|
|
41
|
+
@RequestBody Map<String, String> body) {
|
|
42
|
+
|
|
43
|
+
String roleName = body.get("role");
|
|
44
|
+
Role role;
|
|
45
|
+
try {
|
|
46
|
+
role = Role.valueOf(roleName);
|
|
47
|
+
} catch (IllegalArgumentException e) {
|
|
48
|
+
return ResponseEntity.badRequest()
|
|
49
|
+
.body(Map.of("error", "Invalid role: " + roleName + ". Use ROLE_USER or ROLE_ADMIN"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return userRepository.findById(id).map(user -> {
|
|
53
|
+
user.setRoles(Set.of(role));
|
|
54
|
+
userRepository.save(user);
|
|
55
|
+
return ResponseEntity.ok(Map.of("message", "Role updated to " + roleName));
|
|
56
|
+
}).orElse(ResponseEntity.notFound().build());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** DELETE /api/v1/admin/users/{id} — delete user */
|
|
60
|
+
@DeleteMapping("/users/{id}")
|
|
61
|
+
public ResponseEntity<Map<String, String>> deleteUser(@PathVariable Long id) {
|
|
62
|
+
if (!userRepository.existsById(id)) {
|
|
63
|
+
return ResponseEntity.notFound().build();
|
|
64
|
+
}
|
|
65
|
+
userRepository.deleteById(id);
|
|
66
|
+
return ResponseEntity.ok(Map.of("message", "User deleted"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** GET /api/v1/admin/stats — quick dashboard stats */
|
|
70
|
+
@GetMapping("/stats")
|
|
71
|
+
public ResponseEntity<Map<String, Long>> stats() {
|
|
72
|
+
long total = userRepository.count();
|
|
73
|
+
long admins = userRepository.findAll().stream()
|
|
74
|
+
.filter(User::isAdmin)
|
|
75
|
+
.count();
|
|
76
|
+
return ResponseEntity.ok(Map.of(
|
|
77
|
+
"totalUsers", total,
|
|
78
|
+
"admins", admins,
|
|
79
|
+
"regularUsers", total - admins
|
|
80
|
+
));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package <%= packageName %>.controller;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.dto.AuthRequest;
|
|
4
|
+
import <%= packageName %>.dto.AuthResponse;
|
|
5
|
+
import <%= packageName %>.dto.RegisterRequest;
|
|
6
|
+
import <%= packageName %>.entity.Role;
|
|
7
|
+
import <%= packageName %>.entity.User;
|
|
8
|
+
import <%= packageName %>.repository.UserRepository;
|
|
9
|
+
import <%= packageName %>.service.JwtService;
|
|
10
|
+
import jakarta.validation.Valid;
|
|
11
|
+
import lombok.RequiredArgsConstructor;
|
|
12
|
+
import org.springframework.http.ResponseEntity;
|
|
13
|
+
import org.springframework.security.authentication.AuthenticationManager;
|
|
14
|
+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
15
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
16
|
+
import org.springframework.web.bind.annotation.*;
|
|
17
|
+
|
|
18
|
+
import java.util.HashSet;
|
|
19
|
+
import java.util.Set;
|
|
20
|
+
|
|
21
|
+
@RestController
|
|
22
|
+
@RequestMapping("/api/v1/auth")
|
|
23
|
+
@RequiredArgsConstructor
|
|
24
|
+
public class AuthController {
|
|
25
|
+
|
|
26
|
+
private final UserRepository userRepository;
|
|
27
|
+
private final PasswordEncoder passwordEncoder;
|
|
28
|
+
private final JwtService jwtService;
|
|
29
|
+
private final AuthenticationManager authenticationManager;
|
|
30
|
+
|
|
31
|
+
@PostMapping("/register")
|
|
32
|
+
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
|
33
|
+
if (userRepository.existsByEmail(request.getEmail())) {
|
|
34
|
+
return ResponseEntity.badRequest().build();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// First registered user becomes ADMIN, everyone else is USER
|
|
38
|
+
boolean isFirstUser = userRepository.count() == 0;
|
|
39
|
+
Set<Role> roles = isFirstUser
|
|
40
|
+
? new HashSet<>(Set.of(Role.ROLE_ADMIN, Role.ROLE_USER))
|
|
41
|
+
: new HashSet<>(Set.of(Role.ROLE_USER));
|
|
42
|
+
|
|
43
|
+
var user = User.builder()
|
|
44
|
+
.firstName(request.getFirstName())
|
|
45
|
+
.lastName(request.getLastName())
|
|
46
|
+
.email(request.getEmail())
|
|
47
|
+
.password(passwordEncoder.encode(request.getPassword()))
|
|
48
|
+
.roles(roles)
|
|
49
|
+
.build();
|
|
50
|
+
|
|
51
|
+
userRepository.save(user);
|
|
52
|
+
var jwtToken = jwtService.generateToken(user);
|
|
53
|
+
|
|
54
|
+
return ResponseEntity.ok(AuthResponse.builder()
|
|
55
|
+
.token(jwtToken)
|
|
56
|
+
.email(user.getEmail())
|
|
57
|
+
.firstName(user.getFirstName())
|
|
58
|
+
.lastName(user.getLastName())
|
|
59
|
+
.role(isFirstUser ? "ROLE_ADMIN" : "ROLE_USER")
|
|
60
|
+
.build());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@PostMapping("/login")
|
|
64
|
+
public ResponseEntity<AuthResponse> login(@Valid @RequestBody AuthRequest request) {
|
|
65
|
+
authenticationManager.authenticate(
|
|
66
|
+
new UsernamePasswordAuthenticationToken(
|
|
67
|
+
request.getEmail(),
|
|
68
|
+
request.getPassword()
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
var user = userRepository.findByEmail(request.getEmail())
|
|
73
|
+
.orElseThrow();
|
|
74
|
+
|
|
75
|
+
var jwtToken = jwtService.generateToken(user);
|
|
76
|
+
String primaryRole = user.isAdmin() ? "ROLE_ADMIN" : "ROLE_USER";
|
|
77
|
+
|
|
78
|
+
return ResponseEntity.ok(AuthResponse.builder()
|
|
79
|
+
.token(jwtToken)
|
|
80
|
+
.email(user.getEmail())
|
|
81
|
+
.firstName(user.getFirstName())
|
|
82
|
+
.lastName(user.getLastName())
|
|
83
|
+
.role(primaryRole)
|
|
84
|
+
.build());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package <%= packageName %>.controller;
|
|
2
|
+
|
|
3
|
+
import lombok.RequiredArgsConstructor;
|
|
4
|
+
<%_ if (isFullstack && isJwtAuth) { _%>
|
|
5
|
+
import org.springframework.stereotype.Controller;
|
|
6
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
7
|
+
|
|
8
|
+
@Controller
|
|
9
|
+
@RequiredArgsConstructor
|
|
10
|
+
public class HomeController {
|
|
11
|
+
|
|
12
|
+
/** Serve the landing page */
|
|
13
|
+
@GetMapping("/")
|
|
14
|
+
public String home() {
|
|
15
|
+
return "forward:/index.html";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Each page is standalone — just permit static file serving */
|
|
19
|
+
@GetMapping({"/login", "/register"})
|
|
20
|
+
public String authPages() {
|
|
21
|
+
return "forward:/index.html";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
<%_ } else if (isFullstack && isSessionAuth) { _%>
|
|
25
|
+
import org.springframework.stereotype.Controller;
|
|
26
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
27
|
+
|
|
28
|
+
@Controller
|
|
29
|
+
@RequiredArgsConstructor
|
|
30
|
+
public class HomeController {
|
|
31
|
+
|
|
32
|
+
@GetMapping("/")
|
|
33
|
+
public String home() {
|
|
34
|
+
return "redirect:/dashboard";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
<%_ } else { _%>
|
|
38
|
+
import org.springframework.http.ResponseEntity;
|
|
39
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
40
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
41
|
+
|
|
42
|
+
import java.time.LocalDateTime;
|
|
43
|
+
import java.util.LinkedHashMap;
|
|
44
|
+
import java.util.Map;
|
|
45
|
+
|
|
46
|
+
@RestController
|
|
47
|
+
@RequiredArgsConstructor
|
|
48
|
+
public class HomeController {
|
|
49
|
+
|
|
50
|
+
@GetMapping("/")
|
|
51
|
+
public ResponseEntity<Map<String, Object>> home() {
|
|
52
|
+
Map<String, Object> info = new LinkedHashMap<>();
|
|
53
|
+
info.put("status", "UP");
|
|
54
|
+
info.put("app", "<%= projectName %>");
|
|
55
|
+
info.put("version", "1.0.0");
|
|
56
|
+
info.put("auth", "JWT");
|
|
57
|
+
info.put("docs", "/swagger-ui/index.html");
|
|
58
|
+
info.put("health", "/actuator/health");
|
|
59
|
+
info.put("time", LocalDateTime.now().toString());
|
|
60
|
+
return ResponseEntity.ok(info);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
<%_ } _%>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package <%= packageName %>.controller;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.dto.ProfileUpdateRequest;
|
|
4
|
+
import <%= packageName %>.entity.User;
|
|
5
|
+
import <%= packageName %>.repository.UserRepository;
|
|
6
|
+
import lombok.RequiredArgsConstructor;
|
|
7
|
+
import org.springframework.http.ResponseEntity;
|
|
8
|
+
import org.springframework.security.access.prepost.PreAuthorize;
|
|
9
|
+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
|
10
|
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
11
|
+
import org.springframework.web.bind.annotation.*;
|
|
12
|
+
|
|
13
|
+
import java.util.Map;
|
|
14
|
+
|
|
15
|
+
@RestController
|
|
16
|
+
@RequestMapping("/api/v1/profile")
|
|
17
|
+
@RequiredArgsConstructor
|
|
18
|
+
@PreAuthorize("isAuthenticated()")
|
|
19
|
+
public class ProfileController {
|
|
20
|
+
|
|
21
|
+
private final UserRepository userRepository;
|
|
22
|
+
private final PasswordEncoder passwordEncoder;
|
|
23
|
+
|
|
24
|
+
/** GET /api/v1/profile — get own profile */
|
|
25
|
+
@GetMapping
|
|
26
|
+
public ResponseEntity<User> getProfile(@AuthenticationPrincipal User user) {
|
|
27
|
+
return ResponseEntity.ok(user);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** PUT /api/v1/profile — update name, bio, avatarUrl */
|
|
31
|
+
@PutMapping
|
|
32
|
+
public ResponseEntity<User> updateProfile(
|
|
33
|
+
@AuthenticationPrincipal User user,
|
|
34
|
+
@RequestBody ProfileUpdateRequest request) {
|
|
35
|
+
|
|
36
|
+
if (request.getFirstName() != null) user.setFirstName(request.getFirstName());
|
|
37
|
+
if (request.getLastName() != null) user.setLastName(request.getLastName());
|
|
38
|
+
if (request.getBio() != null) user.setBio(request.getBio());
|
|
39
|
+
if (request.getAvatarUrl() != null) user.setAvatarUrl(request.getAvatarUrl());
|
|
40
|
+
|
|
41
|
+
userRepository.save(user);
|
|
42
|
+
return ResponseEntity.ok(user);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** PUT /api/v1/profile/password — change own password */
|
|
46
|
+
@PutMapping("/password")
|
|
47
|
+
public ResponseEntity<Map<String, String>> changePassword(
|
|
48
|
+
@AuthenticationPrincipal User user,
|
|
49
|
+
@RequestBody ProfileUpdateRequest request) {
|
|
50
|
+
|
|
51
|
+
if (request.getCurrentPassword() == null || request.getNewPassword() == null) {
|
|
52
|
+
return ResponseEntity.badRequest()
|
|
53
|
+
.body(Map.of("error", "currentPassword and newPassword are required"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
|
|
57
|
+
return ResponseEntity.status(403)
|
|
58
|
+
.body(Map.of("error", "Current password is incorrect"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
|
62
|
+
userRepository.save(user);
|
|
63
|
+
return ResponseEntity.ok(Map.of("message", "Password updated successfully"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package <%= packageName %>.controller;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.entity.User;
|
|
4
|
+
import <%= packageName %>.service.UserService;
|
|
5
|
+
import lombok.RequiredArgsConstructor;
|
|
6
|
+
import org.springframework.http.ResponseEntity;
|
|
7
|
+
import org.springframework.web.bind.annotation.*;
|
|
8
|
+
|
|
9
|
+
import java.util.List;
|
|
10
|
+
|
|
11
|
+
@RestController
|
|
12
|
+
@RequestMapping("/api/v1/users")
|
|
13
|
+
@RequiredArgsConstructor
|
|
14
|
+
public class UserController {
|
|
15
|
+
|
|
16
|
+
private final UserService userService;
|
|
17
|
+
|
|
18
|
+
@GetMapping
|
|
19
|
+
public ResponseEntity<List<User>> getAllUsers() {
|
|
20
|
+
return ResponseEntity.ok(userService.getAllUsers());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@GetMapping("/{id}")
|
|
24
|
+
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
|
25
|
+
return userService.getUserById(id)
|
|
26
|
+
.map(ResponseEntity::ok)
|
|
27
|
+
.orElse(ResponseEntity.notFound().build());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@DeleteMapping("/{id}")
|
|
31
|
+
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
|
32
|
+
userService.deleteUser(id);
|
|
33
|
+
return ResponseEntity.noContent().build();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package <%= packageName %>.dto;
|
|
2
|
+
|
|
3
|
+
import lombok.AllArgsConstructor;
|
|
4
|
+
import lombok.Builder;
|
|
5
|
+
import lombok.Data;
|
|
6
|
+
import lombok.NoArgsConstructor;
|
|
7
|
+
|
|
8
|
+
@Data
|
|
9
|
+
@Builder
|
|
10
|
+
@AllArgsConstructor
|
|
11
|
+
@NoArgsConstructor
|
|
12
|
+
public class AuthRequest {
|
|
13
|
+
private String email;
|
|
14
|
+
private String password;
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package <%= packageName %>.dto;
|
|
2
|
+
|
|
3
|
+
import lombok.AllArgsConstructor;
|
|
4
|
+
import lombok.Builder;
|
|
5
|
+
import lombok.Data;
|
|
6
|
+
import lombok.NoArgsConstructor;
|
|
7
|
+
|
|
8
|
+
@Data
|
|
9
|
+
@Builder
|
|
10
|
+
@AllArgsConstructor
|
|
11
|
+
@NoArgsConstructor
|
|
12
|
+
public class AuthResponse {
|
|
13
|
+
private String token;
|
|
14
|
+
private String email;
|
|
15
|
+
private String firstName;
|
|
16
|
+
private String lastName;
|
|
17
|
+
private String role;
|
|
18
|
+
}
|