Skip to content

Commit 3cfa8b0

Browse files
authored
feat: admin api and roles (#230)
1 parent 5f58441 commit 3cfa8b0

File tree

14 files changed

+264
-103
lines changed

14 files changed

+264
-103
lines changed

src/main/java/com/sterul/opencookbookapiserver/configurations/security/WebSecurityConfiguration.java

Lines changed: 88 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -31,93 +31,93 @@
3131
@EnableMethodSecurity(prePostEnabled = true)
3232
public class WebSecurityConfiguration {
3333

34-
private static final List<String> AUTH_WHITELIST = Arrays.asList(
35-
"/api/v1/users/signup",
36-
"/api/v1/users/activate",
37-
"/api/v1/users/resendActivationLink",
38-
"/api/v1/users/requestPasswordReset",
39-
"/api/v1/users/resetPassword",
40-
"/api/v1/users/login",
41-
"/api/v1/users/refreshToken",
42-
"/swagger-ui/*",
43-
"/v3/api-docs/*",
44-
"/api-docs*",
45-
"/api-docs",
46-
"/api-docs/*",
47-
"/api-docs/*/*",
48-
"/api/v1/instance*",
49-
"/api/v1/bringexport*",
50-
"/h2-console/*",
51-
"/error",
52-
"/actuator/health");
53-
@Autowired
54-
private UserDetailsService userDetailsService;
55-
@Autowired
56-
private UnauthorizedEntryPoint unauthorizedEntryPoint;
57-
@Autowired
58-
private JwtRequestFilter jwtRequestFilter;
59-
@Autowired
60-
private PasswordEncoder passwordEncoder;
61-
62-
@Autowired
63-
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
64-
// Configure service to check if credentials are valid
65-
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
66-
}
67-
68-
private RequestMatcher allowedPathRequestMatcher() {
69-
return (HttpServletRequest request) -> AUTH_WHITELIST.stream()
70-
.anyMatch(whitelistedUrl -> new AntPathRequestMatcher(whitelistedUrl).matches(request));
71-
72-
};
73-
74-
@Bean
75-
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
76-
77-
// Cors and csrf not needed in an api server
78-
http.cors(configurer -> configurer.configurationSource(c -> allowAllCorsConfig()));
79-
http.csrf(conf -> conf.disable());
80-
81-
// Allow frames needed for h2 console
82-
http.headers(config -> config.frameOptions(options -> options.sameOrigin()));
83-
84-
// Permit whitelist and authenticated request
85-
http.authorizeHttpRequests(
86-
authorize -> authorize.requestMatchers(allowedPathRequestMatcher()).permitAll()
87-
.anyRequest().authenticated());
88-
89-
http.exceptionHandling(configurer -> configurer
90-
.authenticationEntryPoint(unauthorizedEntryPoint));
91-
92-
// Disable sessions since auth/session token is passed in every request
93-
http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
94-
95-
// Add a filter to check the sent token and authenticate
96-
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
97-
98-
return http.build();
99-
}
100-
101-
private CorsConfiguration allowAllCorsConfig() {
102-
List<String> permittedCorsMethods = Collections.unmodifiableList(Arrays.asList(
103-
HttpMethod.GET.name(),
104-
HttpMethod.HEAD.name(),
105-
HttpMethod.POST.name(),
106-
HttpMethod.PUT.name(),
107-
HttpMethod.DELETE.name()));
108-
109-
var corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
110-
corsConfiguration.setAllowedMethods(permittedCorsMethods);
111-
return corsConfiguration;
112-
113-
}
114-
115-
@Bean
116-
public AuthenticationManager authenticationManager(HttpSecurity http)
117-
throws Exception {
118-
return http.getSharedObject(AuthenticationManagerBuilder.class)
119-
.userDetailsService(userDetailsService)
120-
.passwordEncoder(passwordEncoder).and().build();
121-
}
34+
private static final List<String> AUTH_WHITELIST = Arrays.asList(
35+
"/api/v1/users/signup",
36+
"/api/v1/users/activate",
37+
"/api/v1/users/resendActivationLink",
38+
"/api/v1/users/requestPasswordReset",
39+
"/api/v1/users/resetPassword",
40+
"/api/v1/users/login",
41+
"/api/v1/users/refreshToken",
42+
"/swagger-ui/*",
43+
"/v3/api-docs/*",
44+
"/api-docs*",
45+
"/api-docs",
46+
"/api-docs/*",
47+
"/api-docs/*/*",
48+
"/api/v1/instance*",
49+
"/api/v1/bringexport*",
50+
"/h2-console/*",
51+
"/error",
52+
"/actuator/health");
53+
@Autowired
54+
private UserDetailsService userDetailsService;
55+
@Autowired
56+
private UnauthorizedEntryPoint unauthorizedEntryPoint;
57+
@Autowired
58+
private JwtRequestFilter jwtRequestFilter;
59+
@Autowired
60+
private PasswordEncoder passwordEncoder;
61+
62+
@Autowired
63+
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
64+
// Configure service to check if credentials are valid
65+
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
66+
}
67+
68+
private RequestMatcher allowedPathRequestMatcher() {
69+
return (HttpServletRequest request) -> AUTH_WHITELIST.stream()
70+
.anyMatch(whitelistedUrl -> new AntPathRequestMatcher(whitelistedUrl).matches(request));
71+
72+
};
73+
74+
@Bean
75+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
76+
77+
// Cors and csrf not needed in an api server
78+
http.cors(configurer -> configurer.configurationSource(c -> allowAllCorsConfig()));
79+
http.csrf(conf -> conf.disable());
80+
81+
// Allow frames needed for h2 console
82+
http.headers(config -> config.frameOptions(options -> options.sameOrigin()));
83+
84+
// Permit whitelist and authenticated request
85+
http.authorizeHttpRequests(
86+
authorize -> authorize.requestMatchers(allowedPathRequestMatcher()).permitAll()
87+
.anyRequest().authenticated());
88+
89+
http.exceptionHandling(configurer -> configurer
90+
.authenticationEntryPoint(unauthorizedEntryPoint));
91+
92+
// Disable sessions since auth/session token is passed in every request
93+
http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
94+
95+
// Add a filter to check the sent token and authenticate
96+
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
97+
98+
return http.build();
99+
}
100+
101+
private CorsConfiguration allowAllCorsConfig() {
102+
List<String> permittedCorsMethods = Collections.unmodifiableList(Arrays.asList(
103+
HttpMethod.GET.name(),
104+
HttpMethod.HEAD.name(),
105+
HttpMethod.POST.name(),
106+
HttpMethod.PUT.name(),
107+
HttpMethod.DELETE.name()));
108+
109+
var corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
110+
corsConfiguration.setAllowedMethods(permittedCorsMethods);
111+
return corsConfiguration;
112+
113+
}
114+
115+
@Bean
116+
public AuthenticationManager authenticationManager(HttpSecurity http)
117+
throws Exception {
118+
return http.getSharedObject(AuthenticationManagerBuilder.class)
119+
.userDetailsService(userDetailsService)
120+
.passwordEncoder(passwordEncoder).and().build();
121+
}
122122

