Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • xpokorn8/sprachschulsystem
1 result
Show changes
Commits on Source (43)
Showing
with 353 additions and 131 deletions
......@@ -71,10 +71,41 @@ System has three authorization roles - **Lecturer**, **Student** and **Admin**.
## Module services
- **module-language-school**
- **module-certificate**
- **module-exercice**
- **module-mail**
- **module-language-school** (port 5000)
- **module-certificate** (port 5001)
- **module-exercise** (port 5002)
- **module-mail** (port 5003)
# System Scenarios
## Enrollment and Lecture Selection Process
A lecturer authenticates into the system, creates a few courses with arbitrary names, capacities, languages and proficiencies, and adds themself to those courses. The lecturer then creates corresponding lectures for those courses, assigning them to specific date-times and appointing them a topic and capacity.
Meanwhile, an arbitrary number of students authenticate into the system and access a list of available courses for enrollment. They browse through the list of courses, read the descriptions, and select the ones they are interested in. Once they choose, they try to enrol in those courses. The system checks if any seats are left, and if there are, the students are successfully enrolled.
After enrolling in the courses, the students browse through the list of lectures for their enrolled courses and select the ones they would like to attend. They check the dates and times of the lectures, and if they find a suitable one, they enrol in that lecture. Once again, the system checks if there are any available seats for the selected lectures and if there are, the students are successfully enrolled.
_**tldr;**_ scenario mimics the _cliché_ school system lecture/course enrollment process, where students _fight_ against others for any available seats in courses and lectures they are interested in. It also demonstrates the ability of the system to handle multiple users simultaneously, ensuring that the enrollment process is seamless and hassle-free.
- Prerequisite is having locust installed - please refer to documentation https://docs.locust.io/en/stable/installation.html
- Starting locust (while running all system modules):
~~~console
cd ./application
locust
~~~
- Navigate to http://localhost:8089
- Enter following data for basic showcasing the app usage
- Number of users: 3
- Spawn rate: 1
- Host: http://localhost:8081
- Token: enter token retrieved from http://localhost:8080/token (see corresponding section of this readme)
- Enter following data for simulating high system load
- Number of users: 60 (or try more)
- Spawn rate: 5
- Host: http://localhost:8081
- Token: enter token retrieved from http://localhost:8080/token (see corresponding section of this readme)
- Switch tabs to explore the current state of the API
# Diagrams
......
FROM docker.io/library/eclipse-temurin:17-jre-focal
COPY ./target/confidentialClient-0.0.1-SNAPSHOT.jar /app.jar
ENV DOCKER_RUNNING=true
ENTRYPOINT ["java", "-jar", "/app.jar"]
\ No newline at end of file
package org.fuseri.confidentialclient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthClient {
@GetMapping("/token")
public String getToken( @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oauth2Client) {
return oauth2Client.getAccessToken().getTokenValue();
}
}
package org.fuseri.confidentialclient;
import org.fuseri.model.dto.user.UserCreateDto;
import org.fuseri.model.dto.user.UserDto;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@RestController
public class AuthClientController {
@GetMapping("/token")
public String getToken(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oauth2Client) {
return oauth2Client.getAccessToken().getTokenValue();
}
@GetMapping("/register")
public String register(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oauth2Client, @AuthenticationPrincipal OidcUser user) {
var createDto = new UserCreateDto();
createDto.setLastName(user.getFamilyName());
createDto.setFirstName(user.getGivenName());
createDto.setEmail(user.getSubject()); // MUNI includes as subject user email
createDto.setUsername(user.getPreferredUsername());
// add access token to API call
OAuth2AccessToken accessToken = oauth2Client.getAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken.getTokenValue());
HttpEntity<UserCreateDto> request = new HttpEntity<>(createDto, headers);
String url = buildRegisterUrl();
RestTemplate userRegisterRestTemplate = new RestTemplate();
try {
var response = userRegisterRestTemplate.postForObject(url, request, UserDto.class);
return response == null ? "Unable to register user." : response.toString();
} catch (RestClientException e) {
return "Unable to register user: " + e.getMessage();
}
}
private static String buildRegisterUrl() {
String host;
if (!Boolean.parseBoolean(System.getenv("DOCKER_RUNNING")))
host = "localhost";
else host = switch (System.getProperty("os.name")) {
case "mac os x" -> "docker.for.mac.localhost";
case "windows" -> "host.docker.internal";
default -> "language-school"; // linux and others
};
return "http://" + host + ":8081/users/register";
}
}
package org.fuseri.confidentialclient;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.fuseri.model.dto.user.UserCreateDto;
import org.fuseri.model.dto.user.UserDto;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import java.io.IOException;
@SpringBootApplication
public class ConfidentialClientApplication {
......@@ -30,10 +13,6 @@ public class ConfidentialClientApplication {
SpringApplication.run(ConfidentialClientApplication.class, args);
}
private static String asJsonString(final Object obj) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(obj);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
......@@ -43,47 +22,9 @@ public class ConfidentialClientApplication {
// all other requests must be authenticated
.anyRequest().authenticated()
)
.oauth2Login(x -> x
// our custom handler for successful logins
.successHandler(authenticationSuccessHandler())
)
.oauth2Login()
;
return httpSecurity.build();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new SavedRequestAwareAuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws ServletException, IOException {
var a= res.getHeaderNames();
for (var name : a) {
System.out.println(res.getHeader(name));
}
System.out.println("got here");
if (auth instanceof OAuth2AuthenticationToken token
&& token.getPrincipal() instanceof OidcUser user) {
var createDto = new UserCreateDto();
createDto.setLastName(user.getFamilyName());
createDto.setFirstName(user.getGivenName());
createDto.setEmail(user.getEmail());
createDto.setUsername(user.getPreferredUsername());
//var result = WebClient.builder().baseUrl("http://localhost:8081/users/register").build().post()
//.contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(createDto))
//.retrieve();
//var out = result.bodyToMono(UserDto.class).block();
//System.out.println(out);
}
super.onAuthenticationSuccess(req, res, auth);
}
};
}
}
......@@ -10,6 +10,8 @@ services:
image: xpokorn8/sprachschulsystem:certificate
ports:
- "8082:8082"
environment:
- DOCKER_RUNNING=true
exercise:
build: ./module-exercise
......@@ -17,6 +19,8 @@ services:
image: xpokorn8/sprachschulsystem:exercise
ports:
- "8083:8083"
environment:
- DOCKER_RUNNING=true
language-school:
build: ./module-language-school
......@@ -24,6 +28,8 @@ services:
image: xpokorn8/sprachschulsystem:language-school
ports:
- "8081:8081"
environment:
- DOCKER_RUNNING=true
mail:
build: ./module-mail
......@@ -31,11 +37,15 @@ services:
image: xpokorn8/sprachschulsystem:mail
ports:
- "8084:8084"
environment:
- DOCKER_RUNNING=true
confidential-client:
build: ./confidentialClient
container_name: confidential-client
image: xpokorn8/sprachschulsystem:confidential-client
environment:
- DOCKER_RUNNING=true
ports:
- "8080:8080"
......@@ -45,6 +55,8 @@ services:
volumes:
- ./prometheus:/etc/prometheus
- prometheus_data:/prometheus
environment:
- DOCKER_RUNNING=true
expose:
- 9090
ports:
......@@ -56,6 +68,8 @@ services:
image: grafana/grafana:7.5.7
ports:
- "3000:3000"
environment:
- DOCKER_RUNNING=true
restart: unless-stopped
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
......
import random
from locust import HttpUser, task, between, events
from locust.exception import StopUser
@events.init_command_line_parser.add_listener
def _(parser):
parser.add_argument("--token",
type=str,
env_var="LOCUST_MY_ARGUMENT",
is_secret=True,
help=":8080/token",
default="")
class StudentActor(HttpUser):
wait_time = between(.250, .500)
weight = 1
course_id = None
lecture_id = None
id = None
header = None
def on_start(self):
self.header = {'Authorization': f'Bearer {self.environment.parsed_options.token}'}
response = self.client.post(
url='/users',
auth=None,
headers=self.header,
json={
"username": "xnocni",
"email": f"xnocni{random.randint(0, 42069)}@mail.muni.cz",
"firstName": "Hana",
"lastName": "Sykorova"
},
name="Add User"
)
if response.status_code == 201:
self.id = response.json()["id"]
return super().on_start()
@task(3)
def get_courses_english(self):
response = self.client.get(
url='/courses/findAllByLang?lang=ENGLISH',
auth=None,
headers=self.header,
name="Find Courses"
)
if response.status_code == 200:
json = response.json()
if len(json) > 0:
self.course_id = json[0]["id"]
@task
def enrol(self):
if self.course_id is None or self.id is None:
return
self.client.patch(
url=f'/courses/enrol/{self.course_id}?studentId={self.id}',
auth=None,
headers=self.header,
name="Enrol Courses"
)
@task
def get_lectures_by_course(self):
if self.course_id is None:
return
response = self.client.get(
url=f'/lectures/findByCourse?courseId={self.course_id}',
auth=None,
headers=self.header,
name="Find Lectures"
)
if response.status_code == 200:
json = response.json()
if len(json) > 0:
self.lecture_id = json[0]["id"]
class LecturerActor(HttpUser):
weight = 0
fixed_count = 1
wait_time = between(.100, .900)
courses_created = 0
lecture_created = 0
course_ids = set()
id = None
header = None
def on_start(self):
self.header = {'Authorization': f'Bearer {self.environment.parsed_options.token}'}
response = self.client.post(
url='/users',
auth=None,
headers=self.header,
json={
"username": "xhana",
"email": f"xhana{random.randint(0, 42069)}@mail.muni.cz",
"firstName": "Hana",
"lastName": "Sykorova"
},
name="Add User"
)
if response.status_code == 201:
self.id = response.json()["id"]
return super().on_start()
@task
def new_course(self):
if self.courses_created < 3:
response = self.client.post(
url="/courses",
auth=None,
headers=self.header,
name="Create Course",
json={
"name": "string",
"capacity": 10,
"language": random.choice(["SPANISH", "ENGLISH"]),
"proficiency": random.choice(["A1", "B1", "C1"])
}
)
if response.status_code == 201:
self.course_ids.add(response.json()['id'])
self.courses_created += 1
@task
def new_lecture(self):
if self.courses_created > 0 and self.lecture_created < 10:
response = self.client.post(
url="/lectures",
auth=None,
headers=self.header,
name="Create Lecture",
json={
"lectureFrom": "2024-04-26T18:06:30.658Z",
"lectureTo": "2024-04-27T18:06:30.658Z",
"topic": "string",
"capacity": random.choice([10, 20, 30]),
"courseId": random.choice(list(self.course_ids))
}
)
if response.status_code == 201:
self.lecture_created += 1
if self.lecture_created >= 20:
raise StopUser()
......@@ -26,6 +26,7 @@ import java.util.Map;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@ToString
@NoArgsConstructor
public class UserDto extends DomainObjectDto {
......
FROM docker.io/library/eclipse-temurin:17-jre-focal
COPY ./target/module-certificate-0.0.1-SNAPSHOT.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
ENV DOCKER_RUNNING=true
ENTRYPOINT ["java", "-jar", "/app.jar"]
\ No newline at end of file
......@@ -10,6 +10,7 @@ import jakarta.validation.constraints.NotNull;
import org.fuseri.model.dto.certificate.CertificateCreateDto;
import org.fuseri.model.dto.certificate.CertificateSimpleDto;
import org.fuseri.modulecertificate.ModuleCertificateApplication;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
......@@ -42,7 +43,7 @@ public class CertificateController {
* @param certificateCreateDto Dto with data used for generating certificate
* @return certificate file
*/
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Generate certificate",
description = "Generates certificate, saves it into database and returns certificate file.")
@ApiResponses(value = {
......@@ -61,7 +62,7 @@ public class CertificateController {
* @param id ID of certificate to be retrieved
* @return CertificateDto with data of previously generated certificate with specified ID
*/
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Get a certificate by ID", description = "Returns a certificate with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Certificate with the specified ID retrieved successfully."),
......@@ -80,13 +81,13 @@ public class CertificateController {
* @return List of CertificateDto objects with previously generated certificates
* for specified User.
*/
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Get certificates for user", description = "Returns certificates for given user in list.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved certificates"),
@ApiResponse(responseCode = "500", description = "Internal server error."),
})
@GetMapping("/findForUser")
@GetMapping("/find-for-user")
public ResponseEntity<List<CertificateSimpleDto>> findForUser(@RequestParam Long userId) {
return ResponseEntity.ok(certificateFacade.findByUserId(userId));
}
......@@ -99,7 +100,7 @@ public class CertificateController {
* @return List of CertificateDto objects with previously generated certificates
* for specified User and Course.
*/
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Get certificates for user and course",
description = "Returns certificates for given user and course in list.")
@ApiResponses(value = {
......@@ -107,7 +108,7 @@ public class CertificateController {
@ApiResponse(responseCode = "500", description = "Internal server error."),
@ApiResponse(responseCode = "400", description = "Invalid input."),
})
@GetMapping("/findForUserAndCourse")
@GetMapping("/find-for-user-and-course")
public ResponseEntity<List<CertificateSimpleDto>> findForUserAndCourse(@RequestParam Long userId, @RequestParam Long courseId) {
return ResponseEntity.ok(certificateFacade.findByUserIdAndCourseId(userId, courseId));
}
......@@ -117,7 +118,7 @@ public class CertificateController {
*
* @param id Id of certificate to be deleted.
*/
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Delete a certificate with specified ID", description = "Deletes a certificate with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Certificate with the specified ID deleted successfully."),
......@@ -135,14 +136,14 @@ public class CertificateController {
*
* @return a Result object containing a list of CertificateDto objects and pagination information
*/
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Get certificates in paginated format", description = "Returns certificates in paginated format.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved paginated certificates"),
@ApiResponse(responseCode = "500", description = "Internal server error.")
})
@GetMapping
public ResponseEntity<Page<CertificateSimpleDto>> findAllCertificates(Pageable pageable) {
public ResponseEntity<Page<CertificateSimpleDto>> findAllCertificates(@ParameterObject Pageable pageable) {
return ResponseEntity.ok(certificateFacade.findAll(pageable));
}
}
......@@ -15,16 +15,15 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
public class AppSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable();
httpSecurity.authorizeHttpRequests(x -> x
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.POST, "/certificates/**").hasAuthority("SCOPE_test_1")
.requestMatchers(HttpMethod.DELETE, "/certificates/**").hasAuthority("SCOPE_test_1")
.requestMatchers(HttpMethod.PUT, "/certificates/**").hasAnyAuthority("SCOPE_test_1","SCOPE_test_2")
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/datainitializer").permitAll()
.requestMatchers(HttpMethod.POST, "/certificates/**").hasAnyAuthority( "SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.GET, "/certificates/**").hasAnyAuthority("SCOPE_test_1")
.requestMatchers(HttpMethod.DELETE, "/certificates/**").hasAnyAuthority("SCOPE_test_1")
.anyRequest().authenticated()
).oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken)
;
).oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
return httpSecurity.build();
}
}
......@@ -24,7 +24,7 @@ public class DataInitializerController {
this.dataInitializer = dataInitializer;
}
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1", "test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Seed certificate database",
description = "Seeds certificate database. Drops all data first.")
@ApiResponses(value = {
......@@ -36,7 +36,7 @@ public class DataInitializerController {
return ResponseEntity.noContent().build();
}
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME,scopes = {"test_1", "test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleCertificateApplication.SECURITY_SCHEME_NAME),
summary = "Drop certificate database",
description = "Drops all data from certificate database")
@ApiResponses(value = {
......
......@@ -7,7 +7,6 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.util.UrlPathHelper;
......@@ -41,7 +40,7 @@ public class RestResponseEntityExceptionHandler {
* @param request request
* @return response entity
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<ApiError> handleValidationErrors(MethodArgumentNotValidException ex, HttpServletRequest request) {
List<ApiSubError> subErrors = ex.getBindingResult().getFieldErrors()
.stream()
......@@ -55,6 +54,22 @@ public class RestResponseEntityExceptionHandler {
return buildResponseEntity(error);
}
/**
* Handle MessageNotReadable exceptions
*
* @param ex exception
* @param request request
* @return response entity
*/
@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity<ApiError> handleMessageNotReadableErrors(HttpMessageNotReadableException ex, HttpServletRequest request) {
ApiError error = new ApiError(
HttpStatus.BAD_REQUEST,
ex,
URL_PATH_HELPER.getRequestUri(request));
return buildResponseEntity(error);
}
/**
* Handle exceptions not matched by above handler methods
*
......
......@@ -120,7 +120,7 @@ class CertificateControllerTests {
void findCertificatesForUser() throws Exception {
Mockito.when(certificateFacade.findByUserId(ArgumentMatchers.anyLong())).thenReturn(List.of(certificateDto));
mockMvc.perform(get("/certificates/findForUser").param("userId", "0"))
mockMvc.perform(get("/certificates/find-for-user").param("userId", "0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$").isNotEmpty());
......@@ -129,7 +129,7 @@ class CertificateControllerTests {
@WithMockUser(authorities = {"SCOPE_test_1"})
@Test
void findCertificatesWithoutUserId() throws Exception {
mockMvc.perform(get("/certificates/findForUser"))
mockMvc.perform(get("/certificates/find-for-user"))
.andExpect(status().is5xxServerError());
}
......@@ -140,7 +140,7 @@ class CertificateControllerTests {
ArgumentMatchers.anyLong()))
.thenReturn(List.of(certificateDto));
mockMvc.perform(get("/certificates/findForUserAndCourse")
mockMvc.perform(get("/certificates/find-for-user-and-course")
.param("userId", "0")
.param("courseId", "0"))
.andExpect(status().isOk())
......@@ -151,7 +151,7 @@ class CertificateControllerTests {
@WithMockUser(authorities = {"SCOPE_test_1"})
@Test
void findCertificateIdWithoutUserId() throws Exception {
mockMvc.perform(get("/certificates/findForUserAndCourse")
mockMvc.perform(get("/certificates/find-for-user-and-course")
.param("courseId", "0"))
.andExpect(status().is5xxServerError());
}
......@@ -159,7 +159,7 @@ class CertificateControllerTests {
@WithMockUser(authorities = {"SCOPE_test_1"})
@Test
void findCertificateIdWithoutCourseId() throws Exception {
mockMvc.perform(get("/certificates/findForUserAndCourse")
mockMvc.perform(get("/certificates/find-for-user-and-course")
.param("userId", "0"))
.andExpect(status().is5xxServerError());
}
......@@ -167,7 +167,7 @@ class CertificateControllerTests {
@WithMockUser(authorities = {"SCOPE_test_1"})
@Test
void findCertificateIdWithoutParams() throws Exception {
mockMvc.perform(get("/certificates/findForUserAndCourse"))
mockMvc.perform(get("/certificates/find-for-user-and-course"))
.andExpect(status().is5xxServerError());
}
......
FROM docker.io/library/eclipse-temurin:17-jre-focal
COPY ./target/module-exercise-0.0.1-SNAPSHOT.jar /app.jar
ENV DOCKER_RUNNING=true
ENTRYPOINT ["java", "-jar", "/app.jar"]
......@@ -11,21 +11,20 @@ public class ModuleExerciseApplication {
private static final String SECURITY_SCHEME_BEARER = "Bearer";
public static final String SECURITY_SCHEME_NAME = SECURITY_SCHEME_BEARER;
public static void main(String[] args) {
SpringApplication.run(ModuleExerciseApplication.class, args);
}
@Bean
public OpenApiCustomizer openAPICustomizer() {
return openApi -> {
openApi.getComponents()
.addSecuritySchemes(SECURITY_SCHEME_BEARER,
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.description("provide a valid access token")
);
};
return openApi -> openApi.getComponents()
.addSecuritySchemes(SECURITY_SCHEME_BEARER,
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.description("provide a valid access token")
);
}
}
......@@ -43,7 +43,7 @@ public class AnswerController {
* @return a ResponseEntity containing an AnswerDto object representing the newly created answer, or a 404 Not Found response
* if the question with the specified ID in dto was not found
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Create new answer for question", description = "Creates new answer for question.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Answers created successfully."),
......@@ -62,7 +62,7 @@ public class AnswerController {
* @return A ResponseEntity with an AnswerDto object representing the updated answer on an HTTP status code of 200 if the update was successful.
* or a NOT_FOUND response if the answer ID is invalid
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Update an answer", description = "Updates an answer with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Answer with the specified ID updated successfully."),
......@@ -80,7 +80,7 @@ public class AnswerController {
* @param id of answer to delete
* @throws ResponseStatusException if answer with specified id does not exist
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Delete an answer with specified ID", description = "Deletes an answer with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Answer with the specified ID deleted successfully."),
......
package org.fuseri.moduleexercise.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
......@@ -19,19 +20,20 @@ public class AppSecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
httpSecurity.authorizeHttpRequests(x -> x
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/datainitializer").permitAll()
.requestMatchers(HttpMethod.POST, "/answers/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.DELETE, "/answers/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.PUT, "/answers/**").hasAnyAuthority("SCOPE_test_1","SCOPE_test_2")
.requestMatchers(HttpMethod.PUT, "/answers/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.POST, "/questions/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.DELETE, "/questions/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.PUT, "/questions/**").hasAnyAuthority("SCOPE_test_1","SCOPE_test_2")
.requestMatchers(HttpMethod.PUT, "/questions/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.PATCH, "/questions/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.POST, "/exercises/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.DELETE, "/exercises/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.requestMatchers(HttpMethod.PUT, "/exercises/**").hasAnyAuthority("SCOPE_test_1","SCOPE_test_2")
.requestMatchers(HttpMethod.PUT, "/exercises/**").hasAnyAuthority("SCOPE_test_1", "SCOPE_test_2")
.anyRequest().authenticated()
).oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken)
......
......@@ -40,7 +40,7 @@ public class RestResponseEntityExceptionHandler {
* @param request request
* @return response entity
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<ApiError> handleValidationErrors(MethodArgumentNotValidException ex, HttpServletRequest request) {
List<ApiSubError> subErrors = ex.getBindingResult().getFieldErrors()
.stream()
......@@ -54,6 +54,22 @@ public class RestResponseEntityExceptionHandler {
return buildResponseEntity(error);
}
/**
* Handle MessageNotReadable exceptions
*
* @param ex exception
* @param request request
* @return response entity
*/
@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity<ApiError> handleMessageNotReadableErrors(HttpMessageNotReadableException ex, HttpServletRequest request) {
ApiError error = new ApiError(
HttpStatus.BAD_REQUEST,
ex,
URL_PATH_HELPER.getRequestUri(request));
return buildResponseEntity(error);
}
/**
* Handle exceptions not matched by above handler methods
*
......
......@@ -51,7 +51,7 @@ public class ExerciseController {
* @param dto containing information about the exercise to create
* @return a ResponseEntity containing an ExerciseDto object representing the newly created exercise
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Create an exercise", description = "Creates a new exercise.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Exercise created successfully."),
......@@ -70,7 +70,7 @@ public class ExerciseController {
* @return a ResponseEntity containing an ExerciseDto object representing the found exercise, or a 404 Not Found response
* if the exercise with the specified ID was not found
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Get an exercise by ID", description = "Returns an exercise with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Exercise with the specified ID retrieved successfully."),
......@@ -87,7 +87,7 @@ public class ExerciseController {
* @param page the page number of the exercises to retrieve
* @return A ResponseEntity containing paginated ExerciseDTOs.
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Get exercises in paginated format", description = "Returns exercises in paginated format.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved paginated exercises"),
......@@ -106,7 +106,7 @@ public class ExerciseController {
* @param page the page number of the exercises to retrieve
* @return A ResponseEntity containing filtered and paginated ExerciseDTOs
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Filter exercises per difficulty and per course", description = "Returns exercises which belong to specified course and have specified difficulty.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved filtered paginated exercises."),
......@@ -127,7 +127,7 @@ public class ExerciseController {
* @return a ResponseEntity containing paginated QuestionDTOs which belong to an exercise with exerciseId
* or a NOT_FOUND response if the exercise ID is invalid
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Find questions belonging to exercise by exercise ID",
description = "Returns a paginated list of questions for the specified exercise ID.")
@ApiResponses(value = {
......@@ -149,7 +149,7 @@ public class ExerciseController {
* @return A ResponseEntity with an ExerciseDto object representing the updated exercise an HTTP status code of 200 if the update was successful.
* or a NOT_FOUND response if the exercise ID is invalid
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Update a exercise", description = "Updates a exercise with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Exercise with the specified ID updated successfully."),
......@@ -166,7 +166,7 @@ public class ExerciseController {
*
* @param id the ID of the exercise to delete
*/
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME,scopes = {"test_1","test_2"}),
@Operation(security = @SecurityRequirement(name = ModuleExerciseApplication.SECURITY_SCHEME_NAME),
summary = "Delete a exercise with specified ID", description = "Deletes a exercise with the specified ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Exercise with the specified ID deleted successfully."),
......