Skip to content

Commit

Permalink
Added Swagger to resource server
Browse files Browse the repository at this point in the history
  • Loading branch information
roar-skinderviken committed Dec 8, 2024
1 parent d1ea6e2 commit 855d2d0
Show file tree
Hide file tree
Showing 24 changed files with 424 additions and 109 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,12 @@ poetry install
```shell
poetry run uvicorn src.app:app --reload
```

## API Documentation

The OpenAPI documentation for the API is available at the following URL:

[Swagger UI](http://localhost:8080/swagger-ui/index.html)

You can use this interface to explore and interact with the API endpoints, view their descriptions,
and test requests directly from the UI.
3 changes: 2 additions & 1 deletion backend-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ repositories {

dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation(libs.springdoc.openapi)

runtimeOnly("com.github.ben-manes.caffeine:caffeine")
runtimeOnly(libs.logback.access.spring.boot.starter)

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package no.vicx.backend.calculator;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import no.vicx.backend.calculator.vm.CalcVm;
import no.vicx.backend.calculator.vm.CalculatorRequestVm;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -17,6 +27,7 @@
import java.util.List;
import java.util.Optional;

@Tag(name = "Calculator", description = "API for performing calculations and managing history")
@RequestMapping("/api/calculator")
@RestController
@Validated
Expand All @@ -28,6 +39,14 @@ public CalculatorController(CalculatorService calculatorService) {
this.calculatorService = calculatorService;
}

@SecurityRequirement(name = "security_auth")
@Operation(
summary = "Delete calculations by IDs",
description = "Deletes calculations identified by a list of IDs. Requires authorization.",
responses = {
@ApiResponse(responseCode = "204", description = "Successfully deleted the calculations."),
@ApiResponse(responseCode = "403", description = "Access denied."),
@ApiResponse(responseCode = "400", description = "Invalid request.")})
@DeleteMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("@calculatorSecurityService.isAllowedToDelete(#ids, authentication)")
public ResponseEntity<Void> deleteByIds(
Expand All @@ -36,12 +55,24 @@ public ResponseEntity<Void> deleteByIds(
return ResponseEntity.noContent().build();
}

@SecurityRequirement(name = "security_auth")
@Operation(
summary = "Perform a calculation",
description = "Processes the provided calculation request and returns the result.",
responses = {
@ApiResponse(
responseCode = "200",
description = "Calculation performed successfully.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = CalcVm.class))),
@ApiResponse(responseCode = "400", description = "Invalid calculation request.")})
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public CalcVm calculateAndReturnResult(
@Valid @RequestBody CalculatorRequestVm request,
Authentication authentication) {
@Parameter(hidden = true) Authentication authentication) {
return calculatorService.calculate(
request,
Optional.ofNullable(authentication)
Expand All @@ -50,8 +81,27 @@ public CalcVm calculateAndReturnResult(
);
}

@Operation(
summary = "List all calculations",
description = "Retrieves a paginated list of all calculations.",
responses = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved the calculations.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
array = @ArraySchema(schema = @Schema(implementation = CalcVm.class))))})
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public Page<CalcVm> index(Pageable pageable) {
public Page<CalcVm> index(
@Parameter(description = "Page number (0-based)")
@RequestParam(value = "page", required = false) Integer page,
@Parameter(description = "Page size")
@RequestParam(value = "size", required = false) Integer size,
@Parameter(description = "Sort criteria")
@RequestParam(value = "sort", required = false) String sort) {
Pageable pageable = PageRequest.of(page != null ? page : 0,
size != null ? size : 10,
Sort.by(sort != null ? sort : "id").ascending());
return calculatorService.getAllCalculations(pageable);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
package no.vicx.backend.calculator.vm;

import io.swagger.v3.oas.annotations.media.Schema;
import no.vicx.database.calculator.CalcEntry;
import no.vicx.database.calculator.CalculatorOperation;

import java.time.LocalDateTime;

/**
* Represents a detailed view of a calculator operation, including metadata.
*/
@Schema(description = "Represents a detailed view of a calculator operation, including metadata.")
public record CalcVm(
@Schema(description = "The unique identifier of the calculation.", example = "12345")
long id,

@Schema(description = "The first value used in the calculation.", example = "10")
long firstValue,

@Schema(description = "The second value used in the calculation.", example = "5")
long secondValue,

@Schema(description = "The operation performed on the values (e.g., ADD, SUBTRACT).",
implementation = CalculatorOperation.class)
CalculatorOperation operation,

@Schema(description = "The result of the calculation.", example = "15")
long result,

@Schema(description = "The username of the person who performed the calculation.",
example = "john_doe")
String username,

@Schema(description = "The timestamp when the calculation was created.",
example = "2024-12-07T10:15:30")
LocalDateTime createdAt
) {
public static CalcVm fromEntity(CalcEntry entity) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
package no.vicx.backend.calculator.vm;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import no.vicx.database.calculator.CalculatorOperation;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

/**
* A request object representing a calculator operation with two values and an operator.
*/
@Schema(description = "Represents a request for a calculator operation.")
public record CalculatorRequestVm(
@Schema(description = "The first value in the calculation.",
example = "10",
requiredMode = REQUIRED)
@NotNull Long firstValue,

@Schema(description = "The second value in the calculation.",
example = "5",
requiredMode = REQUIRED)
@NotNull Long secondValue,
@NotNull CalculatorOperation operation) {

@Schema(description = "The operation to perform on the values (e.g., ADD, SUBTRACT).",
implementation = CalculatorOperation.class,
requiredMode = REQUIRED)
@NotNull CalculatorOperation operation
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package no.vicx.backend.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.OAuthFlow;
import io.swagger.v3.oas.annotations.security.OAuthFlows;
import io.swagger.v3.oas.annotations.security.OAuthScope;
import io.swagger.v3.oas.annotations.security.SecurityScheme;

@OpenAPIDefinition(info = @Info(title = "Vicx API",
description = "The Vicx API provides endpoints for managing resources, performing operations, " +
"and accessing features of the Vicx platform.",
version = "v1"))
@SecurityScheme(
name = "security_auth",
type = SecuritySchemeType.OAUTH2,
flows = @OAuthFlows(authorizationCode = @OAuthFlow(
authorizationUrl = "${springdoc.oAuthFlow.authorizationUrl}",
tokenUrl = "${springdoc.oAuthFlow.tokenUrl}",
scopes = {@OAuthScope(name = "openid", description = "openid scope")})))
public class OpenApiConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -15,6 +16,8 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebSecurity
Expand All @@ -26,6 +29,19 @@ public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(@NonNull CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "OPTIONS", "HEAD")
.allowCredentials(true);
}
};
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
Expand All @@ -40,46 +56,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
// exposes same info as /actuator/info, but on port 8080
.requestMatchers(HttpMethod.GET, "/gitproperties").permitAll()

.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()

.requestMatchers(HttpMethod.GET, "/api/calculator").permitAll()
.requestMatchers(HttpMethod.POST, "/api/calculator").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/calculator").hasAnyRole("USER", "GITHUB_USER")

.requestMatchers(HttpMethod.POST, "/api/user").permitAll()
.requestMatchers(HttpMethod.POST, "/api/**").hasRole("USER")

.requestMatchers(HttpMethod.PATCH, "/api/**").hasRole("USER")

.requestMatchers(HttpMethod.GET, "/api/**").hasRole("USER")

.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("USER")
.requestMatchers("/api/**").hasRole("USER")

.requestMatchers("/error").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(new FusedClaimConverter()))
)
.headers(headers -> {
headers.permissionsPolicyHeader(permissions ->
permissions.policy("geolocation=(), microphone=(), camera=()"));

headers.contentSecurityPolicy(policyConfig ->
policyConfig.policyDirectives(
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self'; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"media-src 'self'; " +
"frame-src 'none'; " + // Example for blocking iframes
"frame-ancestors 'none'; " + // Prevent framing
"form-action 'self'; " + // Restrict form submissions
"base-uri 'self';" // Restrict base URI
)
);
}
)
.jwt(jwt -> jwt.jwtAuthenticationConverter(new FusedClaimConverter())))
.build();
}
}

This file was deleted.

Loading

0 comments on commit 855d2d0

Please sign in to comment.