123123
}

src/main/java/com/sterul/opencookbookapiserver/controllers/UserController.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.security.authentication.BadCredentialsException;
88
import org.springframework.security.authentication.DisabledException;
99
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10+
import org.springframework.security.core.context.SecurityContextHolder;
1011
import org.springframework.security.core.userdetails.UserDetails;
1112
import org.springframework.transaction.annotation.Transactional;
1213
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -75,8 +76,10 @@ public class UserController extends BaseController {
7576
@Operation(summary = "Creates a new user")
7677
@PostMapping("/signup")
7778
@Transactional
78-
public CookpalUser signup(@Valid @RequestBody UserCreationRequest userCreationRequest) throws UserAlreadyExistsException {
79-
var createdUser = userService.createUser(userCreationRequest.getEmailAddress(), userCreationRequest.getPassword());
79+
public CookpalUser signup(@Valid @RequestBody UserCreationRequest userCreationRequest)
80+
throws UserAlreadyExistsException {
81+
var createdUser = userService.createUser(userCreationRequest.getEmailAddress(),
82+
userCreationRequest.getPassword());
8083
var activationLink = userService.createActivationLink(createdUser);
8184
try {
8285
emailService.sendActivationMail(activationLink);
@@ -173,8 +176,11 @@ public ResponseEntity<String> resendActivationLink(@Valid @RequestBody ResendAct
173176
@Operation(summary = "Get information about authenticated user account")
174177
@GetMapping("/self")
175178
public UserInfoResponse getOwnUserInfo() {
179+
var user = getLoggedInUser();
176180
var response = new UserInfoResponse();
177-
response.setEmail(getLoggedInUser().getEmailAddress());
181+
response.setEmail(user.getEmailAddress());
182+
response.setRoles(SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
183+
.map(authority -> authority.getAuthority()).toList());
178184
return response;
179185
}
180186

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.sterul.opencookbookapiserver.controllers.admin;
2+
3+
import java.util.List;
4+
5+
import org.springframework.security.access.prepost.PreAuthorize;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import com.sterul.opencookbookapiserver.entities.Ingredient;
11+
import com.sterul.opencookbookapiserver.services.IngredientService;
12+
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
@RestController
17+
@RequestMapping("/api/v1/admin/ingredients")
18+
@Tag(name = "Users", description = "Admin ingredient api")
19+
@Slf4j
20+
public class AdminIngredientsController {
21+
22+
private IngredientService ingredientService;
23+
24+
public AdminIngredientsController(IngredientService ingredientService) {
25+
this.ingredientService = ingredientService;
26+
}
27+
@GetMapping
28+
@PreAuthorize("hasAuthority('ADMIN')")
29+
public List<Ingredient> getAll() {
30+
log.info("Admin: Accessing all ingredients");
31+
return ingredientService.getAllIngredients();
32+
}
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.sterul.opencookbookapiserver.controllers.admin;
2+
3+
import java.util.List;
4+
5+
import org.springframework.security.access.prepost.PreAuthorize;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import com.sterul.opencookbookapiserver.entities.recipe.Recipe;
11+
import com.sterul.opencookbookapiserver.services.RecipeService;
12+
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
@RestController
17+
@RequestMapping("/api/v1/admin/recipes")
18+
@Tag(name = "Users", description = "Admin recipe api")
19+
@Slf4j
20+
public class AdminRecipeController {
21+
22+
private RecipeService recipeService;
23+
24+
public AdminRecipeController(RecipeService recipeService) {
25+
this.recipeService = recipeService;
26+
}
27+
28+
@GetMapping
29+
@PreAuthorize("hasAuthority('ADMIN')")
30+
public List<Recipe> getAll() {
31+
log.info("Admin: Accessing all recipes");
32+
return recipeService.getAllRecipes();
33+
}
34+
@GetMapping("/count")
35+
@PreAuthorize("hasAuthority('ADMIN')")
36+
public List<Recipe> getCount() {
37+
log.info("Admin: Accessing recipe count");
38+
return recipeService.getAllRecipes();
39+
}
40+
41+
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.sterul.opencookbookapiserver.controllers.admin;
2+
3+
import java.util.List;
4+
5+
import org.springframework.security.access.prepost.PreAuthorize;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import com.sterul.opencookbookapiserver.entities.account.CookpalUser;
11+
import com.sterul.opencookbookapiserver.services.UserService;
12+
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
@RestController
17+
@RequestMapping("/api/v1/admin/users")
18+
@Tag(name = "Users", description = "Admin user api")
19+
@Slf4j
20+
public class AdminUserController {
21+
22+
private UserService userService;
23+
24+
public AdminUserController(UserService userService) {
25+
this.userService = userService;
26+
}
27+
28+
@GetMapping
29+
@PreAuthorize("hasAuthority('ADMIN')")
30+
public List<CookpalUser> getAll() {
31+
log.info("Admin: Accessing all users");
32+
return userService.getAllUsers();
33+
}
34+
35+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.sterul.opencookbookapiserver.controllers.responses;
22

3+
import java.util.List;
4+
35
import lombok.Data;
46

57
@Data
68
public class UserInfoResponse {
79
String email;
10+
List<String> roles;
811
}

src/main/java/com/sterul/opencookbookapiserver/entities/account/CookpalUser.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.sterul.opencookbookapiserver.entities.AuditableEntity;
55

66
import jakarta.persistence.Entity;
7+
import jakarta.persistence.EnumType;
8+
import jakarta.persistence.Enumerated;
79
import jakarta.persistence.GeneratedValue;
810
import jakarta.persistence.Id;
911
import jakarta.persistence.SequenceGenerator;
@@ -28,6 +30,9 @@ public class CookpalUser extends AuditableEntity {
2830

2931
private boolean activated;
3032

33+
@Enumerated(EnumType.STRING)
34+
private Role roles;
35+
3136
@Override
3237
public String toString() {
3338
return getUserId() + " " + getEmailAddress();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.sterul.opencookbookapiserver.entities.account;
2+
3+
public enum Role {
4+
ADMIN,
5+
}

src/main/java/com/sterul/opencookbookapiserver/services/RecipeService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import java.util.Arrays;
77
import java.util.List;
88

9-
import jakarta.transaction.Transactional;
10-
119
import org.springframework.beans.factory.annotation.Autowired;
1210
import org.springframework.context.annotation.Lazy;
1311
import org.springframework.stereotype.Service;
@@ -21,6 +19,7 @@
2119
import com.sterul.opencookbookapiserver.repositories.RecipeRepository;
2220
import com.sterul.opencookbookapiserver.services.exceptions.ElementNotFound;
2321

22+
import jakarta.transaction.Transactional;
2423
import lombok.extern.slf4j.Slf4j;
2524

2625
@Service
@@ -186,4 +185,12 @@ private List<Recipe> searchByStringAndType(CookpalUser user, String searchString
186185
.get())
187186
.toList();
188187
}
188+
189+
public List<Recipe> getAllRecipes() {
190+
return recipeRepository.findAll();
191+
}
192+
193+
public long getRecipeCount() {
194+
return recipeRepository.count();
195+
}
189196
}

0 commit comments

Comments
 (0)