Skip to content

Commit

Permalink
Keep track of expired stripe sessions (#11)
Browse files Browse the repository at this point in the history
* Add checkExpiredStripeSessions() method into StripePaymentServiceImpl

* Add renewPaymentSession() method into PaymentController, StripePaymentServiceImpl

* Add isAllowedToRentCar() method into RentalServiceImpl

* remove field 'user' from 'Payment'

* add CarServiceTest

* add RentalServiceTest

* add UserServiceTest

* add TelegramUserServiceTest

* add TelegramNotificationServiceTest

* add StripePaymentServiceTest

* add Dockerfile, docker-compose.yml

* add columnDefinition = "TINYINT" to boolean fields

* finish tests

* add custom annotation @UserRoleDescription to print roles in swagger doc.

* Update ci.yml

* Update ci.yml

* Update ci.yml
  • Loading branch information
Krasnov-Maksim authored Mar 26, 2024
1 parent 86e6bb0 commit 70487ec
Show file tree
Hide file tree
Showing 44 changed files with 1,907 additions and 64 deletions.
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Builder stage
FROM openjdk:17-jdk-alpine as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

# Final stage
FROM openjdk:17-jdk-alpine
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
EXPOSE 8080
Binary file removed car-sharing-app-db-model.mwb
Binary file not shown.
44 changes: 44 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: car-sharing-application
services:
mysqldb:
image: mysql:8.0.36-oracle
restart: unless-stopped
env_file:
- ./.env
environment:
- MYSQL_DATABASE=$MYSQLDB_DATABASE
- MYSQL_ROOT_PASSWORD=$MYSQLDB_ROOT_PASSWORD
ports:
- $MYSQLDB_LOCAL_PORT:$MYSQLDB_DOCKER_PORT
healthcheck:
test: [ "CMD-SHELL", "mysqladmin ping -h localhost -u${MYSQLDB_USER} -p${MYSQLDB_ROOT_PASSWORD}" ]
interval: 30s
timeout: 30s
retries: 30
app:
depends_on:
- mysqldb
image: car-sharing-image
restart: on-failure
container_name: app
build: .
env_file: ./.env
ports:
- $SPRING_LOCAL_PORT:$SPRING_DOCKER_PORT
- $DEBUG_PORT:$DEBUG_PORT
environment:
SPRING_APPLICATION_JSON: '{
"spring.datasource.url" : "jdbc:mysql://mysqldb:$MYSQLDB_DOCKER_PORT/$MYSQLDB_DATABASE?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=UTC",
"spring.datasource.username" : "$MYSQLDB_USER",
"spring.datasource.password" : "$MYSQLDB_ROOT_PASSWORD",
"spring.jpa.hibernate.ddl-auto" : "validate",
"spring.jpa.show-sql" : "true",
"jwt.expiration" : "$JWT_EXPIRATION_TIME",
"jwt.secret" : "$JWT_SECRET",
"STRIPE_APY_KEY" : "$STRIPE_APY_KEY",
"STRIPE_SUCCESS_LINK" : "$STRIPE_SUCCESS_LINK",
"STRIPE_CANCEL_LINK" : "$STRIPE_CANCEL_LINK",
"TELEGRAM_BOT_TOKEN" : "$TELEGRAM_BOT_TOKEN",
"TELEGRAM_BOT_USERNAME" : "$TELEGRAM_BOT_USERNAME"
}'
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<testcontainers.version>1.19.4</testcontainers.version>
<telegrambot.version>6.9.0</telegrambot.version>
<stripe.version>24.19.0</stripe.version>
<jackson.version>2.17.0</jackson.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -136,6 +137,17 @@
<artifactId>stripe-java</artifactId>
<version>${stripe.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- support for "Java 8 Dates" -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mate.academy.carsharing.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserRoleDescription {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package mate.academy.carsharing.annotation;

import io.swagger.v3.oas.models.Operation;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;

@Component
public class UserRoleDescriptionCustomizer implements OperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
UserRoleDescription userRoleAnnotation =
handlerMethod.getMethodAnnotation(UserRoleDescription.class);
if (userRoleAnnotation != null) {
PreAuthorize preAuthorizeAnnotation =
handlerMethod.getMethodAnnotation(PreAuthorize.class);
if (preAuthorizeAnnotation != null
&& preAuthorizeAnnotation.value().contains("ROLE_")) {
String description = operation.getDescription() == null
? "" : (operation.getDescription()/* + "\n"*/);
int firstBraceIndex = preAuthorizeAnnotation.value().indexOf('(');
int lastBraceIndex = preAuthorizeAnnotation.value().lastIndexOf(')');
String rolesString = preAuthorizeAnnotation.value()
.substring(firstBraceIndex + 1, lastBraceIndex);
operation.setDescription(description + " Required roles: " + rolesString + '.');
}
}
return operation;
}
}
20 changes: 20 additions & 0 deletions src/main/java/mate/academy/carsharing/config/OpenApiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package mate.academy.carsharing.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.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
import org.springframework.context.annotation.Configuration;

@Configuration
@OpenAPIDefinition(info = @Info(title = "Car-Sharing-App REST API", version = "1.0",
description = "REST API for Car-Sharing-App. Register, Login and use JWT token."),
security = {@SecurityRequirement(name = "bearerToken")}
)
@SecuritySchemes({@SecurityScheme(name = "bearerToken", type = SecuritySchemeType.HTTP,
scheme = "bearer", bearerFormat = "JWT")}
)
public class OpenApiConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public SecurityFilterChain securityFilterChain(UserDetailsService userDetailsSer
// list cars.
.requestMatchers(HttpMethod.GET, "api/cars", "api/cars/{id}")
.permitAll()
.requestMatchers(HttpMethod.GET,
"api/payments/cancel", "api/payments/success")
.permitAll()
.requestMatchers("api/auth/**", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package mate.academy.carsharing.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -17,7 +18,7 @@
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Authentication manager", description = "Endpoints for registration")
@Tag(name = "Authentication controller", description = "Endpoints for registration and login.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
Expand All @@ -26,13 +27,16 @@ public class AuthenticationController {
private final AuthenticationService authenticationService;

@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Register a new user.", description = "Register a new user.")
@PostMapping("/registration")
public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto request)
throws RegistrationException {
return userService.register(request);
}

@PostMapping("/login")
@Operation(summary = "Login to the app.",
description = "Login to the app, get JWT Token and use it to request data.")
public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) {
return authenticationService.authentication(requestDto);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import mate.academy.carsharing.annotation.UserRoleDescription;
import mate.academy.carsharing.dto.car.CarResponseDto;
import mate.academy.carsharing.dto.car.CreateCarRequestDto;
import mate.academy.carsharing.service.CarService;
Expand All @@ -25,29 +26,32 @@
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/api/cars")
@Tag(name = "Car management", description = "Cars Management Endpoints")
@Tag(name = "Car management", description = "Cars Management Endpoints.")
public class CarController {
private final CarService carService;

@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ROLE_MANAGER')")
@Operation(summary = "Create a new car", description = "Create a new car")
@UserRoleDescription
@Operation(summary = "Create a new car.", description = "Create a new car.")
@PostMapping
public CarResponseDto create(@RequestBody @Valid CreateCarRequestDto carDto) {
return carService.save(carDto);
}

@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ROLE_MANAGER')")
@Operation(summary = "Delete a car by id", description = "Delete car with specified id")
@UserRoleDescription
@Operation(summary = "Delete a car by id.", description = "Delete car with specified id.")
@DeleteMapping("/{id}")
public void deleteById(@PathVariable Long id) {
carService.deleteById(id);
}

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('ROLE_MANAGER')")
@Operation(summary = "Update a car by id", description = "Update car with specified id")
@UserRoleDescription
@Operation(summary = "Update a car by id.", description = "Update car with specified id.")
@PutMapping("/{id}")
public CarResponseDto updateById(@PathVariable Long id,
@RequestBody @Valid CreateCarRequestDto requestDto) {
Expand All @@ -56,18 +60,20 @@ public CarResponseDto updateById(@PathVariable Long id,

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER', 'ROLE_ANONYMOUS')")
@Operation(summary = "Get a car by id", description = "Get a car with specified id")
@UserRoleDescription
@Operation(summary = "Get a car by id.", description = "Get a car with specified id.")
@GetMapping("/{id}")
public CarResponseDto getById(@PathVariable Long id) {
return carService.getById(id);
}

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("permitAll()")
@Operation(summary = "Get all cars", description = "Get a list of all cars")
@UserRoleDescription
@Operation(summary = "Get all cars.", description = "Get a list of all cars.")
@Parameter(name = "page", description = "page index, default value = 0")
@Parameter(name = "size", description = "elements per page, default value = 20")
@Parameter(name = "sort", description = "sort criteria", example = "title,Desc")
@Parameter(name = "sort", description = "sort criteria", example = "brand,Desc")
@GetMapping
public List<CarResponseDto> getAll(Pageable pageable) {
return carService.getAll(pageable);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package mate.academy.carsharing.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import mate.academy.carsharing.annotation.UserRoleDescription;
import mate.academy.carsharing.dto.payment.CreatePaymentRequestDto;
import mate.academy.carsharing.dto.payment.PaymentResponseDto;
import mate.academy.carsharing.dto.payment.PaymentSearchParametersDto;
import mate.academy.carsharing.dto.payment.RenewPaymentRequestDto;
import mate.academy.carsharing.service.PaymentService;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
Expand All @@ -30,7 +33,8 @@ public class PaymentController {

@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ROLE_MANAGER')")
@Operation(summary = "Create new payment", description = "Create new payment session")
@UserRoleDescription
@Operation(summary = "Create new payment.", description = "Create new payment session.")
@PostMapping()
public PaymentResponseDto createPayment(
@RequestBody @Valid CreatePaymentRequestDto requestDto) {
Expand All @@ -39,6 +43,12 @@ public PaymentResponseDto createPayment(

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER')")
@UserRoleDescription
@Operation(summary = "Search payments.", description = "User can view own payments. "
+ "Admin can view all payments.")
@Parameter(name = "page", description = "page index, default value = 0")
@Parameter(name = "size", description = "elements per page, default value = 20")
@Parameter(name = "sort", description = "sort criteria", example = "amountToPay,Desc")
@GetMapping("/search")
public List<PaymentResponseDto> searchPayments(Authentication authentication,
PaymentSearchParametersDto searchParameters, Pageable pageable) {
Expand All @@ -47,8 +57,9 @@ public List<PaymentResponseDto> searchPayments(Authentication authentication,

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("permitAll()")
@Operation(summary = "Stripe redirection endpoint",
description = "Mark payment as success, send message to Telegram")
@UserRoleDescription
@Operation(summary = "Stripe redirection endpoint marks payment as success.",
description = "Mark payment as success, send message to Telegram.")
@GetMapping("/success")
public PaymentResponseDto processSuccessfulPayment(
@RequestParam(name = "session_id") String sessionId) {
Expand All @@ -57,11 +68,24 @@ public PaymentResponseDto processSuccessfulPayment(

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("permitAll()")
@Operation(summary = "Stripe redirection endpoint",
description = "Mark payment as canceled, send message to Telegram")
@UserRoleDescription
@Operation(summary = "Stripe redirection endpoint marks payment as canceled",
description = "Marks payment as canceled, send message to Telegram.")
@GetMapping("/cancel")
public PaymentResponseDto processCanceledPayment(
@RequestParam(name = "session_id") String sessionId) {
return paymentService.processCanceledPayment(sessionId);
}

@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER')")
@UserRoleDescription
@Operation(summary = "Renew expired session.",
description = "User can renew expired session and get new payment link.")
@PostMapping("/renew")
public PaymentResponseDto renewPaymentSession(
@RequestBody @Valid RenewPaymentRequestDto requestDto,
Authentication authentication) {
return paymentService.renewPaymentSession(requestDto.paymentId(), authentication.getName());
}
}
Loading

0 comments on commit 70487ec

Please sign in to comment.