├── Dockerfile ├── src ├── main │ ├── java │ │ └── com │ │ │ └── onurhizar │ │ │ └── gamepass │ │ │ ├── model │ │ │ ├── enums │ │ │ │ └── UserRole.java │ │ │ ├── response │ │ │ │ ├── AuthenticationResponse.java │ │ │ │ ├── GameResponse.java │ │ │ │ ├── ContractRecordResponse.java │ │ │ │ ├── InvoiceResponse.java │ │ │ │ ├── SubscriptionResponse.java │ │ │ │ ├── CategoryResponse.java │ │ │ │ └── UserResponse.java │ │ │ ├── request │ │ │ │ ├── UpdateCategoryRequest.java │ │ │ │ ├── CreateGameRequest.java │ │ │ │ ├── CreateCategoryRequest.java │ │ │ │ ├── UpdateSubscriptionRequest.java │ │ │ │ ├── CreateSubscriptionRequest.java │ │ │ │ ├── auth │ │ │ │ │ ├── LoginRequest.java │ │ │ │ │ └── RegisterRequest.java │ │ │ │ ├── PaymentRequest.java │ │ │ │ └── CreateUserRequest.java │ │ │ └── entity │ │ │ │ ├── Subscription.java │ │ │ │ ├── Payment.java │ │ │ │ ├── Invoice.java │ │ │ │ ├── Game.java │ │ │ │ ├── ContractRecord.java │ │ │ │ ├── Category.java │ │ │ │ ├── BaseEntity.java │ │ │ │ └── User.java │ │ │ ├── repository │ │ │ ├── GameRepository.java │ │ │ ├── PaymentRepository.java │ │ │ ├── SubscriptionRepository.java │ │ │ ├── ContractRecordRepository.java │ │ │ ├── CategoryRepository.java │ │ │ ├── UserRepository.java │ │ │ └── InvoiceRepository.java │ │ │ ├── GamePassApplication.java │ │ │ ├── exception │ │ │ ├── EntityNotFoundException.java │ │ │ ├── UnacceptableRequestException.java │ │ │ └── GlobalControllerAdvice.java │ │ │ ├── controller │ │ │ ├── IndexController.java │ │ │ ├── InvoiceController.java │ │ │ ├── CategoryController.java │ │ │ ├── SubscriptionController.java │ │ │ ├── GameController.java │ │ │ ├── UserController.java │ │ │ ├── UserInterestController.java │ │ │ └── AuthenticationController.java │ │ │ ├── service │ │ │ ├── PaymentService.java │ │ │ ├── UserInterestService.java │ │ │ ├── SubscriptionService.java │ │ │ ├── ContractRecordService.java │ │ │ ├── ScheduleService.java │ │ │ ├── GameService.java │ │ │ ├── InvoiceService.java │ │ │ ├── AuthenticationService.java │ │ │ ├── CategoryService.java │ │ │ └── UserService.java │ │ │ ├── security │ │ │ ├── SecurityConstants.java │ │ │ ├── FilterChainConfig.java │ │ │ ├── JwtService.java │ │ │ ├── SecurityConfig.java │ │ │ ├── JwtAuthenticationFilter.java │ │ │ ├── SelfFilter.java │ │ │ └── SecurityConfiguration.java │ │ │ └── config │ │ │ ├── OpenApiConfig.java │ │ │ └── ConfigRunner.java │ └── resources │ │ ├── application.yml │ │ └── data.sql └── test │ ├── java │ └── com │ │ └── onurhizar │ │ └── gamepass │ │ ├── GamePassApplicationTests.java │ │ ├── util │ │ └── AuthTokenHelper.java │ │ ├── abstracts │ │ └── AbstractIntegrationTest.java │ │ ├── containers │ │ └── PostgresTestContainer.java │ │ └── controller │ │ ├── GameControllerTests.java │ │ ├── CategoryControllerTests.java │ │ ├── UserControllerTests.java │ │ └── AuthControllerTests.java │ └── resources │ └── application.yml ├── docker-compose.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENCE ├── .gitlab-ci.yml ├── README.md ├── pom.xml ├── mvnw.cmd └── mvnw /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:8 2 | COPY target/gamepass-0.0.1-SNAPSHOT.jar app.jar 3 | EXPOSE 8080 4 | ENTRYPOINT ["java","-jar","app.jar"] 5 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/enums/UserRole.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.enums; 2 | 3 | public enum UserRole { 4 | ADMIN, 5 | MEMBER, 6 | GUEST, 7 | INACTIVE_MEMBER 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/AuthenticationResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import lombok.*; 4 | 5 | @Data 6 | @NoArgsConstructor 7 | @AllArgsConstructor 8 | public class AuthenticationResponse { 9 | private String token; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/GameRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.Game; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface GameRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.Payment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface PaymentRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/SubscriptionRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.Subscription; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface SubscriptionRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/UpdateCategoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public class UpdateCategoryRequest { 9 | private String name; 10 | private String parentName; 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/GamePassApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 7 | class GamePassApplicationTests { 8 | @Test 9 | void contextLoads() { 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/CreateGameRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CreateGameRequest { 13 | private String title; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/CreateCategoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CreateCategoryRequest { 13 | private String name; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/UpdateSubscriptionRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public class UpdateSubscriptionRequest { 9 | private String name; 10 | private int monthlyFee; 11 | private int duration; 12 | private boolean active; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/GamePassApplication.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class GamePassApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(GamePassApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/ContractRecordRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.ContractRecord; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface ContractRecordRepository extends JpaRepository { 9 | public List findByActiveIsTrue(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/Subscription.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import javax.persistence.Entity; 4 | 5 | import lombok.*; 6 | 7 | @Entity 8 | @Getter 9 | @Setter 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class Subscription extends BaseEntity { 14 | 15 | private String name; 16 | private int monthlyFee; 17 | private int duration; 18 | private boolean isActive; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/CreateSubscriptionRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | public class CreateSubscriptionRequest { 11 | private String name; 12 | private int monthlyFee; 13 | private int duration; 14 | private boolean active; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/Payment.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import javax.persistence.*; 4 | import lombok.*; 5 | 6 | @Entity 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | @Builder 11 | @AllArgsConstructor 12 | public class Payment extends BaseEntity { 13 | 14 | private int amount; 15 | private String senderCard; 16 | private String receiverCard; 17 | 18 | @ManyToOne 19 | private Invoice invoice; 20 | } -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/auth/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request.auth; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | import lombok.*; 6 | 7 | @Data 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class LoginRequest { 11 | 12 | @NotBlank 13 | private String username; 14 | 15 | @NotBlank 16 | @Size(min = 6, max = 32) 17 | private String password; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/exception/EntityNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | public class EntityNotFoundException extends RuntimeException { 10 | 11 | private final HttpStatus statusCode = HttpStatus.NOT_FOUND; 12 | private final String message = "entity could not found with given credentials"; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.servlet.view.RedirectView; 6 | 7 | @Controller 8 | public class IndexController { 9 | 10 | @GetMapping("/") 11 | public RedirectView redirectToSwaggerUI() { 12 | return new RedirectView("/swagger-ui/index.html"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/PaymentRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | public class PaymentRequest { 13 | @NotNull 14 | private int amount; 15 | @NotBlank 16 | private String senderCard; 17 | @NotBlank 18 | private String receiverCard; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/PaymentService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.model.entity.Payment; 4 | import com.onurhizar.gamepass.repository.PaymentRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | @RequiredArgsConstructor 10 | public class PaymentService { 11 | private final PaymentRepository repository; 12 | 13 | public void addPayment(Payment payment){ 14 | repository.save(payment); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - 8080:8080 9 | depends_on: 10 | - db 11 | environment: 12 | - DB_URL=jdbc:postgresql://db:5432/gamepass 13 | - DB_USERNAME=postgres 14 | - DB_PASSWORD=secretpassword 15 | db: 16 | image: postgres 17 | restart: always 18 | ports: 19 | - 5432:5432 20 | environment: 21 | POSTGRES_USER: postgres 22 | POSTGRES_PASSWORD: secretpassword 23 | POSTGRES_DB: gamepass 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | /.mvn/wrapper/maven-wrapper.jar 35 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/SecurityConstants.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import lombok.Getter; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | 8 | @Configuration 9 | @Getter 10 | public class SecurityConstants { 11 | 12 | @Value("${jwt.secret}") 13 | private String JWT_SECRET_KEY; 14 | 15 | @Value("${jwt.issuer}") 16 | private String JWT_TOKEN_ISSUER; 17 | 18 | @Value("${jwt.expiration.hours}") 19 | private int JWT_EXPIRATION_HOURS; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/exception/UnacceptableRequestException.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | public class UnacceptableRequestException extends RuntimeException { 10 | 11 | private final HttpStatus statusCode = HttpStatus.BAD_REQUEST; 12 | private String message = "request is not acceptable"; // default message 13 | 14 | public UnacceptableRequestException(String message){ 15 | this.message = message; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/CategoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.Category; 4 | import com.onurhizar.gamepass.model.entity.Game; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface CategoryRepository extends JpaRepository { 11 | 12 | Optional findCategoryByName(String name); 13 | 14 | List findCategoriesByGames(Game game); 15 | 16 | List findCategoriesByParentId(String parentId); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/Invoice.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import javax.persistence.*; 4 | import lombok.*; 5 | 6 | @Entity 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Builder 12 | public class Invoice extends BaseEntity { 13 | 14 | private int fee; 15 | 16 | @ManyToOne 17 | private ContractRecord contractRecord; 18 | 19 | @Override 20 | public String toString() { 21 | return "Invoice{" + "id=" + getId() + 22 | ", fee=" + fee + 23 | ", contractRecordID=" + contractRecord.getId() + 24 | '}'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/auth/RegisterRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request.auth; 2 | 3 | import javax.validation.constraints.Email; 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.Size; 6 | import lombok.*; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class RegisterRequest { 12 | 13 | @NotBlank 14 | private String name; 15 | 16 | @NotBlank 17 | private String surname; 18 | 19 | @Email 20 | @NotBlank 21 | private String email; 22 | 23 | @NotBlank 24 | @Size(min = 6, max = 32) 25 | private String password; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/Game.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import javax.persistence.*; 5 | import lombok.*; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | @Entity 11 | @Getter 12 | @Setter 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class Game extends BaseEntity { 17 | 18 | private String title; 19 | 20 | @ManyToMany(mappedBy = "games", fetch = FetchType.LAZY) 21 | @JsonIgnoreProperties("games") 22 | @Builder.Default 23 | private List categories = new LinkedList<>(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: ${DB_URL:jdbc:postgresql://localhost:5432/gamepass} 4 | username: ${DB_USERNAME:postgres} 5 | password: ${DB_PASSWORD:secretpassword} 6 | 7 | jpa: 8 | hibernate: 9 | ddl-auto: create-drop 10 | # show-sql: true 11 | properties: 12 | hibernate: 13 | format_sql: true 14 | defer-datasource-initialization: true 15 | 16 | sql: 17 | init: 18 | mode: always 19 | 20 | jwt: 21 | secret: ${JWT_SECRET_KEY:1010635266556A587E32D2357538782F413F4428472B4BA250F153675B6B5C75} 22 | issuer: ${JWT_ISSUER:GamePass App} 23 | expiration: 24 | hours: ${JWT_EXPIRATION_HOURS:12} 25 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.Category; 4 | import com.onurhizar.gamepass.model.entity.Game; 5 | import com.onurhizar.gamepass.model.entity.User; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import java.util.List; 9 | 10 | public interface UserRepository extends JpaRepository { 11 | public User findByEmail(String email); 12 | 13 | public User findUserByVerificationCode(String verificationCode); 14 | 15 | public User findUserByRecoveryCode(String recoveryCode); 16 | 17 | public List findByFollowedCategories(Category category); 18 | 19 | public List findByFavoriteGames(Game game); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/util/AuthTokenHelper.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.util; 2 | 3 | import com.onurhizar.gamepass.security.JwtService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.test.web.client.TestRestTemplate; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class AuthTokenHelper { 12 | 13 | public final TestRestTemplate restTemplate; 14 | public final JwtService jwtService; 15 | 16 | public HttpHeaders generateJwtHeader(String username){ 17 | String token = jwtService.generateToken(username); 18 | HttpHeaders headers = new HttpHeaders(); 19 | headers.setBearerAuth(token); 20 | return headers; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: ${DB_URL:jdbc:postgresql://localhost:5432/gamepass} 4 | username: ${DB_USERNAME:postgres} 5 | password: ${DB_PASSWORD:secretpassword} 6 | hikari: 7 | maximum-pool-size: 5 8 | tomcat: 9 | max-wait: 10000 10 | 11 | jpa: 12 | hibernate: 13 | ddl-auto: create-drop 14 | show-sql: false 15 | properties: 16 | hibernate: 17 | dialect: org.hibernate.dialect.PostgreSQLDialect 18 | format_sql: true 19 | defer-datasource-initialization: true 20 | 21 | sql: 22 | init: 23 | mode: always 24 | 25 | jwt: 26 | secret: 1010635266556A587E32D2357538782F413F4428472B4BA250F153675B6B5C75 27 | issuer: GamePass App 28 | expiration: 29 | hours: 12 30 | 31 | upload: 32 | directory: ${UPLOAD_DIRECTORY:/tmp} -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/abstracts/AbstractIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.abstracts; 2 | 3 | import com.onurhizar.gamepass.containers.PostgresTestContainer; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.web.client.TestRestTemplate; 7 | import org.testcontainers.containers.PostgreSQLContainer; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | 11 | @Testcontainers 12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 13 | public abstract class AbstractIntegrationTest { 14 | 15 | @Autowired 16 | protected TestRestTemplate restTemplate; 17 | 18 | @Container 19 | static PostgreSQLContainer postgresContainer = PostgresTestContainer.getInstance(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/ContractRecord.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import javax.persistence.*; 4 | import lombok.*; 5 | 6 | import java.time.ZonedDateTime; 7 | 8 | @Entity 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Builder 14 | public class ContractRecord extends BaseEntity { 15 | 16 | private String name; 17 | private int monthlyFee; 18 | private int duration; 19 | 20 | @Column(name = "is_active") 21 | private boolean active; 22 | 23 | @OneToOne 24 | private User user; 25 | private ZonedDateTime createdDate; 26 | 27 | @Override 28 | public String toString() { 29 | return "ContractRecord [name=" + name + ", monthlyFee=" + monthlyFee + ", duration=" + duration + ", isActive=" 30 | + active + ", userId=" + user.getId() + ", createdDate=" + createdDate + "]"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/GameResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import com.onurhizar.gamepass.model.entity.Category; 4 | import com.onurhizar.gamepass.model.entity.Game; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.Getter; 8 | 9 | import java.time.ZonedDateTime; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @AllArgsConstructor 14 | @Data 15 | public class GameResponse { 16 | 17 | private String id; 18 | private String title; 19 | private List categories; 20 | 21 | private ZonedDateTime createdAt; 22 | private ZonedDateTime updatedAt; 23 | 24 | public static GameResponse fromEntity(Game game){ 25 | return new GameResponse( 26 | game.getId(), 27 | game.getTitle(), 28 | game.getCategories().stream().map(Category::getName).collect(Collectors.toList()), 29 | game.getCreatedAt(), 30 | game.getUpdatedAt() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.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.security.SecurityRequirement; 6 | import io.swagger.v3.oas.models.security.SecurityScheme; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class OpenApiConfig { 12 | 13 | @Bean 14 | public OpenAPI customizeOpenAPI() { 15 | 16 | final String securitySchemeName = "Enter the JWT"; 17 | 18 | return new OpenAPI() 19 | .addSecurityItem(new SecurityRequirement() 20 | .addList(securitySchemeName)) 21 | .components(new Components() 22 | .addSecuritySchemes(securitySchemeName, new SecurityScheme() 23 | .name(securitySchemeName) 24 | .type(SecurityScheme.Type.HTTP) 25 | .scheme("bearer") 26 | .bearerFormat("JWT"))); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/repository/InvoiceRepository.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.repository; 2 | 3 | import com.onurhizar.gamepass.model.entity.Invoice; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.time.ZonedDateTime; 7 | import java.util.List; 8 | 9 | public interface InvoiceRepository extends JpaRepository { 10 | List findByCreatedAtBefore(ZonedDateTime time); 11 | List findByCreatedAtBeforeAndFeeIsNot(ZonedDateTime time, int fee); 12 | 13 | /** Finds invoices for a user */ 14 | List findByContractRecordUserId(String userId); 15 | 16 | /** Finds invoices for a user that are not paid and past-due */ 17 | List findByContractRecordUserIdAndFeeNotAndCreatedAtBefore(String userId, int fee, ZonedDateTime time); 18 | 19 | /** returns invoices for a single contract record only after given created time
20 | * it is used to list invoices for a contract record in current month */ 21 | List findByContractRecordIdAndCreatedAtAfter(String contractRecordId, ZonedDateTime time); 22 | } 23 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Onur Hızar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/ContractRecordResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import com.onurhizar.gamepass.model.entity.ContractRecord; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.time.ZonedDateTime; 8 | 9 | @Data 10 | @Builder 11 | public class ContractRecordResponse { 12 | private String id; 13 | private String name; 14 | private int monthlyFee; 15 | private int duration; 16 | private boolean isActive; 17 | 18 | private ZonedDateTime createdAt; 19 | private ZonedDateTime updatedAt; 20 | 21 | public static ContractRecordResponse fromEntity(ContractRecord contractRecord){ 22 | return ContractRecordResponse.builder() 23 | .id(contractRecord.getId()) 24 | .name(contractRecord.getName()) 25 | .monthlyFee(contractRecord.getMonthlyFee()) 26 | .duration(contractRecord.getDuration()) 27 | .isActive(contractRecord.isActive()) 28 | .createdAt(contractRecord.getCreatedAt()) 29 | .updatedAt(contractRecord.getUpdatedAt()) 30 | .build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/InvoiceResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import com.onurhizar.gamepass.model.entity.ContractRecord; 4 | import com.onurhizar.gamepass.model.entity.Invoice; 5 | import javax.persistence.ManyToOne; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.time.ZonedDateTime; 12 | 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | @Builder 17 | public class InvoiceResponse { 18 | private String id; 19 | private int fee; 20 | private ContractRecordResponse contractRecord; 21 | 22 | private ZonedDateTime createdAt; 23 | private ZonedDateTime updatedAt; 24 | 25 | public static InvoiceResponse fromEntity(Invoice invoice){ 26 | return InvoiceResponse.builder() 27 | .id(invoice.getId()) 28 | .fee(invoice.getFee()) 29 | .contractRecord(ContractRecordResponse.fromEntity(invoice.getContractRecord())) 30 | .createdAt(invoice.getCreatedAt()) 31 | .updatedAt(invoice.getUpdatedAt()) 32 | .build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/request/CreateUserRequest.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.request; 2 | 3 | import com.onurhizar.gamepass.model.entity.User; 4 | import javax.validation.constraints.Email; 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.Size; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | 11 | /** 12 | * This DTO is also used for updating user. 13 | * If it will be necessary, separate DTO will be implemented. 14 | */ 15 | @Getter 16 | @Setter 17 | @NoArgsConstructor 18 | public class CreateUserRequest { 19 | @NotBlank 20 | private String name; 21 | @NotBlank 22 | private String surname; 23 | @NotBlank 24 | @Email 25 | private String email; 26 | @NotBlank 27 | @Size(min = 6) 28 | private String password; 29 | 30 | public static User toEntity(CreateUserRequest request){ 31 | return User.builder() 32 | .name(request.getName()) 33 | .surname(request.getSurname()) 34 | .email(request.getEmail()) 35 | .passwordHash(request.getPassword()) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/config/ConfigRunner.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.config; 2 | 3 | import com.onurhizar.gamepass.model.entity.ContractRecord; 4 | import com.onurhizar.gamepass.service.*; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.CommandLineRunner; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | @RequiredArgsConstructor 12 | @Slf4j 13 | public class ConfigRunner implements CommandLineRunner { 14 | 15 | private final UserService userService; 16 | private final ContractRecordService contractRecordService; 17 | 18 | @Override 19 | public void run(String... args) throws Exception { 20 | ContractRecord contractRecord = createContractRecord("ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9", 21 | "66b455da-665a-4dc1-b4f1-b526c1c9ab4e" ); // contract record of admin 22 | contractRecordService.createInvoice(contractRecord); // create invoice for admin 23 | } 24 | 25 | 26 | private ContractRecord createContractRecord(String userId, String subscriptionId){ 27 | return userService.subscribe(userId,subscriptionId); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/Category.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import javax.persistence.*; 5 | import lombok.*; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | @Entity 11 | @Getter 12 | @Setter 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class Category extends BaseEntity { 17 | 18 | private String name; 19 | 20 | @OneToOne 21 | private Category parent; 22 | 23 | private boolean isSuperCategory; 24 | 25 | @ManyToMany(fetch = FetchType.LAZY) 26 | @JoinTable(name = "categories_games", 27 | joinColumns = @JoinColumn(name = "category_id"), 28 | inverseJoinColumns = @JoinColumn(name = "game_id") 29 | ) 30 | @JsonIgnoreProperties("categories") 31 | @Builder.Default 32 | private List games = new LinkedList<>(); 33 | 34 | 35 | @Override 36 | public String toString() { 37 | return "Category{" + 38 | "id='" + getId() + '\'' + 39 | "name='" + name + '\'' + 40 | ", isSuperCategory=" + isSuperCategory + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/containers/PostgresTestContainer.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.containers; 2 | 3 | import org.testcontainers.containers.PostgreSQLContainer; 4 | 5 | public class PostgresTestContainer extends PostgreSQLContainer { 6 | 7 | public static final String IMAGE_VERSION = "postgres:11.1"; 8 | public static final String DATABASE_NAME = "test_db"; 9 | public static PostgreSQLContainer container; 10 | 11 | public PostgresTestContainer(){ 12 | super(IMAGE_VERSION); 13 | } 14 | 15 | public static PostgreSQLContainer getInstance(){ 16 | if (container==null) container = new PostgresTestContainer() 17 | .withUsername("sa") 18 | .withPassword("sa") 19 | .withDatabaseName(DATABASE_NAME); 20 | return container; 21 | } 22 | 23 | @Override 24 | public void start(){ // TODO use @DynamicPropertySource 25 | super.start(); 26 | System.setProperty("DB_URL", container.getJdbcUrl()); 27 | System.setProperty("DB_USERNAME", container.getUsername()); 28 | System.setProperty("DB_PASSWORD", container.getPassword()); 29 | } 30 | 31 | @Override 32 | public void stop(){ 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/SubscriptionResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import com.onurhizar.gamepass.model.entity.Invoice; 4 | import com.onurhizar.gamepass.model.entity.Subscription; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.ZonedDateTime; 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @Builder 16 | public class SubscriptionResponse { 17 | private String id; 18 | 19 | private String name; 20 | private int monthlyFee; 21 | private int duration; 22 | private boolean active; 23 | 24 | private ZonedDateTime createdAt; 25 | private ZonedDateTime updatedAt; 26 | 27 | public static SubscriptionResponse fromEntity(Subscription subscription){ 28 | return SubscriptionResponse.builder() 29 | .id(subscription.getId()) 30 | .name(subscription.getName()) 31 | .monthlyFee(subscription.getMonthlyFee()) 32 | .duration(subscription.getDuration()) 33 | .active(subscription.isActive()) 34 | .createdAt(subscription.getCreatedAt()) 35 | .updatedAt(subscription.getUpdatedAt()) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/InvoiceController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.model.request.PaymentRequest; 4 | import com.onurhizar.gamepass.model.response.InvoiceResponse; 5 | import com.onurhizar.gamepass.service.InvoiceService; 6 | import javax.validation.Valid; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @RestController 14 | @RequestMapping("/invoice") 15 | @RequiredArgsConstructor 16 | public class InvoiceController { 17 | private final InvoiceService invoiceService; 18 | 19 | @GetMapping 20 | public List listInvoices(){ 21 | return invoiceService.listInvoices() 22 | .stream().map(InvoiceResponse::fromEntity).collect(Collectors.toList()); 23 | } 24 | 25 | @GetMapping("{id}") 26 | public InvoiceResponse getInvoice(@PathVariable String id){ 27 | return InvoiceResponse.fromEntity(invoiceService.findById(id)); 28 | } 29 | 30 | // POST /invoice/{id}/pay -> Invoice (owner of the invoice only) 31 | @PostMapping("{id}/pay") 32 | public InvoiceResponse payInvoice(@PathVariable String id, 33 | @Valid @RequestBody PaymentRequest paymentRequest){ 34 | return InvoiceResponse.fromEntity(invoiceService.payInvoice(id, paymentRequest)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/CategoryResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import com.onurhizar.gamepass.model.entity.Category; 4 | import com.onurhizar.gamepass.model.entity.Game; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | 8 | import java.time.ZonedDateTime; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @AllArgsConstructor 14 | @Getter 15 | public class CategoryResponse { 16 | 17 | private String id; 18 | private String name; 19 | private boolean isSuperCategory; 20 | private List games; 21 | private List parentCategories; 22 | private ZonedDateTime createdAt; 23 | private ZonedDateTime updatedAt; 24 | 25 | public static CategoryResponse fromEntity(Category category){ 26 | return new CategoryResponse( 27 | category.getId(), 28 | category.getName(), 29 | category.isSuperCategory(), 30 | category.getGames().stream().map(Game::getTitle).collect(Collectors.toList()), 31 | getParentCategories(category), 32 | category.getCreatedAt(), 33 | category.getUpdatedAt() 34 | ); 35 | } 36 | 37 | private static List getParentCategories(Category category) { 38 | List parents = new LinkedList<>(); 39 | 40 | Category parentNode = category.getParent(); 41 | while (parentNode != null){ 42 | parents.add(parentNode.getName()); 43 | parentNode = parentNode.getParent(); 44 | } 45 | return parents; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/CategoryController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | 4 | import com.onurhizar.gamepass.model.request.CreateCategoryRequest; 5 | import com.onurhizar.gamepass.model.response.CategoryResponse; 6 | import com.onurhizar.gamepass.model.request.UpdateCategoryRequest; 7 | import com.onurhizar.gamepass.service.CategoryService; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/category") 15 | @RequiredArgsConstructor 16 | public class CategoryController { 17 | 18 | private final CategoryService categoryService; 19 | 20 | @GetMapping 21 | public List listCategories(){ 22 | return categoryService.listCategories(); 23 | } 24 | 25 | @PostMapping("{parentId}") 26 | public CategoryResponse addCategory(@PathVariable String parentId, @RequestBody CreateCategoryRequest request){ 27 | return categoryService.addCategory(parentId, request.getName()); 28 | } 29 | 30 | @GetMapping("{id}") 31 | public CategoryResponse singleCategory(@PathVariable String id){ 32 | return CategoryResponse.fromEntity(categoryService.findCategoryById(id)); 33 | } 34 | 35 | @PutMapping("{id}") 36 | public CategoryResponse updateCategory(@PathVariable String id, @RequestBody UpdateCategoryRequest request){ 37 | return categoryService.updateCategory(id, request); 38 | } 39 | 40 | @DeleteMapping("{id}") 41 | public void deleteCategory(@PathVariable String id){ 42 | categoryService.deleteCategoryByAssignChildrenToGrandParent(id); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/response/UserResponse.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.response; 2 | 3 | import com.onurhizar.gamepass.model.entity.Category; 4 | import com.onurhizar.gamepass.model.entity.Game; 5 | import com.onurhizar.gamepass.model.entity.User; 6 | import com.onurhizar.gamepass.model.enums.UserRole; 7 | import lombok.*; 8 | 9 | import java.time.ZonedDateTime; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @AllArgsConstructor 14 | @Builder 15 | @Getter 16 | @Setter 17 | @NoArgsConstructor 18 | public class UserResponse { 19 | 20 | private String id; 21 | private String name; 22 | private String surname; 23 | private String email; 24 | private UserRole role; 25 | private boolean verified; 26 | 27 | private List favoriteGames; 28 | private List followedCategories; 29 | 30 | private ZonedDateTime createdAt; // TODO: extend with abstract class? But @Builder makes it harder to implement 31 | private ZonedDateTime updatedAt; 32 | 33 | public static UserResponse fromEntity(User user){ 34 | return UserResponse.builder() 35 | .id(user.getId()) 36 | .name(user.getName()) 37 | .surname(user.getSurname()) 38 | .email(user.getEmail()) 39 | .role(user.getRole()) 40 | .verified(user.isVerified()) 41 | .favoriteGames(user.getFavoriteGames().stream().map(Game::getTitle).collect(Collectors.toList())) 42 | .followedCategories(user.getFollowedCategories().stream().map(Category::getName).collect(Collectors.toList())) 43 | .createdAt(user.getCreatedAt()) 44 | .updatedAt(user.getUpdatedAt()) 45 | .build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/SubscriptionController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.model.request.CreateSubscriptionRequest; 4 | import com.onurhizar.gamepass.model.request.UpdateSubscriptionRequest; 5 | import com.onurhizar.gamepass.model.response.SubscriptionResponse; 6 | import com.onurhizar.gamepass.service.SubscriptionService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | 12 | @RestController 13 | @RequestMapping("/subscription") 14 | @RequiredArgsConstructor 15 | public class SubscriptionController { 16 | 17 | private final SubscriptionService subscriptionService; 18 | 19 | @GetMapping 20 | public List listSubscriptions(){ 21 | return subscriptionService.listSubscriptions(); 22 | } 23 | 24 | @GetMapping("/{id}") 25 | public SubscriptionResponse getSubscriptionById(@PathVariable String id){ 26 | return SubscriptionResponse.fromEntity(subscriptionService.findById(id)); 27 | } 28 | 29 | @PostMapping 30 | public SubscriptionResponse postSubscription(@RequestBody CreateSubscriptionRequest request){ 31 | return SubscriptionResponse.fromEntity(subscriptionService.addSubscription(request)); 32 | } 33 | 34 | @PutMapping("/{id}") 35 | public SubscriptionResponse updateSubscriptionById(@PathVariable String id, @RequestBody UpdateSubscriptionRequest request){ 36 | return SubscriptionResponse.fromEntity(subscriptionService.updateSubscription(id, request)); 37 | } 38 | 39 | @DeleteMapping("/{id}") 40 | public SubscriptionResponse deleteSubscriptionById(@PathVariable String id){ 41 | return SubscriptionResponse.fromEntity(subscriptionService.deleteById(id)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/GameController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.model.request.CreateGameRequest; 4 | import com.onurhizar.gamepass.model.response.GameResponse; 5 | import com.onurhizar.gamepass.service.GameService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springdoc.api.annotations.ParameterObject; 8 | import org.springframework.data.domain.Page; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | 13 | @RestController 14 | @RequestMapping("/game") 15 | @RequiredArgsConstructor 16 | public class GameController { 17 | 18 | private final GameService gameService; 19 | 20 | @GetMapping 21 | public Page listGames(@ParameterObject Pageable pageable) { 22 | return gameService.listGames(pageable); 23 | } 24 | 25 | @GetMapping("{gameId}") 26 | public GameResponse getGame(@PathVariable String gameId){ 27 | return GameResponse.fromEntity(gameService.getGame(gameId)); 28 | } 29 | 30 | @PostMapping 31 | public GameResponse postGame(@RequestBody CreateGameRequest createGameRequest){ 32 | return GameResponse.fromEntity(gameService.createGame(createGameRequest)); 33 | } 34 | 35 | @PutMapping("{gameId}") 36 | public GameResponse updateGame(@PathVariable String gameId, @RequestBody CreateGameRequest createGameRequest){ 37 | return GameResponse.fromEntity(gameService.updateGame(gameId,createGameRequest)); 38 | } 39 | 40 | @DeleteMapping("{gameId}") 41 | public void deleteGame(@PathVariable String gameId){ 42 | gameService.deleteGame(gameId); 43 | } 44 | 45 | @PostMapping("{gameId}/category/{categoryId}") 46 | public GameResponse addCategoryToGame(@PathVariable String gameId, @PathVariable String categoryId){ 47 | return GameResponse.fromEntity(gameService.addCategoryToGame(gameId, categoryId)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import javax.persistence.*; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.hibernate.annotations.GenericGenerator; 9 | import org.springframework.data.annotation.CreatedDate; 10 | import org.springframework.data.annotation.LastModifiedDate; 11 | 12 | import java.time.ZonedDateTime; 13 | import java.util.Objects; 14 | 15 | @MappedSuperclass 16 | @Getter 17 | @Setter 18 | public abstract class BaseEntity { 19 | 20 | @Id 21 | @GenericGenerator(name = "uuid2", strategy = "uuid2") 22 | @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "uuid2") 23 | @Column(length = 36, nullable = false, updatable = false) 24 | @Setter(AccessLevel.NONE) 25 | private String id; 26 | 27 | @Column(name = "created_at", nullable = false, updatable = false) 28 | @CreatedDate 29 | @Setter(AccessLevel.NONE) 30 | private ZonedDateTime createdAt; 31 | 32 | @Column(name = "updated_at", nullable = false) 33 | @LastModifiedDate 34 | @Setter(AccessLevel.NONE) 35 | private ZonedDateTime updatedAt; 36 | 37 | @PrePersist 38 | public void prePersist() { 39 | createdAt = ZonedDateTime.now(); 40 | updatedAt = ZonedDateTime.now(); 41 | } 42 | 43 | @PreUpdate 44 | public void preUpdate() { 45 | updatedAt = ZonedDateTime.now(); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) { 50 | if (o == null) return false; 51 | if (this == o) return true; 52 | 53 | if (!Objects.equals(getClass(), o.getClass())) { 54 | return false; 55 | } 56 | 57 | BaseEntity that = (BaseEntity) o; 58 | return this.id != null && Objects.equals(this.id, that.id); 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | return getClass().hashCode(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | - deploy 5 | 6 | services: 7 | - postgres:11.1 8 | - docker:dind 9 | 10 | variables: 11 | POSTGRES_DB: gamepass 12 | POSTGRES_USER: postgres 13 | POSTGRES_PASSWORD: secretpassword 14 | DB_URL: jdbc:postgresql://postgres:5432/gamepass 15 | 16 | build-job: 17 | stage: build 18 | image: maven:3-eclipse-temurin-8 19 | script: mvn clean package -DskipTests 20 | artifacts: 21 | paths: 22 | - target/gamepass-0.0.1-SNAPSHOT.jar 23 | 24 | integration-test-job: 25 | stage: test 26 | image: maven:3-eclipse-temurin-8 27 | script: mvn test 28 | variables: 29 | DOCKER_HOST: tcp://docker:2375 30 | 31 | docker-deploy-job: 32 | stage: deploy 33 | image: docker:dind 34 | variables: 35 | IMAGE_NAME: registry.gitlab.com/onurhizar/spring-boot-gamepass-app 36 | IMAGE_LABEL: gamepass-image 37 | before_script: 38 | - mkdir -p ~/.ssh 39 | - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa 40 | - chmod 600 ~/.ssh/id_rsa 41 | - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts 42 | - chmod 644 ~/.ssh/known_hosts 43 | - ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts 44 | - ssh $SSH_USERNAME@$SSH_HOST "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY" 45 | script: 46 | - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY 47 | - docker build -t $IMAGE_NAME . 48 | - docker push $IMAGE_NAME 49 | - ssh $SSH_USERNAME@$SSH_HOST "docker pull $IMAGE_NAME" 50 | - ssh $SSH_USERNAME@$SSH_HOST "docker stop $IMAGE_LABEL || true" # if stop command fails, don't exit the process 51 | - ssh $SSH_USERNAME@$SSH_HOST "docker container rm $IMAGE_LABEL || true" # if stop command fails, don't exit the process 52 | - ssh $SSH_USERNAME@$SSH_HOST "docker run -d --name $IMAGE_LABEL -p 8080:8080 -e DB_URL=jdbc:postgresql://172.31.39.102:5432/gamepass $IMAGE_NAME" 53 | dependencies: # only run after build-job 54 | - build-job 55 | only: 56 | - main 57 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/FilterChainConfig.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 5 | 6 | @Configuration 7 | @EnableWebSecurity 8 | public class FilterChainConfig { 9 | 10 | 11 | /** 12 | * 13 | * @Bean 14 | * SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { 15 | * http 16 | * .csrf().disable() 17 | * .authorizeHttpRequests() 18 | * .antMatchers("/login").permitAll() 19 | * .antMatchers("/register").permitAll() 20 | * .antMatchers("/swagger-ui/index.html").permitAll() // OpenAPI 21 | * .antMatchers("/v3/api-docs").permitAll() // OpenAPI 22 | * .antMatchers("/user/admin-or-self-test/**") // FOR TESTING PURPOSES, TODO : remove later 23 | * .hasAnyAuthority("ADMIN", "SELF") 24 | * .antMatchers(HttpMethod.DELETE, "/user/**").hasAuthority("ADMIN") 25 | * .antMatchers(HttpMethod.POST, "/category/**").hasAuthority("ADMIN") 26 | * .antMatchers(HttpMethod.PUT, "/category/**").hasAuthority("ADMIN") 27 | * .antMatchers(HttpMethod.DELETE, "/category/**").hasAuthority("ADMIN") 28 | * .anyRequest().permitAll() //.authenticated() 29 | * .and() 30 | * .sessionManagement() 31 | * .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 32 | * .and() 33 | * .authenticationProvider(authenticationProvider) 34 | * .addFilterBefore(selfFilter, UsernamePasswordAuthenticationFilter.class) 35 | * .addFilterBefore(jwtAuthFilter, SelfFilter.class); 36 | * 37 | * return http.build(); 38 | * } 39 | */ 40 | 41 | } -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/UserInterestService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.model.entity.Category; 5 | import com.onurhizar.gamepass.model.entity.Game; 6 | import com.onurhizar.gamepass.model.entity.User; 7 | import com.onurhizar.gamepass.model.response.CategoryResponse; 8 | import com.onurhizar.gamepass.model.response.GameResponse; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | public class UserInterestService { 18 | 19 | private final UserService userService; 20 | 21 | public List getFavoriteGamesOfUser(String userId) { 22 | User user = userService.findById(userId); 23 | return user.getFavoriteGames().stream() 24 | .map(GameResponse::fromEntity).collect(Collectors.toList()); 25 | } 26 | 27 | public Game getFavoriteGameDetailOfUser(String userId, String gameId) { 28 | User user = userService.findById(userId); 29 | for (Game game : user.getFavoriteGames()) { 30 | if (game.getId().equals(gameId)) return game; 31 | } 32 | throw new EntityNotFoundException(); 33 | } 34 | 35 | public List getFollowedCategoriesOfUser(String userId) { 36 | User user = userService.findById(userId); 37 | return user.getFollowedCategories().stream() 38 | .map(CategoryResponse::fromEntity).collect(Collectors.toList()); 39 | } 40 | 41 | public Category getFollowedCategoryDetailOfUser(String userId, String categoryId) { 42 | User user = userService.findById(userId); 43 | for (Category category : user.getFollowedCategories()) { 44 | if (category.getId().equals(categoryId)) return category; 45 | } 46 | throw new EntityNotFoundException(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.model.request.CreateUserRequest; 4 | import com.onurhizar.gamepass.model.response.InvoiceResponse; 5 | import com.onurhizar.gamepass.model.response.UserResponse; 6 | import com.onurhizar.gamepass.service.InvoiceService; 7 | import com.onurhizar.gamepass.service.UserService; 8 | import javax.validation.Valid; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | @RestController 16 | @RequestMapping("/user") 17 | @RequiredArgsConstructor 18 | public class UserController { 19 | 20 | private final UserService userService; 21 | private final InvoiceService invoiceService; 22 | 23 | @GetMapping 24 | public List listUsers(){ 25 | return userService.listUsers().stream().map(UserResponse::fromEntity).collect(Collectors.toList()); 26 | } 27 | 28 | @GetMapping("{userId}") 29 | public UserResponse getUser(@PathVariable String userId){ 30 | return UserResponse.fromEntity(userService.findById(userId)); 31 | } 32 | 33 | @PostMapping 34 | public UserResponse addUser(@Valid @RequestBody CreateUserRequest request){ 35 | return UserResponse.fromEntity(userService.addUser(request)); 36 | } 37 | 38 | @PutMapping("{userId}") 39 | public UserResponse updateUser(@Valid @RequestBody CreateUserRequest request, @PathVariable String userId){ 40 | return UserResponse.fromEntity(userService.updateUser(userId, request)); 41 | } 42 | 43 | @DeleteMapping("{userId}") 44 | public void deleteUser(@PathVariable String userId){ 45 | userService.deleteUser(userId); 46 | } 47 | 48 | /** subscribe or upgrade to a subscription */ 49 | @PostMapping("{userId}/subscribe/{subscriptionId}") 50 | public void subscribe(@PathVariable String userId, @PathVariable String subscriptionId){ 51 | userService.subscribe(userId,subscriptionId); 52 | } 53 | 54 | @GetMapping("{userId}/invoice") 55 | public List getInvoicesOfUser(@PathVariable String userId){ 56 | return invoiceService.getInvoicesOfUser(userId); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/SubscriptionService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.model.entity.Subscription; 5 | import com.onurhizar.gamepass.model.request.CreateSubscriptionRequest; 6 | import com.onurhizar.gamepass.model.request.UpdateSubscriptionRequest; 7 | import com.onurhizar.gamepass.model.response.SubscriptionResponse; 8 | import com.onurhizar.gamepass.repository.SubscriptionRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | public class SubscriptionService { 18 | private final SubscriptionRepository repository; 19 | 20 | public List listSubscriptions(){ 21 | return repository.findAll().stream().map(SubscriptionResponse::fromEntity).collect(Collectors.toList()); 22 | } 23 | 24 | public Subscription findById(String id){ 25 | return repository.findById(id).orElseThrow(EntityNotFoundException::new); 26 | } 27 | 28 | public Subscription deleteById(String id) { 29 | Subscription subscription = findById(id); 30 | subscription.setActive(false); // soft delete 31 | return repository.save(subscription); 32 | } 33 | 34 | public Subscription addSubscription(CreateSubscriptionRequest request) { 35 | Subscription subscription = Subscription.builder() 36 | .name(request.getName()) 37 | .monthlyFee(request.getMonthlyFee()) 38 | .duration(request.getDuration()) 39 | .isActive(request.isActive()) 40 | .build(); 41 | return repository.save(subscription); 42 | } 43 | 44 | public Subscription updateSubscription(String id, UpdateSubscriptionRequest request) { 45 | Subscription subscription = findById(id); 46 | subscription.setName(request.getName()); 47 | subscription.setMonthlyFee(request.getMonthlyFee()); 48 | subscription.setDuration(request.getDuration()); 49 | subscription.setActive(request.isActive()); 50 | return repository.save(subscription); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import com.onurhizar.gamepass.model.entity.User; 4 | import io.jsonwebtoken.Claims; 5 | import io.jsonwebtoken.Jws; 6 | import io.jsonwebtoken.Jwts; 7 | import io.jsonwebtoken.io.Decoders; 8 | import io.jsonwebtoken.security.Keys; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.security.Key; 12 | import java.util.*; 13 | 14 | @Service 15 | public class JwtService { 16 | private final String SECRET_KEY; 17 | private final String TOKEN_ISSUER; 18 | private final int EXPIRE_HOURS; // token expires in given hours 19 | 20 | public JwtService(SecurityConstants securityConstants){ 21 | SECRET_KEY = securityConstants.getJWT_SECRET_KEY(); 22 | TOKEN_ISSUER = securityConstants.getJWT_TOKEN_ISSUER(); 23 | EXPIRE_HOURS = securityConstants.getJWT_EXPIRATION_HOURS(); 24 | } 25 | 26 | public Key getSigningKey() { 27 | byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); 28 | return Keys.hmacShaKeyFor(keyBytes); 29 | } 30 | 31 | 32 | public String generateToken(String username){ 33 | return Jwts.builder() 34 | .signWith(getSigningKey()) 35 | .setIssuer(TOKEN_ISSUER) 36 | .setSubject(username) 37 | .setIssuedAt(new Date(System.currentTimeMillis())) 38 | .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * EXPIRE_HOURS)) 39 | .compact(); 40 | } 41 | 42 | // TODO: what if dont specify algorithm? vulnerability by giving json with no 43 | // alg 44 | public String generateToken(User user) { 45 | return generateToken(user.getUsername()); 46 | } 47 | 48 | /** 49 | * Takes token from Bearer header and returns Jws parsed claims. 50 | * 51 | * @param authHeader whole header like "Bearer eyJhbGciOiJ.." 52 | * @return Jws Parsed Claims 53 | */ 54 | public Jws verifyAuthHeader(String authHeader) { 55 | if (authHeader == null || !authHeader.startsWith("Bearer ")) return null; 56 | 57 | String token = authHeader.substring(7); 58 | 59 | return Jwts.parserBuilder() 60 | .setSigningKey(getSigningKey()) 61 | .build() 62 | .parseClaimsJws(token); 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import com.onurhizar.gamepass.repository.UserRepository; 4 | import com.onurhizar.gamepass.model.entity.User; 5 | 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.security.authentication.AuthenticationProvider; 12 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 13 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import org.springframework.security.authentication.AuthenticationManager; 17 | 18 | @Configuration 19 | @RequiredArgsConstructor 20 | public class SecurityConfig { 21 | 22 | private final UserRepository userRepository; 23 | 24 | @Bean 25 | public UserDetailsService userDetailsService() { 26 | return username -> { 27 | User user = userRepository.findByEmail(username); 28 | if (user == null) 29 | throw new UsernameNotFoundException(username); 30 | 31 | return User.builder().email(user.getUsername()) 32 | .passwordHash(user.getPassword()) 33 | .role(user.getRole()) 34 | .name(user.getName()) 35 | .build(); 36 | }; 37 | } 38 | 39 | @Bean 40 | public AuthenticationProvider authenticationProvider() { 41 | DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 42 | authProvider.setUserDetailsService(userDetailsService()); 43 | authProvider.setPasswordEncoder(passwordEncoder()); 44 | return authProvider; 45 | 46 | } 47 | 48 | @Bean 49 | public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { 50 | return config.getAuthenticationManager(); 51 | } 52 | 53 | @Bean 54 | public PasswordEncoder passwordEncoder() { 55 | return new BCryptPasswordEncoder(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import com.onurhizar.gamepass.model.entity.User; 4 | import com.onurhizar.gamepass.repository.UserRepository; 5 | import io.jsonwebtoken.Claims; 6 | import io.jsonwebtoken.ExpiredJwtException; 7 | import io.jsonwebtoken.Jws; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.lang.NonNull; 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 16 | import org.springframework.security.core.Authentication; 17 | import org.springframework.security.core.context.SecurityContextHolder; 18 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.web.filter.OncePerRequestFilter; 21 | 22 | import java.io.IOException; 23 | 24 | @Component 25 | @RequiredArgsConstructor 26 | @Slf4j 27 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 28 | private final JwtService jwtService; 29 | private final UserRepository userRepository; 30 | 31 | 32 | @Override 33 | protected void doFilterInternal( 34 | @NonNull HttpServletRequest request, 35 | @NonNull HttpServletResponse response, 36 | @NonNull FilterChain filterChain) throws ServletException, IOException { 37 | 38 | final String authorizationHeader = request.getHeader("Authorization"); 39 | 40 | try { 41 | Jws jws = jwtService.verifyAuthHeader(authorizationHeader); 42 | Authentication authentication = claimsToAuthentication(jws); 43 | SecurityContextHolder.getContext().setAuthentication(authentication); 44 | } catch (ExpiredJwtException e) { 45 | // TODO throw specific exception 46 | log.warn("JWT is expired"); 47 | } 48 | 49 | filterChain.doFilter(request, response); 50 | } 51 | 52 | private Authentication claimsToAuthentication(Jws jws) { 53 | if (jws == null) return null; 54 | String email = jws.getBody().getSubject(); // email 55 | User user = userRepository.findByEmail(email); 56 | if(user == null) throw new UsernameNotFoundException(email); // even though jwt is valid, email does not exist 57 | return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/ContractRecordService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.model.entity.ContractRecord; 4 | import com.onurhizar.gamepass.model.entity.Invoice; 5 | import com.onurhizar.gamepass.model.entity.Subscription; 6 | import com.onurhizar.gamepass.model.entity.User; 7 | import com.onurhizar.gamepass.repository.ContractRecordRepository; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.transaction.Transactional; 13 | import java.time.ZonedDateTime; 14 | import java.util.List; 15 | 16 | @Service 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | public class ContractRecordService { 20 | private final ContractRecordRepository repository; 21 | private final InvoiceService invoiceService; 22 | 23 | /** Copies subscription details to avoid fee change (denormalization) */ 24 | public ContractRecord addContract(User user, Subscription subscription){ 25 | return repository.save(ContractRecord.builder() 26 | .name(subscription.getName()) 27 | .monthlyFee(subscription.getMonthlyFee()) 28 | .duration(subscription.getDuration()) 29 | .active(subscription.isActive()) 30 | .user(user) 31 | .createdDate(ZonedDateTime.now()) 32 | .build()); 33 | } 34 | 35 | public ContractRecord updateContract(ContractRecord contractRecord, Subscription subscription){ 36 | contractRecord.setName(subscription.getName()); 37 | contractRecord.setDuration(subscription.getDuration()); 38 | contractRecord.setMonthlyFee(subscription.getMonthlyFee()); 39 | return repository.save(contractRecord); 40 | } 41 | 42 | 43 | @Transactional 44 | public void createInvoice(ContractRecord contractRecord){ 45 | if (!contractRecord.isActive()) return; // do not create invoice if contract is not active 46 | 47 | int duration = contractRecord.getDuration(); // remaining invoices 48 | contractRecord.setDuration(duration-1); // decrease the duration 49 | if (duration==1) contractRecord.setActive(false); // deactivate if duration is 1 50 | 51 | invoiceService.addInvoice(new Invoice(contractRecord.getMonthlyFee(),contractRecord)); 52 | log.info("Invoice created, remaining: " + (duration-1)); 53 | 54 | repository.save(contractRecord); // TODO : no need to contractRepository save? 55 | } 56 | 57 | public List findActiveContractRecords() { 58 | return repository.findByActiveIsTrue(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GamePass App 2 | 3 | This is a Spring Boot application that clones the XBOX GamePass platform as a practice. 4 | 5 | 6 | 7 | ## Table of Contents 8 | 9 | - [Features](#features) 10 | - [Installation](#installation) 11 | - [Prerequisites](#prerequisites) 12 | - [Installation Steps](#installation-steps) 13 | - [Project Steps](#project-steps) 14 | - [Sample Environment Variables](#sample-environment-variables) 15 | - [License](#license) 16 | 17 | 18 | 19 | ## Features 20 | 21 | - JWT Authentication 22 | - Exception Handling 23 | - Validation 24 | - Swagger OpenAPI 25 | - Subscription, Payment and Invoice 26 | - Integration Testing 27 | - TestContainers 28 | - Scheduling 29 | - Docker Image 30 | - Pagination 31 | 32 | 33 | 34 | ## Installation 35 | 36 | ### Prerequisites 37 | - Docker 38 | 39 | ### Installation Steps 40 | - `git clone https://gitlab.com/onurhizar/spring-boot-gamepass-app` 41 | - `cd spring-boot-gamepass-app` 42 | - `./mvnw clean package -DskipTests` to build JAR file 43 | - `docker compose up` to run app 44 | - `http://localhost:8080/swagger-ui.html` to see Swagger UI of API routes 45 | - `docker compose down` to stop the app and remove the images 46 | 47 | 48 | 49 | ## Project Steps 50 | - [x] Game entity and database connection 51 | - [x] Populate sample data with data.sql 52 | - [x] Hierarchical Categories for Game entity 53 | - [x] Exception Handling (initialized) 54 | - [x] ManyToMany Bidirectional relationship on Category-Game 55 | - [x] DTO for bidirectional entities 56 | - [x] CRUD on Category with DTO 57 | - [x] User entity 58 | - [x] Auth (JWT) 59 | - [x] OpenAPI & Swagger 60 | - [x] JWT environment variable constants 61 | - [x] Validation 62 | - [x] Integration Testing (initialized) 63 | - [x] User Interests : Favorite / Unfavorite Games 64 | - [x] User Verification & Password Reset 65 | - [x] Admin or Self Authorization Logic 66 | - [x] Scheduling 67 | - [x] Subscription and ContractRecord 68 | - [x] Invoice and Payment 69 | - [x] TestContainers 70 | - [x] CommonEntity for createdAt and updatedAt 71 | - [x] Dockerfile 72 | - [x] Other entities 73 | - [x] Business Logic 74 | - [x] Cron Expressions for Production 75 | - [x] Downgrade from Java 17 to Java 8 76 | - [x] Pagination 77 | - [ ] Review and Refactoring 78 | 79 | 80 | 81 | ## Sample Environment Variables 82 | 83 | ```text 84 | DB_URL=jdbc:postgresql://localhost:5432/gamepass 85 | DB_USERNAME=postgres 86 | DB_PASSWORD=secretpassword 87 | JWT_ISSUER=GamePass 88 | JWT_EXPIRATION_HOURS=12 89 | JWT_SECRET_KEY=104E635266556A586E3272367538722F413F4438472B4B6250645367266B5275 90 | UPLOAD_DIRECTORY=/tmp 91 | ``` 92 | 93 | 94 | 95 | ## License 96 | 97 | This application is licensed under the MIT License. See the LICENSE file for more information. 98 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/ScheduleService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.model.entity.ContractRecord; 4 | import com.onurhizar.gamepass.model.entity.Invoice; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.HashSet; 12 | import java.util.List; 13 | 14 | @Service 15 | @EnableScheduling 16 | @RequiredArgsConstructor 17 | @Slf4j 18 | public class ScheduleService { 19 | 20 | private final ContractRecordService contractRecordService; 21 | private final InvoiceService invoiceService; 22 | private final UserService userService; 23 | 24 | // FOR TESTING PURPOSES you can use "0 */2 * * * *" to run every 2 minutes (even minutes) 25 | @Scheduled(cron = "0 0 0 L * *") 26 | public void createInvoicesInLastDayOfMonth(){ 27 | List activeContractRecords = contractRecordService.findActiveContractRecords(); 28 | log.info("Create invoices for active contract records"); 29 | log.info("Active contract records: "+activeContractRecords.size()); 30 | 31 | for (ContractRecord contractRecord : activeContractRecords) { 32 | contractRecordService.createInvoice(contractRecord); 33 | } 34 | 35 | } 36 | 37 | // FOR TESTING PURPOSES you can use "0 1/2 * * * *" to run every 2 minutes (odd minutes) 38 | @Scheduled(cron = "0 0 0 15 * *") 39 | public void checkUnpaidPastInvoicesIn15thDayOfMonth(){ 40 | log.info("Check for non paid invoices and downgrade corresponding users role to guest"); 41 | HashSet userIds = findPastInvoicesAndExtractTheirUserIds(); 42 | downgradeUsersToGuestStatus(userIds); 43 | } 44 | 45 | private HashSet findPastInvoicesAndExtractTheirUserIds(){ 46 | HashSet userIds = new HashSet<>(); 47 | 48 | List oldInvoices = invoiceService.findInvoicesBy15DaysOld(); 49 | log.info("Old Non Paid invoices: "+oldInvoices.size()); 50 | for (Invoice invoice : oldInvoices) { 51 | log.info("Invoice: "+invoice); 52 | log.info("UserID: "+invoice.getContractRecord().getUser().getId()); 53 | log.info("User Email: "+invoice.getContractRecord().getUser().getEmail()); 54 | userIds.add(invoice.getContractRecord().getUser().getId()); 55 | } 56 | 57 | return userIds; 58 | } 59 | 60 | private void downgradeUsersToGuestStatus(HashSet userIds){ 61 | for (String userId : userIds) userService.downgradeNonAdminUserRoleToGuest(userId); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/UserInterestController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.model.response.CategoryResponse; 4 | import com.onurhizar.gamepass.model.response.GameResponse; 5 | import com.onurhizar.gamepass.model.response.UserResponse; 6 | import com.onurhizar.gamepass.service.UserInterestService; 7 | import com.onurhizar.gamepass.service.UserService; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/user/{userId}") 15 | @RequiredArgsConstructor 16 | public class UserInterestController { 17 | 18 | private final UserService userService; 19 | private final UserInterestService userInterestService; 20 | 21 | @GetMapping("/game") 22 | public List getFavoriteGamesOfUser(@PathVariable String userId){ 23 | return userInterestService.getFavoriteGamesOfUser(userId); 24 | } 25 | 26 | @GetMapping("/game/{gameId}") 27 | public GameResponse getFavoriteGameDetailOfUser(@PathVariable String userId, @PathVariable String gameId){ 28 | return GameResponse.fromEntity(userInterestService.getFavoriteGameDetailOfUser(userId, gameId)); 29 | } 30 | 31 | @GetMapping("/category") 32 | public List getFollowedCategoriesOfUser(@PathVariable String userId){ 33 | return userInterestService.getFollowedCategoriesOfUser(userId); 34 | } 35 | 36 | @GetMapping("/category/{categoryId}") 37 | public CategoryResponse getFollowedCategoryDetailOfUser(@PathVariable String userId, @PathVariable String categoryId) { 38 | return CategoryResponse.fromEntity( 39 | userInterestService.getFollowedCategoryDetailOfUser(userId, categoryId) 40 | ); 41 | } 42 | 43 | 44 | @PostMapping("/game/{gameId}/favorite") 45 | public UserResponse favoriteGame(@PathVariable String userId, @PathVariable String gameId){ 46 | return UserResponse.fromEntity(userService.favoriteGame(userId, gameId)); 47 | } 48 | 49 | @PostMapping("/game/{gameId}/unfavorite") 50 | public UserResponse unfavoriteGame(@PathVariable String userId, @PathVariable String gameId){ 51 | return UserResponse.fromEntity(userService.unfavoriteGame(userId, gameId)); 52 | } 53 | 54 | @PostMapping("/category/{categoryId}/follow") 55 | public UserResponse followCategory(@PathVariable String userId, @PathVariable String categoryId){ 56 | return UserResponse.fromEntity(userService.followCategory(userId, categoryId)); 57 | } 58 | 59 | @PostMapping("/category/{categoryId}/unfollow") 60 | public UserResponse unfollowCategory(@PathVariable String userId, @PathVariable String categoryId){ 61 | return UserResponse.fromEntity(userService.unfollowCategory(userId, categoryId)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/model/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.model.entity; 2 | 3 | import com.onurhizar.gamepass.model.enums.UserRole; 4 | import javax.persistence.*; 5 | import lombok.*; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | 10 | import java.time.ZonedDateTime; 11 | import java.util.*; 12 | 13 | @Entity 14 | @Table(name = "users") 15 | @Builder 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class User extends BaseEntity implements UserDetails { 21 | 22 | private String name; 23 | private String surname; 24 | private String email; 25 | private String passwordHash; 26 | 27 | @Enumerated(EnumType.STRING) 28 | @Builder.Default 29 | private UserRole role = UserRole.GUEST; 30 | 31 | @Builder.Default 32 | private boolean verified = false; 33 | 34 | @Builder.Default 35 | private String verificationCode = UUID.randomUUID().toString(); 36 | @Builder.Default 37 | private ZonedDateTime verificationCodeExpireDate = ZonedDateTime.now().plusDays(1); // TODO fixed value? 38 | 39 | private String recoveryCode; 40 | private ZonedDateTime recoveryCodeExpireDate; 41 | 42 | @ManyToMany 43 | @JoinTable(name = "users_games", 44 | joinColumns = @JoinColumn(name = "user_id"), 45 | inverseJoinColumns = @JoinColumn(name = "game_id") 46 | ) 47 | @Builder.Default 48 | private List favoriteGames = new LinkedList<>(); 49 | 50 | @ManyToMany 51 | @JoinTable(name = "users_categories", 52 | joinColumns = @JoinColumn(name = "user_id"), 53 | inverseJoinColumns = @JoinColumn(name = "category_id") 54 | ) 55 | @Builder.Default 56 | private List followedCategories = new LinkedList<>(); 57 | 58 | @OneToOne(mappedBy = "user") 59 | private ContractRecord contractRecord; 60 | 61 | 62 | // below are for security package 63 | @Override 64 | public Collection getAuthorities() { 65 | return Arrays.asList(new SimpleGrantedAuthority(role.name())); 66 | } 67 | 68 | @Override 69 | public String getPassword() { 70 | return passwordHash; 71 | } 72 | 73 | @Override 74 | public String getUsername() { 75 | return email; 76 | } 77 | 78 | @Override 79 | public boolean isAccountNonExpired() { 80 | return true; 81 | } 82 | 83 | @Override 84 | public boolean isAccountNonLocked() { 85 | return true; 86 | } 87 | 88 | @Override 89 | public boolean isCredentialsNonExpired() { 90 | return true; 91 | } 92 | 93 | @Override 94 | public boolean isEnabled() { 95 | return true; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/SelfFilter.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import com.onurhizar.gamepass.model.entity.User; 4 | import javax.servlet.FilterChain; 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | 18 | import java.io.IOException; 19 | import java.util.LinkedList; 20 | import java.util.List; 21 | 22 | @Component 23 | @RequiredArgsConstructor 24 | /** 25 | * This filter checks the Authentication object from SecurityContext and takes user id from it. 26 | * Then compares requested URL path if contains user's id. 27 | * If there is a match, it appends "SELF" authority to current user. 28 | * 29 | * Example: /user/aa59d163-5e7e-4290-b6ac-b901b0b4543a 30 | * If the authenticated user has the same ID, it can reach the resource, 31 | * so that we can implement "ADMIN OR SELF" logic on some routes. 32 | * 33 | * Note: this filter needs to be improved since it only checks url "contains" the id. 34 | * This is so generic form of controlling, 35 | * instead, use parsing and checking if it comes after "user" path. 36 | */ 37 | public class SelfFilter extends OncePerRequestFilter { 38 | 39 | @Override 40 | protected void doFilterInternal( 41 | @NonNull HttpServletRequest request, 42 | @NonNull HttpServletResponse response, 43 | @NonNull FilterChain filterChain) throws ServletException, IOException { 44 | 45 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 46 | 47 | if (authentication != null) { 48 | User user = (User) authentication.getPrincipal(); 49 | String userId = user.getId(); 50 | 51 | if (request.getRequestURI().contains(userId)){ 52 | List updatedAuthorities = new LinkedList<>(authentication.getAuthorities()); 53 | updatedAuthorities.add(new SimpleGrantedAuthority("SELF")); // TODO : use enum later 54 | Authentication newAuthentication = new UsernamePasswordAuthenticationToken( 55 | authentication.getPrincipal(), 56 | authentication.getCredentials(), 57 | updatedAuthorities 58 | ); 59 | SecurityContextHolder.getContext().setAuthentication(newAuthentication); 60 | } 61 | } 62 | 63 | filterChain.doFilter(request, response); 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/controller/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.model.request.auth.LoginRequest; 4 | import com.onurhizar.gamepass.model.request.auth.RegisterRequest; 5 | import com.onurhizar.gamepass.model.response.AuthenticationResponse; 6 | import com.onurhizar.gamepass.service.AuthenticationService; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.validation.Valid; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | 15 | @RestController 16 | @RequiredArgsConstructor 17 | public class AuthenticationController { 18 | 19 | private final AuthenticationService authenticationService; 20 | 21 | @PostMapping("/register") 22 | public AuthenticationResponse register(@Valid @RequestBody RegisterRequest request) { 23 | return authenticationService.register(request); 24 | } 25 | 26 | @PostMapping("/login") 27 | public AuthenticationResponse login(@Valid @RequestBody LoginRequest request) { 28 | return authenticationService.login(request); 29 | } 30 | 31 | 32 | @GetMapping("/verify") // TODO: in the API contract it is said POST method but I think GET is more intuitive 33 | public void verify(@RequestParam String code){ 34 | authenticationService.verify(code); 35 | } 36 | 37 | @GetMapping("/verify/email") 38 | // TODO email service is later, for now it just sends HTTP response 39 | public String sendVerificationCodeToEmail(@RequestParam String email, HttpServletRequest request){ 40 | String domain = request.getServerName(); 41 | int port = request.getServerPort(); 42 | String code = authenticationService.sendVerificationCodeToEmail(email); 43 | if (port == 80) return "Your verification link is http://" + domain + "/verify?code=" + code; 44 | else if (port == 443) return "Your verification link is https://" + domain + "/verify?code=" + code; 45 | return "Your verification link is http://" + domain + ":" + port + "/verify?code=" + code; 46 | } 47 | 48 | 49 | @GetMapping("/recover") 50 | // sends a new random generated password 51 | public String recoverPassword(@RequestParam String code){ 52 | String newPassword = authenticationService.recoverPasswordByGeneratingNew(code); 53 | return "Your new password is "+newPassword; 54 | } 55 | 56 | @GetMapping("/recover/email/{userId}") 57 | // TODO email service is later, for now it just sends HTTP response 58 | public String sendRecoveryCodeToEmail(@PathVariable String userId, HttpServletRequest request){ 59 | String domain = request.getServerName(); 60 | int port = request.getServerPort(); 61 | String code = authenticationService.createRecoveryCode(userId); 62 | 63 | if (port == 80) return "Your verification link is http://" + domain + "/recover?code=" + code; 64 | else if (port == 443) return "Your verification link is https://" + domain + "/recover?code=" + code; 65 | return "Your verification link is http://" + domain + ":" + port + "/recover?code=" + code; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/exception/GlobalControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.exception; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.http.converter.HttpMessageNotReadableException; 7 | import org.springframework.security.authentication.BadCredentialsException; 8 | import org.springframework.web.bind.MethodArgumentNotValidException; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | 14 | @RestControllerAdvice 15 | @Slf4j 16 | public class GlobalControllerAdvice { 17 | 18 | @ExceptionHandler(EntityNotFoundException.class) 19 | public ResponseEntity handleException(EntityNotFoundException e, HttpServletRequest request) { 20 | logRequesterDetails(request); 21 | return new ResponseEntity<>(e.getMessage(), e.getStatusCode()); 22 | } 23 | 24 | @ExceptionHandler(UnacceptableRequestException.class) 25 | public ResponseEntity handleException(UnacceptableRequestException e, HttpServletRequest request) { 26 | logRequesterDetails(request); 27 | return new ResponseEntity<>(e.getMessage(), e.getStatusCode()); 28 | } 29 | 30 | @ExceptionHandler(BadCredentialsException.class) 31 | public ResponseEntity handleException(BadCredentialsException e, HttpServletRequest request) { 32 | logRequesterDetails(request); 33 | return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); 34 | } 35 | 36 | @ExceptionHandler(HttpMessageNotReadableException.class) 37 | public ResponseEntity handleException(HttpMessageNotReadableException e, HttpServletRequest request) { 38 | logRequesterDetails(request); 39 | return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); 40 | } 41 | 42 | @ExceptionHandler(MethodArgumentNotValidException.class) 43 | public ResponseEntity handleException(MethodArgumentNotValidException e, HttpServletRequest request) { 44 | logRequesterDetails(request); 45 | return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); 46 | } 47 | 48 | @ExceptionHandler(Exception.class) 49 | public ResponseEntity handleException(Exception e, HttpServletRequest request) { 50 | log.error("Unhandled exception occurred", e); 51 | logRequesterDetails(request); 52 | log.info("Internal Server Error: " + e.getMessage()); 53 | return new ResponseEntity<>("internal server error", HttpStatus.INTERNAL_SERVER_ERROR); 54 | } 55 | 56 | private void logRequesterDetails(HttpServletRequest request){ 57 | if (request == null) return; 58 | log.error("Request URI: " + request.getRequestURI()); 59 | log.error("Request Method: " + request.getMethod()); 60 | log.error("Request Query String: " + request.getQueryString()); 61 | log.error("Request Content Type: " + request.getContentType()); 62 | log.error("Request User Agent Header: " + request.getHeader("User-Agent")); 63 | log.error("Request IP: " + request.getRemoteAddr()); 64 | log.error("Request X-Forwarded-For: " + request.getHeader("X-Forwarded-For")); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/GameService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.model.entity.Category; 5 | import com.onurhizar.gamepass.model.entity.Game; 6 | import com.onurhizar.gamepass.model.entity.User; 7 | import com.onurhizar.gamepass.model.request.CreateGameRequest; 8 | import com.onurhizar.gamepass.model.response.GameResponse; 9 | import com.onurhizar.gamepass.repository.CategoryRepository; 10 | import com.onurhizar.gamepass.repository.GameRepository; 11 | import com.onurhizar.gamepass.repository.UserRepository; 12 | import lombok.AllArgsConstructor; 13 | import org.springframework.data.domain.Page; 14 | import org.springframework.data.domain.Pageable; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.util.List; 18 | 19 | @Service 20 | @AllArgsConstructor 21 | public class GameService { 22 | 23 | private GameRepository repository; 24 | private CategoryRepository categoryRepository; 25 | private UserRepository userRepository; 26 | 27 | public void saveGame(Game game){ 28 | repository.save(game); 29 | } 30 | 31 | public Game findGameById(String gameId){ 32 | return repository.findById(gameId).orElseThrow(EntityNotFoundException::new); 33 | } 34 | 35 | public Page listGames(Pageable pageable){ 36 | return repository.findAll(pageable).map(GameResponse::fromEntity); 37 | } 38 | 39 | public Game getGame(String gameId) { 40 | return findGameById(gameId); 41 | } 42 | 43 | public Game createGame(CreateGameRequest createGameRequest) { 44 | Game game = Game.builder() 45 | .title(createGameRequest.getTitle()) 46 | .build(); 47 | saveGame(game); 48 | return game; 49 | } 50 | 51 | public Game updateGame(String gameId, CreateGameRequest createGameRequest) { 52 | Game game = findGameById(gameId); 53 | game.setTitle(createGameRequest.getTitle()); 54 | saveGame(game); 55 | return game; 56 | } 57 | 58 | public void deleteGame(String gameId) { 59 | Game game = findGameById(gameId); // throw error if not found 60 | 61 | // remove associations 62 | List categories = categoryRepository.findCategoriesByGames(game); 63 | for (Category category : categories) { 64 | category.getGames().remove(game); 65 | categoryRepository.save(category); 66 | } 67 | 68 | List users = userRepository.findByFavoriteGames(game); 69 | for (User user : users) { 70 | user.getFavoriteGames().remove(game); 71 | userRepository.save(user); 72 | } 73 | 74 | repository.deleteById(gameId); 75 | } 76 | 77 | public Game addCategoryToGame(String gameId, String categoryId) { 78 | Game game = findGameById(gameId); 79 | Category category = categoryRepository.findById(categoryId).orElseThrow(EntityNotFoundException::new); 80 | 81 | // check if already exists 82 | if (game.getCategories().contains(category)) return game; 83 | 84 | game.getCategories().add(category); 85 | category.getGames().add(game); 86 | saveGame(game); 87 | categoryRepository.save(category); 88 | return game; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.5.4 9 | 10 | 11 | com.onurhizar 12 | gamepass 13 | 0.0.1-SNAPSHOT 14 | gamepass 15 | Game Pass Service 16 | 17 | 1.8 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 2.5.4 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 2.5.4 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-security 37 | 2.5.4 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-devtools 43 | runtime 44 | true 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-validation 50 | 2.5.4 51 | 52 | 53 | 54 | org.postgresql 55 | postgresql 56 | 42.2.27 57 | runtime 58 | 59 | 60 | 61 | org.projectlombok 62 | lombok 63 | RELEASE 64 | compile 65 | 66 | 67 | 68 | org.springdoc 69 | springdoc-openapi-ui 70 | 1.7.0 71 | 72 | 73 | 74 | io.jsonwebtoken 75 | jjwt-api 76 | 0.11.5 77 | 78 | 79 | io.jsonwebtoken 80 | jjwt-impl 81 | 0.11.5 82 | 83 | 84 | io.jsonwebtoken 85 | jjwt-jackson 86 | 0.11.5 87 | 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-starter-test 93 | 2.5.4 94 | test 95 | 96 | 97 | org.testcontainers 98 | postgresql 99 | 1.18.0 100 | test 101 | 102 | 103 | org.testcontainers 104 | junit-jupiter 105 | 1.18.0 106 | test 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | org.springframework.boot 115 | spring-boot-maven-plugin 116 | 117 | 118 | 119 | org.projectlombok 120 | lombok 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/security/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.security; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.security.authentication.AuthenticationProvider; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | import org.springframework.security.config.http.SessionCreationPolicy; 13 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 14 | import org.springframework.security.crypto.password.PasswordEncoder; 15 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 16 | import org.springframework.web.cors.CorsConfiguration; 17 | import org.springframework.web.cors.CorsConfigurationSource; 18 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 19 | 20 | @EnableWebSecurity 21 | @Configuration 22 | @RequiredArgsConstructor 23 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 24 | 25 | private final JwtAuthenticationFilter jwtAuthFilter; 26 | private final SelfFilter selfFilter; 27 | private final AuthenticationProvider authenticationProvider; 28 | 29 | @Override 30 | protected void configure(HttpSecurity http) throws Exception { 31 | http.cors().and().csrf().disable() 32 | .authorizeRequests() 33 | 34 | // ADMIN OR SELF ROUTES RESTRICTIONS 35 | .antMatchers(HttpMethod.GET, "/user/*", "/user/*/invoice").hasAnyAuthority("ADMIN", "SELF") 36 | .antMatchers(HttpMethod.PUT, "/user/*").hasAnyAuthority("ADMIN", "SELF") 37 | 38 | 39 | // SELF ONLY ROUTES RESTRICTIONS 40 | .antMatchers(HttpMethod.GET, "/user/*/game", "/user/*/game/*", 41 | "/user/*/category", "/user/*/category/*").hasAuthority("SELF") 42 | .antMatchers(HttpMethod.POST, "/user/*/subscribe/*","/user/*/game/*/favorite", 43 | "/user/*/game/*/unfavorite", "/user/*/category/*/follow", 44 | "/user/*/category/*/unfollow", "/invoice/*/pay").hasAuthority("SELF") 45 | 46 | 47 | // ADMIN ONLY ROUTES RESTRICTIONS 48 | .antMatchers(HttpMethod.GET, "/user","/invoice","/invoice/**").hasAuthority("ADMIN") 49 | .antMatchers(HttpMethod.POST, "/subscription","/category/**","/game").hasAuthority("ADMIN") 50 | .antMatchers(HttpMethod.PUT, "/subscription/**","/category/**","/game/**").hasAuthority("ADMIN") 51 | .antMatchers(HttpMethod.DELETE, "/subscription/**","/category/**","/game/**", "/user/**") 52 | .hasAuthority("ADMIN") 53 | 54 | 55 | // PUBLIC ROUTES 56 | .antMatchers("/", "/login", "/register").permitAll() 57 | .antMatchers("/verify/**", "/recover/**").permitAll() 58 | .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // Swagger OpenAPI 59 | .antMatchers(HttpMethod.GET, "/subscription", "/subscription/*", 60 | "/category", "/category/*", "/game", "/game/*" ).permitAll() 61 | 62 | 63 | .anyRequest().authenticated() 64 | .and() 65 | .sessionManagement() 66 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 67 | .and() 68 | .authenticationProvider(authenticationProvider) 69 | .addFilterBefore(selfFilter, UsernamePasswordAuthenticationFilter.class) 70 | .addFilterBefore(jwtAuthFilter, SelfFilter.class); 71 | } 72 | 73 | @Bean 74 | public CorsConfigurationSource corsConfigurationSource() { 75 | final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 76 | CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues(); 77 | corsConfiguration.addAllowedMethod("PUT"); 78 | corsConfiguration.addAllowedMethod("DELETE"); 79 | source.registerCorsConfiguration("/**", corsConfiguration); 80 | return source; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/controller/GameControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.abstracts.AbstractIntegrationTest; 4 | import com.onurhizar.gamepass.model.request.CreateGameRequest; 5 | import com.onurhizar.gamepass.model.response.GameResponse; 6 | import com.onurhizar.gamepass.util.AuthTokenHelper; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.http.*; 11 | import org.springframework.test.annotation.DirtiesContext; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | public class GameControllerTests extends AbstractIntegrationTest { 16 | 17 | @Autowired 18 | private TestRestTemplate restTemplate; 19 | @Autowired 20 | private AuthTokenHelper authTokenHelper; 21 | private static String baseUrl = "/game"; 22 | private static String existingGameId = "b4dceb23-d2ea-4432-aa7a-c71b4b15bcee"; 23 | private static String notExistingGameId = "aadceb23-d2ea-4432-aa7a-c71b4b15bcee"; 24 | private static String categoryToBeAdded = "75d1169f-11f9-4c6f-8a9d-4a30dc9bc282"; // CAR RACING category 25 | 26 | @Test 27 | void whenGetRequest_thenStatus200(){ 28 | ResponseEntity response = restTemplate.getForEntity(baseUrl, String.class); 29 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 30 | assertThat(response.getBody()).isNotNull(); 31 | } 32 | 33 | @Test 34 | void givenExistingGameId_whenGetGame_thenStatus200(){ 35 | String url = baseUrl+"/"+existingGameId; 36 | ResponseEntity response = restTemplate.getForEntity(url, String.class); 37 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 38 | } 39 | 40 | @Test 41 | void givenNotExistingGameId_whenGetUser_thenStatus404(){ 42 | String url = baseUrl+"/"+notExistingGameId; 43 | ResponseEntity response = restTemplate.getForEntity(url, String.class); 44 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 45 | } 46 | 47 | @Test 48 | void givenAdminToken_whenPostGame_thenStatus200() { 49 | CreateGameRequest request = new CreateGameRequest("test game title"); 50 | String url = baseUrl; 51 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 52 | HttpEntity httpEntity = new HttpEntity<>(request, headers); 53 | ResponseEntity response = restTemplate.postForEntity(url, httpEntity, GameResponse.class); 54 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 55 | assertThat(response.getBody()).isNotNull(); 56 | } 57 | 58 | @Test 59 | void givenAdminToken_whenUpdateGame_thenStatus200() { 60 | String url = baseUrl + "/" + existingGameId; 61 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 62 | CreateGameRequest request = new CreateGameRequest("updated game title"); 63 | HttpEntity httpEntity = new HttpEntity<>(request, headers); 64 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, GameResponse.class); 65 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 66 | assertThat(response.getBody()).isNotNull(); 67 | } 68 | 69 | @Test 70 | void givenAdminToken_whenAddCategoryToGame_thenStatus200() { 71 | String url = baseUrl + "/" + existingGameId + "/category/" + categoryToBeAdded; 72 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 73 | HttpEntity httpEntity = new HttpEntity<>(headers); 74 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, GameResponse.class); 75 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 76 | assertThat(response.getBody()).isNotNull(); 77 | } 78 | 79 | @Test 80 | @DirtiesContext 81 | void givenAdminToken_whenDeleteGame_thenStatus200() { 82 | String url = baseUrl + "/" + existingGameId; 83 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 84 | HttpEntity httpEntity = new HttpEntity<>(headers); 85 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, String.class); 86 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/controller/CategoryControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.abstracts.AbstractIntegrationTest; 4 | import com.onurhizar.gamepass.model.request.CreateCategoryRequest; 5 | import com.onurhizar.gamepass.model.request.UpdateCategoryRequest; 6 | import com.onurhizar.gamepass.model.response.CategoryResponse; 7 | import com.onurhizar.gamepass.util.AuthTokenHelper; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.web.client.TestRestTemplate; 11 | import org.springframework.http.*; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | public class CategoryControllerTests extends AbstractIntegrationTest { 16 | 17 | @Autowired 18 | private TestRestTemplate restTemplate; 19 | @Autowired 20 | private AuthTokenHelper authTokenHelper; 21 | private static final String baseUrl = "/category"; 22 | private static final String existingCategoryId = "66403305-972b-42b1-a71a-d7bb2828eebe"; 23 | private static final String notExistingCategoryId = "aadceb23-d2ea-4432-aa7a-c71b4b15bcee"; 24 | private static final String categoryIdToBeDeleted = "50a5fc87-4cbe-4b50-ac5a-acdd90bbfbf4"; 25 | 26 | @Test 27 | void whenGetRequest_thenStatus200(){ 28 | ResponseEntity response = restTemplate.getForEntity(baseUrl, String.class); 29 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 30 | assertThat(response.getBody()).isNotNull(); 31 | } 32 | 33 | @Test 34 | void givenExistingGameId_whenGetGame_thenStatus200(){ 35 | String url = baseUrl+"/"+existingCategoryId; 36 | ResponseEntity response = restTemplate.getForEntity(url, String.class); 37 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 38 | assertThat(response.getBody()).isNotNull(); 39 | } 40 | 41 | @Test 42 | void givenNotExistingGameId_whenGetUser_thenStatus404(){ 43 | String url = baseUrl+"/"+notExistingCategoryId; 44 | ResponseEntity response = restTemplate.getForEntity(url, String.class); 45 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 46 | } 47 | 48 | @Test 49 | void givenAdminToken_whenAddCategory_thenStatus200() { 50 | CreateCategoryRequest request = new CreateCategoryRequest("TestCategory"); 51 | String url = baseUrl + "/" + existingCategoryId; 52 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 53 | HttpEntity httpEntity = new HttpEntity<>(request, headers); 54 | ResponseEntity response = restTemplate.postForEntity(url, httpEntity, CategoryResponse.class); 55 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 56 | assertThat(response.getBody()).isNotNull(); 57 | } 58 | 59 | @Test 60 | void givenNoAdminToken_whenAddCategory_thenStatus403() { 61 | CreateCategoryRequest request = new CreateCategoryRequest("TestCategory"); 62 | String url = baseUrl + "/" + existingCategoryId; 63 | HttpEntity httpEntity = new HttpEntity<>(request); 64 | ResponseEntity response = restTemplate.postForEntity(url, httpEntity, CategoryResponse.class); 65 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); 66 | assertThat(response.getBody()).isNotNull(); 67 | } 68 | 69 | @Test 70 | void givenAdminToken_whenUpdateCategory_thenStatus200() { 71 | UpdateCategoryRequest request = new UpdateCategoryRequest("TestCategory", "GAME"); 72 | String url = baseUrl + "/" + existingCategoryId; 73 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 74 | HttpEntity httpEntity = new HttpEntity<>(request, headers); 75 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class); 76 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 77 | assertThat(response.getBody()).isNotNull(); 78 | } 79 | 80 | @Test 81 | void givenNoAdminToken_whenUpdateCategory_thenStatus403() { 82 | UpdateCategoryRequest request = new UpdateCategoryRequest("TestCategory", "GAME"); 83 | String url = baseUrl + "/" + existingCategoryId; 84 | HttpEntity httpEntity = new HttpEntity<>(request); 85 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class); 86 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); 87 | assertThat(response.getBody()).isNotNull(); 88 | } 89 | 90 | @Test 91 | void givenAdminToken_whenDeleteCategory_thenStatus200() { 92 | String url = baseUrl + "/" + categoryIdToBeDeleted; 93 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 94 | HttpEntity httpEntity = new HttpEntity<>(headers); 95 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, Void.class); 96 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/InvoiceService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.exception.UnacceptableRequestException; 5 | import com.onurhizar.gamepass.model.entity.ContractRecord; 6 | import com.onurhizar.gamepass.model.entity.Invoice; 7 | import com.onurhizar.gamepass.model.entity.Payment; 8 | import com.onurhizar.gamepass.model.entity.User; 9 | import com.onurhizar.gamepass.model.enums.UserRole; 10 | import com.onurhizar.gamepass.model.request.PaymentRequest; 11 | import com.onurhizar.gamepass.model.response.InvoiceResponse; 12 | import com.onurhizar.gamepass.repository.InvoiceRepository; 13 | import javax.transaction.Transactional; 14 | 15 | import com.onurhizar.gamepass.repository.UserRepository; 16 | import lombok.RequiredArgsConstructor; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.time.ZonedDateTime; 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | @Service 25 | @RequiredArgsConstructor 26 | @Slf4j 27 | public class InvoiceService { 28 | private final InvoiceRepository repository; 29 | private final PaymentService paymentService; 30 | private final UserRepository userRepository; 31 | 32 | public List listInvoices(){ 33 | return repository.findAll(); 34 | } 35 | 36 | public Invoice addInvoice(Invoice invoice){ 37 | return repository.save(invoice); 38 | } 39 | 40 | 41 | @Transactional 42 | public Invoice payInvoice(String invoiceId, PaymentRequest paymentRequest) { 43 | Invoice invoice = findById(invoiceId); 44 | int amount = paymentRequest.getAmount(); 45 | 46 | // validations, TODO : prettify later, custom annotation based validations 47 | if(amount<1) throw new UnacceptableRequestException("amount cannot be lesser than 1"); 48 | else if (amount > invoice.getFee()) { 49 | throw new UnacceptableRequestException("amount cannot be greater than invoice fee"); 50 | } 51 | 52 | int remainingFee = invoice.getFee()-amount; 53 | 54 | invoice.setFee(remainingFee); 55 | Payment payment = Payment.builder() 56 | .amount(paymentRequest.getAmount()) 57 | .invoice(invoice) 58 | .senderCard(paymentRequest.getSenderCard()) 59 | .receiverCard(paymentRequest.getReceiverCard()) 60 | .build(); 61 | 62 | paymentService.addPayment(payment); 63 | 64 | if (remainingFee!=0) return invoice; 65 | 66 | /* if remaining fee is 0, then invoice is paid. 67 | Check if user is GUEST and upgrade to MEMBER if all invoices are paid */ 68 | ContractRecord contractRecord = invoice.getContractRecord(); 69 | User user = contractRecord.getUser(); 70 | 71 | if (!user.getRole().equals(UserRole.GUEST)) return invoice; 72 | 73 | List unpaidInvoices = findAllNonPaidPastDueInvoicesForSpecificUser(user.getId()); 74 | log.info("unpaid invoices:"+unpaidInvoices.size()); 75 | 76 | // TODO note that, if user role is ADMIN, new role will be MEMBER. Ignore this possibility 77 | if (unpaidInvoices.isEmpty()) { 78 | upgradeUserRoleToMember(user.getId()); 79 | log.info("User is UPGRADED to MEMBER, usedId: " + user.getId()); 80 | } 81 | 82 | return invoice; 83 | } 84 | 85 | public Invoice findById(String id){ 86 | return repository.findById(id).orElseThrow(EntityNotFoundException::new); 87 | } 88 | 89 | /** to list invoices that are older than 15 days from now so that we can downgrade the user to guest role */ 90 | public List findInvoicesBy15DaysOld(){ 91 | ZonedDateTime the15DaysOldTime = ZonedDateTime.now().minusDays(15); 92 | return repository.findByCreatedAtBefore(the15DaysOldTime); 93 | } 94 | 95 | public List findAllNonPaidPastDueInvoicesForSpecificUser(String userId){ 96 | ZonedDateTime the5MinsOldTime = ZonedDateTime.now().minusMinutes(5).plusSeconds(2); 97 | return repository.findByContractRecordUserIdAndFeeNotAndCreatedAtBefore(userId, 0, the5MinsOldTime); 98 | } 99 | 100 | /** lists invoices for a user that are created in the last 1 month */ 101 | public List findInvoicesOfContractRecordInCurrentMonth(String contractRecordId){ 102 | // get current month's first day from hour 00:00:00.000 103 | ZonedDateTime currentMonthStartingTime = ZonedDateTime.now().withDayOfMonth(1) 104 | .withHour(0).withMinute(0).withSecond(0).withNano(0); 105 | return repository.findByContractRecordIdAndCreatedAtAfter(contractRecordId, currentMonthStartingTime); 106 | } 107 | 108 | // TODO remove this method later, resolve circular dependency 109 | private void upgradeUserRoleToMember(String userId){ 110 | User user = userRepository.findById(userId).orElseThrow(EntityNotFoundException::new); 111 | user.setRole(UserRole.MEMBER); 112 | userRepository.save(user); 113 | } 114 | 115 | public List getInvoicesOfUser(String userId) { 116 | return repository.findByContractRecordUserId(userId).stream() 117 | .map(InvoiceResponse::fromEntity).collect(Collectors.toList()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.exception.UnacceptableRequestException; 5 | import com.onurhizar.gamepass.model.entity.User; 6 | import com.onurhizar.gamepass.model.enums.UserRole; 7 | import com.onurhizar.gamepass.model.request.auth.RegisterRequest; 8 | import javax.transaction.Transactional; 9 | import org.springframework.security.authentication.BadCredentialsException; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.security.authentication.AuthenticationManager; 12 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | 15 | import com.onurhizar.gamepass.model.request.auth.LoginRequest; 16 | import com.onurhizar.gamepass.model.response.AuthenticationResponse; 17 | import com.onurhizar.gamepass.repository.UserRepository; 18 | import com.onurhizar.gamepass.security.JwtService; 19 | 20 | import lombok.RequiredArgsConstructor; 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | import java.time.ZonedDateTime; 24 | import java.util.UUID; 25 | 26 | @Service 27 | @RequiredArgsConstructor 28 | @Slf4j 29 | public class AuthenticationService { 30 | private final UserRepository userRepository; // TODO make this userService 31 | private final PasswordEncoder passwordEncoder; 32 | private final JwtService jwtService; 33 | private final AuthenticationManager authenticationManager; 34 | 35 | 36 | public AuthenticationResponse register(RegisterRequest request) { 37 | User user = User.builder() 38 | .name(request.getName()) 39 | .surname(request.getSurname()) 40 | .email(request.getEmail()) 41 | .passwordHash(passwordEncoder.encode(request.getPassword())) 42 | .role(UserRole.GUEST) 43 | .build(); 44 | 45 | // check if user exists before register 46 | User foundUser = userRepository.findByEmail(request.getEmail()); 47 | if (foundUser != null) 48 | throw new UnacceptableRequestException("User already exists"); 49 | 50 | User response = userRepository.save(user); 51 | String jwtToken = jwtService.generateToken(response); 52 | return new AuthenticationResponse(jwtToken); 53 | } 54 | 55 | public AuthenticationResponse login(LoginRequest request) { 56 | authenticationManager.authenticate( 57 | new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) 58 | ); 59 | 60 | User user = userRepository.findByEmail(request.getUsername()); 61 | if (passwordEncoder.matches(request.getPassword(), user.getPassword())) { 62 | String token = jwtService.generateToken(user); 63 | return new AuthenticationResponse(token); 64 | } 65 | 66 | throw new BadCredentialsException("Invalid username or password"); 67 | } 68 | 69 | // VERIFICATION LOGIC 70 | public String sendVerificationCodeToEmail(String email) { 71 | User user = userRepository.findByEmail(email); 72 | if (user==null) throw new EntityNotFoundException(); 73 | return user.getVerificationCode(); // TODO : what if expired? Should create new token? 74 | } 75 | 76 | @Transactional 77 | public void verify(String verificationCode) { 78 | User user = userRepository.findUserByVerificationCode(verificationCode); // TODO : make it service method 79 | 80 | // check if not user exists 81 | if (user==null) throw new EntityNotFoundException(); 82 | 83 | // check if verification code is expired 84 | if (user.getVerificationCodeExpireDate().isBefore(ZonedDateTime.now())) 85 | throw new UnacceptableRequestException("verification code is expired"); 86 | 87 | user.setVerified(true); 88 | userRepository.save(user); 89 | } 90 | 91 | // RECOVERY LOGIC 92 | @Transactional 93 | public String createRecoveryCode(String userId){ 94 | User user = userRepository.findById(userId).orElseThrow(EntityNotFoundException::new); 95 | String recoveryCode = UUID.randomUUID().toString(); 96 | user.setRecoveryCode(recoveryCode); 97 | user.setRecoveryCodeExpireDate(ZonedDateTime.now().plusDays(1)); // TODO: fixed value? 98 | 99 | userRepository.save(user); 100 | return recoveryCode; 101 | } 102 | 103 | @Transactional 104 | public String recoverPasswordByGeneratingNew(String recoveryCode){ 105 | User user = userRepository.findUserByRecoveryCode(recoveryCode); 106 | if (user==null) throw new EntityNotFoundException(); 107 | 108 | // code duplication, TODO refactor? 109 | ZonedDateTime expireDate = user.getRecoveryCodeExpireDate(); 110 | if (expireDate==null || expireDate.isBefore(ZonedDateTime.now())) 111 | throw new UnacceptableRequestException("verification code is expired"); 112 | 113 | String newPassword = UUID.randomUUID().toString().substring(0,18); 114 | user.setPasswordHash(passwordEncoder.encode(newPassword)); 115 | user.setRecoveryCode(null); // remove code after using so that it can be used once 116 | user.setRecoveryCodeExpireDate(null); 117 | userRepository.save(user); 118 | 119 | return newPassword; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/CategoryService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.exception.UnacceptableRequestException; 5 | import com.onurhizar.gamepass.model.entity.Category; 6 | import com.onurhizar.gamepass.model.entity.Game; 7 | import com.onurhizar.gamepass.model.entity.User; 8 | import com.onurhizar.gamepass.model.response.CategoryResponse; 9 | import com.onurhizar.gamepass.model.request.UpdateCategoryRequest; 10 | import com.onurhizar.gamepass.repository.CategoryRepository; 11 | import com.onurhizar.gamepass.repository.UserRepository; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.Deque; 17 | import java.util.LinkedList; 18 | import java.util.List; 19 | 20 | @Service 21 | @RequiredArgsConstructor 22 | @Slf4j 23 | public class CategoryService { 24 | 25 | private final CategoryRepository repository; 26 | private final UserRepository userRepository; 27 | 28 | /** 29 | * Only adds child category for now, not super parent 30 | */ 31 | public CategoryResponse addCategory(String parentId, String name) { 32 | // check parent if exists 33 | Category parent = repository.findById(parentId) 34 | .orElseThrow(EntityNotFoundException::new); 35 | log.info(parent.toString()); 36 | Category category = Category.builder() 37 | .name(name) 38 | .isSuperCategory(false) 39 | .parent(parent) 40 | .build(); 41 | return CategoryResponse.fromEntity(repository.save(category)); 42 | } 43 | 44 | public List listCategories(){ 45 | List categoryResponses = new LinkedList<>(); 46 | List categories = repository.findAll(); 47 | for (Category category : categories) { 48 | appendAllGamesFromChildrenCategories(category.getId(), category.getGames()); 49 | categoryResponses.add(CategoryResponse.fromEntity(category)); 50 | } 51 | return categoryResponses; 52 | } 53 | 54 | public Category findCategoryById(String categoryId){ 55 | Category category = repository.findById(categoryId) 56 | .orElseThrow(EntityNotFoundException::new); 57 | appendAllGamesFromChildrenCategories(categoryId, category.getGames()); 58 | return category; 59 | } 60 | 61 | public CategoryResponse updateCategory(String id, UpdateCategoryRequest request) { 62 | Category category = repository.findById(id) 63 | .orElseThrow(EntityNotFoundException::new); 64 | 65 | Category parentCategory = repository.findCategoryByName(request.getParentName()) 66 | .orElseThrow(EntityNotFoundException::new); 67 | 68 | if (category.getId().equals(parentCategory.getId())) 69 | throw new UnacceptableRequestException("a category cannot be its parent, same name is disallowed"); 70 | 71 | category.setName(request.getName()); 72 | category.setParent(parentCategory); 73 | return CategoryResponse.fromEntity(repository.save(category)); 74 | } 75 | 76 | 77 | /** detach users first, then delete category */ 78 | private void deleteCategory(String id) { 79 | Category category = repository.findById(id) 80 | .orElseThrow(EntityNotFoundException::new); 81 | if (category.isSuperCategory()) 82 | throw new UnacceptableRequestException("Super category cannot be deleted"); 83 | 84 | List followingUsers = userRepository.findByFollowedCategories(category); 85 | for (User user : followingUsers) { 86 | user.getFollowedCategories().remove(category); 87 | userRepository.save(user); 88 | } 89 | 90 | repository.deleteById(id); 91 | } 92 | 93 | /** Returns a stack, the most leaf is at the top, root stays at the bottom to implement remove orphan logic */ 94 | public Deque findAllChildrenCategories(String categoryId){ 95 | Category rootCategory = repository.findById(categoryId) 96 | .orElseThrow(EntityNotFoundException::new); 97 | 98 | Deque deque = new LinkedList<>(); 99 | traverseAllChildrenOfCategory(rootCategory, deque); 100 | 101 | // we need to reverse the stack to iterate from leaf to root 102 | Deque reversedStack = new LinkedList<>(); 103 | while (!deque.isEmpty()) reversedStack.push(deque.pop()); 104 | return reversedStack; 105 | } 106 | 107 | 108 | /** Returns reversed stack, starts from leaf and goes to root category */ 109 | private Deque traverseAllChildrenOfCategory(Category rootCategory, Deque deque) { 110 | List childrenCategories = repository.findCategoriesByParentId(rootCategory.getId()); 111 | 112 | if (!childrenCategories.isEmpty()) // first, iterate over children 113 | for (Category category : childrenCategories) 114 | traverseAllChildrenOfCategory(category, deque); 115 | 116 | deque.push(rootCategory); // then push self to stack 117 | return deque; // root node stays at the top of the stack, need to reverse it before using 118 | } 119 | 120 | public void deleteCategoryByAssignChildrenToGrandParent(String categoryId){ 121 | Category category = findCategoryById(categoryId); 122 | Category parentCategory = category.getParent(); 123 | List childrenCategories = repository.findCategoriesByParentId(categoryId); // find children 124 | for (Category childCategory : childrenCategories) { 125 | childCategory.setParent(parentCategory); // assign children to grandparent 126 | repository.save(childCategory); 127 | } 128 | deleteCategory(categoryId); 129 | } 130 | 131 | 132 | /** Appends all games of children categories' games, if same game exists in the list, does not append */ 133 | private List appendAllGamesFromChildrenCategories(String categoryId, List games){ 134 | Deque deque = findAllChildrenCategories(categoryId); 135 | 136 | for (Category category : deque) { 137 | List gamesOfChildrenCategory = category.getGames(); 138 | for (Game game : gamesOfChildrenCategory) 139 | if (!games.contains(game)) games.add(game); 140 | } 141 | return games; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/controller/UserControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.abstracts.AbstractIntegrationTest; 4 | import com.onurhizar.gamepass.model.request.CreateUserRequest; 5 | import com.onurhizar.gamepass.util.AuthTokenHelper; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.web.client.TestRestTemplate; 9 | import org.springframework.http.*; 10 | import org.springframework.test.annotation.DirtiesContext; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class UserControllerTests extends AbstractIntegrationTest { 15 | 16 | @Autowired 17 | private TestRestTemplate restTemplate; 18 | @Autowired 19 | private AuthTokenHelper authTokenHelper; 20 | 21 | @Test 22 | void givenAdminAuth_whenGetRequest_returnUsers(){ 23 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 24 | HttpEntity request = new HttpEntity<>(null, headers); 25 | ResponseEntity response = restTemplate.exchange("/user", HttpMethod.GET, request, String.class); 26 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 27 | } 28 | 29 | @Test 30 | void givenAdminAuthAndExistingUserId_whenGetUser_thenStatus200(){ 31 | String userId = "ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9"; 32 | String url = "/user/"+userId; 33 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 34 | HttpEntity request = new HttpEntity<>(null, headers); 35 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, request, String.class); 36 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 37 | } 38 | 39 | @Test 40 | void givenAdminAuthNotExistingUserId_whenGetUser_thenStatus404(){ 41 | String userId = "111a3d25-2b7a-4683-89ed-ac0e42cdc879"; 42 | String url = "/user/"+userId; 43 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 44 | HttpEntity request = new HttpEntity<>(null, headers); 45 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, request, String.class); 46 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 47 | } 48 | 49 | @Test 50 | void givenNoAuth_whenEmptyRequestPost_thenStatus403(){ 51 | CreateUserRequest requestDto = new CreateUserRequest(); 52 | 53 | HttpHeaders headers = new HttpHeaders(); 54 | 55 | HttpEntity request = new HttpEntity<>(requestDto, headers); 56 | ResponseEntity response = restTemplate.postForEntity("/user",request,String.class); 57 | 58 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); 59 | } 60 | 61 | @Test 62 | void givenAdminAuth_whenEmptyRequestPost_thenStatus400(){ 63 | CreateUserRequest requestDto = new CreateUserRequest(); 64 | 65 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 66 | HttpEntity request = new HttpEntity<>(requestDto, headers); 67 | 68 | ResponseEntity response = restTemplate.exchange("/user", HttpMethod.POST, request, String.class); 69 | 70 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); 71 | } 72 | 73 | @Test 74 | void givenNoAuth_whenValidRequestPost_thenStatus403(){ 75 | CreateUserRequest requestDto = new CreateUserRequest(); 76 | requestDto.setEmail("integration-test@mail.com"); 77 | requestDto.setName("integration"); 78 | requestDto.setSurname("test"); 79 | requestDto.setPassword("123456"); 80 | 81 | ResponseEntity response = restTemplate.postForEntity("/user", requestDto, String.class); 82 | 83 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); 84 | } 85 | 86 | @Test 87 | void givenAdminAuth_whenValidRequestPost_thenStatus200(){ 88 | CreateUserRequest requestDto = new CreateUserRequest(); 89 | requestDto.setEmail("integration-test@mail.com"); 90 | requestDto.setName("integration"); 91 | requestDto.setSurname("test"); 92 | requestDto.setPassword("123456"); 93 | 94 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 95 | HttpEntity request = new HttpEntity<>(requestDto, headers); 96 | 97 | ResponseEntity response = restTemplate.exchange("/user", HttpMethod.POST, request, String.class); 98 | 99 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 100 | } 101 | 102 | 103 | @Test // TODO make it status 400 104 | void givenAdminAuth_whenEmailExistsWhilePost_thenStatus500(){ 105 | CreateUserRequest requestDto = new CreateUserRequest(); 106 | requestDto.setEmail("admin@mail.com"); 107 | requestDto.setName("integration"); 108 | requestDto.setSurname("test"); 109 | requestDto.setPassword("123456"); 110 | 111 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 112 | HttpEntity request = new HttpEntity<>(null, headers); 113 | 114 | ResponseEntity response = restTemplate.exchange("/user", HttpMethod.POST, request, String.class); 115 | 116 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); 117 | } 118 | 119 | @Test 120 | @DirtiesContext 121 | void givenAdminAuth_whenDeleteUser_thenStatus200(){ 122 | String userId = "102b8078-276a-49e2-b1df-ad41415e32b9"; 123 | String url = "/user/"+userId; 124 | HttpHeaders headers = authTokenHelper.generateJwtHeader("admin@mail.com"); 125 | HttpEntity request = new HttpEntity<>(null, headers); 126 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class); 127 | 128 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 129 | } 130 | 131 | @Test 132 | void givenNoAuth_whenDeleteUser_thenStatus401Forbidden(){ 133 | String userId = "102b8078-276a-49e2-b1df-ad41415e32b9"; 134 | String url = "/user/"+userId; 135 | HttpEntity request = new HttpEntity<>(null); 136 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class); 137 | 138 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/com/onurhizar/gamepass/controller/AuthControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.controller; 2 | 3 | import com.onurhizar.gamepass.abstracts.AbstractIntegrationTest; 4 | import com.onurhizar.gamepass.model.request.auth.LoginRequest; 5 | import com.onurhizar.gamepass.model.request.auth.RegisterRequest; 6 | import com.onurhizar.gamepass.model.response.AuthenticationResponse; 7 | import org.assertj.core.api.Assertions; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.TestInstance; 11 | import org.springframework.http.HttpEntity; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | 15 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 16 | public class AuthControllerTests extends AbstractIntegrationTest { 17 | 18 | private static String verificationCode; 19 | private static String recoveryCode; 20 | private final static String testingUserMail = "guest@mail.com"; 21 | private final static String testingUserId = "102b8078-276a-49e2-b1df-ad41415e32b9"; 22 | 23 | @BeforeAll 24 | void getCodes(){ 25 | // get verification code 26 | ResponseEntity response = restTemplate.getForEntity( 27 | "/verify/email?email="+testingUserMail, String.class); 28 | 29 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 30 | Assertions.assertThat(response.getBody()).isNotNull(); 31 | Assertions.assertThat(response.getBody()).contains("verify?code="); 32 | verificationCode = response.getBody().split("=")[1]; 33 | 34 | // get recovery code 35 | response = restTemplate.getForEntity( 36 | "/recover/email/"+testingUserId, String.class); 37 | 38 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 39 | Assertions.assertThat(response.getBody()).isNotNull(); 40 | Assertions.assertThat(response.getBody()).contains("recover?code="); 41 | recoveryCode = response.getBody().split("=")[1]; 42 | } 43 | 44 | @Test 45 | void whenRegisterRequestWithValidBody_thenStatus200(){ 46 | RegisterRequest request = new RegisterRequest("test","user", 47 | "integration@test.com","123456"); 48 | HttpEntity requestEntity = new HttpEntity<>(request); 49 | 50 | ResponseEntity response = restTemplate.postForEntity( 51 | "/register", requestEntity, AuthenticationResponse.class); 52 | 53 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 54 | Assertions.assertThat(response.getBody()).isNotNull(); 55 | } 56 | 57 | @Test 58 | void whenRegisterRequestWithInvalidBody_thenStatus400(){ 59 | RegisterRequest request = new RegisterRequest("test","user", 60 | "nonValidEmail","123456"); 61 | 62 | ResponseEntity response = restTemplate.postForEntity( 63 | "/register", request, Void.class); 64 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); 65 | } 66 | 67 | @Test 68 | void whenLoginRequestWithValidCredentials_thenStatus200() { 69 | // given 70 | LoginRequest request = new LoginRequest("test@mail.com", "123456"); 71 | HttpEntity requestEntity = new HttpEntity<>(request); 72 | 73 | // when 74 | ResponseEntity response = restTemplate.postForEntity( 75 | "/login", requestEntity, AuthenticationResponse.class); 76 | 77 | // then 78 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 79 | Assertions.assertThat(response.getBody()).isNotNull(); 80 | } 81 | 82 | @Test 83 | void whenLoginRequestWithInvalidCredentials_thenStatus401() { 84 | // given 85 | LoginRequest request = new LoginRequest("integration@test.com", "wrongpassword"); 86 | HttpEntity requestEntity = new HttpEntity<>(request); 87 | 88 | // when 89 | ResponseEntity response = restTemplate.postForEntity( 90 | "/login", requestEntity, Void.class); 91 | 92 | // then 93 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); 94 | } 95 | 96 | @Test 97 | void whenVerifyRequestWithValidCode_thenStatus200() { 98 | // given 99 | String code = verificationCode; 100 | 101 | // when 102 | ResponseEntity response = restTemplate.getForEntity( 103 | "/verify?code=" + code, Void.class); 104 | 105 | // then 106 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 107 | } 108 | 109 | @Test 110 | void whenVerifyRequestWithInvalidCode_thenStatus404() { 111 | // given 112 | String code = "invalid-code"; 113 | 114 | // when 115 | ResponseEntity response = restTemplate.getForEntity( 116 | "/verify?code=" + code, Void.class); 117 | 118 | // then 119 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 120 | } 121 | 122 | @Test 123 | void whenSendVerificationCodeToEmailRequest_thenStatus200() { 124 | // given 125 | String email = "guest@mail.com"; 126 | 127 | // when 128 | ResponseEntity response = restTemplate.getForEntity( 129 | "/verify/email?email=" + email, String.class); 130 | 131 | // then 132 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 133 | Assertions.assertThat(response.getBody()).isNotNull(); 134 | Assertions.assertThat(response.getBody()).contains("/verify?code="); 135 | } 136 | 137 | @Test 138 | void whenRecoverPasswordRequestWithValidCode_thenStatus200() { 139 | // given 140 | String code = recoveryCode; 141 | 142 | // when 143 | ResponseEntity response = restTemplate.getForEntity( 144 | "/recover?code=" + code, String.class); 145 | 146 | // then 147 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 148 | Assertions.assertThat(response.getBody()).isNotNull(); 149 | Assertions.assertThat(response.getBody()).contains("Your new password is "); 150 | } 151 | 152 | @Test 153 | void whenRecoverPasswordRequestWithInvalidCode_thenStatus404() { 154 | // given 155 | String code = "invalid-code"; 156 | 157 | // when 158 | ResponseEntity response = restTemplate.getForEntity( 159 | "/recover?code=" + code, Void.class); 160 | 161 | // then 162 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 163 | } 164 | 165 | @Test 166 | void whenSendRecoveryCodeToEmailRequest_thenStatus200() { 167 | // given 168 | String userId = testingUserId; 169 | 170 | // when 171 | ResponseEntity response = restTemplate.getForEntity( 172 | "/recover/email/" + userId, String.class); 173 | 174 | // then 175 | Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 176 | Assertions.assertThat(response.getBody()).isNotNull(); 177 | Assertions.assertThat(response.getBody()).contains("/recover?code="); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | -- User Data 2 | INSERT INTO users (id, email, name, surname, password_hash, role, verified, verification_code, verification_code_expire_date, created_at, updated_at) 3 | VALUES ('ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9', 'admin@mail.com', 'Onur', 'Hızar', 4 | '$2a$10$qcBx2lYIfvJjbwEzQ2chxOL9B.5ydHBvShfOVP7a8nmNTySv6Dmju', 'ADMIN', true, 5 | '72014de5-1e02-4b31-b9f4-0792ce889b83', CURRENT_TIMESTAMP + INTERVAL '1 year', now(), now()); 6 | 7 | 8 | INSERT INTO users (id, email, name, surname, password_hash, role, verified, verification_code, verification_code_expire_date, created_at, updated_at) 9 | VALUES ('ad2aefec-794e-4199-8983-c9fa8f40c383', 'mervan@mail.com', 'Hamit Mervan', 'Çelik', 10 | '$2a$10$oE8Gimn418dNXTzmLRXpOOcxceZOZPqLxS4U1EX2XR1zYzVgq/6GO', 'ADMIN', true, 11 | '103195dc-3049-42a7-bbf0-4371f78327f1', CURRENT_TIMESTAMP + INTERVAL '1 year', now(), now()); 12 | 13 | -- password of rest is 123456 14 | INSERT INTO users (id, email, name, surname, password_hash, role, verified, verification_code, verification_code_expire_date, created_at, updated_at) 15 | VALUES ('aa59d163-5e7e-4290-b6ac-b901b0b4543a', 'test@mail.com', 'Test', 'User', 16 | '$2a$10$NjRTff/grP7d/87oE28BouNtRWto6WA6ApMHwGyqVDyMENh/msb4a', 'MEMBER', true, 17 | '06a3d67a-f588-4298-8cfc-3ee3597b0b89', CURRENT_TIMESTAMP + INTERVAL '1 year', now(), now()); 18 | 19 | INSERT INTO users (id, email, name, surname, password_hash, role, verified, verification_code, verification_code_expire_date, created_at, updated_at) 20 | VALUES ('102b8078-276a-49e2-b1df-ad41415e32b9', 'guest@mail.com', 'Guest', 'User', 21 | '$2a$10$NjRTff/grP7d/87oE28BouNtRWto6WA6ApMHwGyqVDyMENh/msb4a', 'GUEST', false, 22 | 'c4bfe252-8de9-41fa-ae94-d55d78f1adae', CURRENT_TIMESTAMP + INTERVAL '1 year', now(), now()); 23 | 24 | 25 | -- Subscription Data 26 | INSERT INTO subscription (id, duration, is_active, monthly_fee, name, created_at, updated_at) 27 | VALUES ('11b455da-715a-4dc1-b4f1-b526c1c9ab4e', 1, true, 100, '1 MONTH (BRONZE)', now(), now()); 28 | 29 | INSERT INTO subscription (id, duration, is_active, monthly_fee, name, created_at, updated_at) 30 | VALUES ('33b455da-335a-4dc1-b4f1-b526c1c9ab4e', 3, true, 90, '3 MONTHS (SILVER)', now(), now()); 31 | 32 | INSERT INTO subscription (id, duration, is_active, monthly_fee, name, created_at, updated_at) 33 | VALUES ('66b455da-665a-4dc1-b4f1-b526c1c9ab4e', 6, true, 80, '6 MONTHS (GOLD)', now(), now()); 34 | 35 | 36 | -- Category Data 37 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 38 | VALUES ('10b455da-7e5a-4dc3-b4f5-b526c1c9ab4e', 'GAME', null, true, now(), now()); 39 | 40 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 41 | VALUES ('66403305-972b-42b1-a71a-d7bb2828eebe', 'PUZZLE', '10b455da-7e5a-4dc3-b4f5-b526c1c9ab4e', false, now(), now()); 42 | 43 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 44 | VALUES ('50a5fc87-4cbe-4b50-ac5a-acdd90bbfbf4', 'RACING', '10b455da-7e5a-4dc3-b4f5-b526c1c9ab4e', false, now(), now()); 45 | 46 | -- children of PUZZLE category 47 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 48 | VALUES ('932fbf36-b7f9-4c5a-9f6d-ef8b0905e0dc', 'TRADITIONAL PUZZLE', '66403305-972b-42b1-a71a-d7bb2828eebe', false, now(), now()); 49 | 50 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 51 | VALUES ('06046e4c-f8f1-493c-9919-49e0856462cc', 'STRATEGY PUZZLE', '66403305-972b-42b1-a71a-d7bb2828eebe', false, now(), now()); 52 | 53 | -- children of RACING category 54 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 55 | VALUES ('75d1169f-11f9-4c6f-8a9d-4a30dc9bc282', 'CAR RACING', '50a5fc87-4cbe-4b50-ac5a-acdd90bbfbf4', false, now(), now()); 56 | 57 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 58 | VALUES ('2f1036ba-44a1-4c4e-9867-18386a7d4910', 'BIKE RACING', '50a5fc87-4cbe-4b50-ac5a-acdd90bbfbf4', false, now(), now()); 59 | 60 | -- children of CAR RACING category 61 | INSERT INTO category (id, name, parent_id, is_super_category, created_at, updated_at) 62 | VALUES ('1d063f5a-d1f5-4388-afb5-349464a79ac7', 'RALLY RACING', '75d1169f-11f9-4c6f-8a9d-4a30dc9bc282', false, now(), now()); 63 | 64 | 65 | -- Game Data 66 | INSERT INTO game (id, title, created_at, updated_at) 67 | VALUES ('b4dceb23-d2ea-4432-aa7a-c71b4b15bcee', 'Portal 3', now(), now()); 68 | 69 | INSERT INTO game (id, title, created_at, updated_at) 70 | VALUES ('cbf27a05-9abe-40c0-a943-ede62f9ca3de', 'NFS Most Wanted', now(), now()); 71 | 72 | INSERT INTO game (id, title, created_at, updated_at) 73 | VALUES ('421450be-91e2-4184-8450-3dcc12a33e63', 'CS:GO', now(), now()); 74 | 75 | INSERT INTO game (id, title, created_at, updated_at) 76 | VALUES ('559139b9-ff8c-4713-9000-3332dce26359', 'Sudoku', now(), now()); 77 | 78 | INSERT INTO game (id, title, created_at, updated_at) 79 | VALUES ('3e4a7ce1-da2c-4267-9b93-96493be145f2', 'MotoGP 2023', now(), now()); 80 | 81 | INSERT INTO game (id, title, created_at, updated_at) 82 | VALUES ('f724e3e0-d0a4-49cb-a695-5e5a184ec27f', 'Dirt Rally', now(), now()); 83 | 84 | INSERT INTO game (id, title, created_at, updated_at) 85 | VALUES ('1f9e9b57-4f30-4366-a00b-21993248967c', 'Dirt Rally 2', now(), now()); 86 | 87 | -- categories_games join table 88 | INSERT INTO categories_games (category_id, game_id) 89 | VALUES ('10b455da-7e5a-4dc3-b4f5-b526c1c9ab4e', 'b4dceb23-d2ea-4432-aa7a-c71b4b15bcee'); -- game : portal 3 90 | 91 | INSERT INTO categories_games (category_id, game_id) 92 | VALUES ('10b455da-7e5a-4dc3-b4f5-b526c1c9ab4e', 'cbf27a05-9abe-40c0-a943-ede62f9ca3de'); -- game : nfs mw 93 | 94 | INSERT INTO categories_games (category_id, game_id) 95 | VALUES ('10b455da-7e5a-4dc3-b4f5-b526c1c9ab4e', '421450be-91e2-4184-8450-3dcc12a33e63'); -- game : csgo 96 | 97 | INSERT INTO categories_games (category_id, game_id) 98 | VALUES ('66403305-972b-42b1-a71a-d7bb2828eebe', 'b4dceb23-d2ea-4432-aa7a-c71b4b15bcee'); -- puzzle : portal 3 99 | 100 | INSERT INTO categories_games (category_id, game_id) 101 | VALUES ('932fbf36-b7f9-4c5a-9f6d-ef8b0905e0dc', '559139b9-ff8c-4713-9000-3332dce26359'); -- traditional puzzle : sudoku 102 | 103 | INSERT INTO categories_games (category_id, game_id) 104 | VALUES ('2f1036ba-44a1-4c4e-9867-18386a7d4910', '3e4a7ce1-da2c-4267-9b93-96493be145f2'); -- bike racing : MotoGP 2023 105 | 106 | INSERT INTO categories_games (category_id, game_id) 107 | VALUES ('1d063f5a-d1f5-4388-afb5-349464a79ac7', 'f724e3e0-d0a4-49cb-a695-5e5a184ec27f'); -- rally racing : Dirt Rally 108 | 109 | INSERT INTO categories_games (category_id, game_id) 110 | VALUES ('1d063f5a-d1f5-4388-afb5-349464a79ac7', '1f9e9b57-4f30-4366-a00b-21993248967c'); -- rally racing : Dirt Rally 2 111 | 112 | 113 | -- User Interest : favorite games : users_games join table 114 | INSERT INTO users_games (user_id, game_id) 115 | VALUES ('ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9', 'b4dceb23-d2ea-4432-aa7a-c71b4b15bcee'); -- admin favorite portal 3 116 | 117 | INSERT INTO users_games (user_id, game_id) 118 | VALUES ('ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9', 'cbf27a05-9abe-40c0-a943-ede62f9ca3de'); -- admin favorites nfs mw 119 | 120 | 121 | -- User Interest : following categories : users_categories join table 122 | INSERT INTO users_categories (user_id, category_id) 123 | VALUES ('ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9', '66403305-972b-42b1-a71a-d7bb2828eebe'); -- admin follows PUZZLE 124 | 125 | INSERT INTO users_categories (user_id, category_id) 126 | VALUES ('ad1a1ddd-2f1c-4cc9-85ea-312dfc487bc9', '50a5fc87-4cbe-4b50-ac5a-acdd90bbfbf4'); -- admin follows RACING 127 | -------------------------------------------------------------------------------- /src/main/java/com/onurhizar/gamepass/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.onurhizar.gamepass.service; 2 | 3 | import com.onurhizar.gamepass.exception.EntityNotFoundException; 4 | import com.onurhizar.gamepass.exception.UnacceptableRequestException; 5 | import com.onurhizar.gamepass.model.entity.*; 6 | import com.onurhizar.gamepass.model.enums.UserRole; 7 | import com.onurhizar.gamepass.model.request.CreateUserRequest; 8 | import com.onurhizar.gamepass.repository.UserRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import java.util.List; 15 | 16 | @Service 17 | @RequiredArgsConstructor 18 | public class UserService { 19 | 20 | private final UserRepository repository; 21 | private final GameService gameService; 22 | private final CategoryService categoryService; 23 | private final SubscriptionService subscriptionService; 24 | private final ContractRecordService contractRecordService; 25 | private final InvoiceService invoiceService; 26 | private final PasswordEncoder passwordEncoder; 27 | 28 | 29 | public User addUser(CreateUserRequest request){ 30 | User foundUser = repository.findByEmail(request.getEmail()); // check if email exists 31 | if (foundUser != null) throw new UnacceptableRequestException("email exists"); 32 | 33 | 34 | User newUser = User.builder() 35 | .name(request.getName()) 36 | .surname(request.getSurname()) 37 | .email(request.getEmail()) 38 | .passwordHash(passwordEncoder.encode(request.getPassword())) 39 | .role(UserRole.GUEST) // default 40 | .build(); 41 | return repository.save(newUser); 42 | } 43 | 44 | public void deleteUser(String userId){ 45 | User user = findById(userId); 46 | repository.delete(user); 47 | } 48 | 49 | 50 | public User updateUser(String id, CreateUserRequest userDto) { 51 | User user = repository.findById(id).orElseThrow(EntityNotFoundException::new); 52 | // TODO : a better approach? too much repetition 53 | if (userDto.getName() != null) user.setName(userDto.getName()); 54 | if (userDto.getSurname() != null) user.setSurname(userDto.getSurname()); 55 | if (userDto.getEmail() != null) user.setEmail(userDto.getEmail()); 56 | if (userDto.getPassword() != null) { 57 | user.setPasswordHash(passwordEncoder.encode(userDto.getPassword())); 58 | } 59 | return repository.save(user); 60 | } 61 | 62 | public List listUsers(){ 63 | return repository.findAll(); 64 | } 65 | 66 | 67 | public User findById(String id){ 68 | return repository.findById(id).orElseThrow(EntityNotFoundException::new); 69 | } 70 | 71 | 72 | @Transactional 73 | public ContractRecord subscribe(String userId, String subscriptionId){ 74 | User user = findById(userId); 75 | Subscription newSubscription = subscriptionService.findById(subscriptionId); 76 | 77 | // check if user is verified 78 | if(!user.isVerified()) throw new UnacceptableRequestException("only verified users can subscribe"); 79 | 80 | // check if user has already a subscription (allow only upgrading) 81 | ContractRecord contractRecord = user.getContractRecord(); 82 | 83 | if (contractRecord != null){ 84 | if (contractRecord.getMonthlyFee() >= newSubscription.getMonthlyFee()) 85 | throw new UnacceptableRequestException("you can only upgrade your subscription"); 86 | 87 | findInvoicesInCurrentMonthAndUpdateTheirFees(contractRecord, newSubscription); 88 | } 89 | 90 | // when a guest user buys a subscription, assign a member role 91 | if (user.getRole() == UserRole.GUEST) user.setRole(UserRole.MEMBER); 92 | 93 | if (contractRecord == null) return contractRecordService.addContract(user, newSubscription); 94 | else return contractRecordService.updateContract(contractRecord, newSubscription); 95 | } 96 | 97 | // Interests : Follow Categories and Favorite Games 98 | public User favoriteGame(String userId, String gameId){ 99 | return addOrRemoveGameFromUserFavoriteGames(userId, gameId, true); 100 | } 101 | 102 | public User unfavoriteGame(String userId, String gameId){ 103 | return addOrRemoveGameFromUserFavoriteGames(userId, gameId, false); 104 | } 105 | 106 | public User followCategory(String userId, String categoryId){ 107 | return followHelper(userId, categoryId, true); 108 | } 109 | 110 | public User unfollowCategory(String userId, String categoryId){ 111 | return followHelper(userId, categoryId, false); 112 | } 113 | 114 | private void findInvoicesInCurrentMonthAndUpdateTheirFees(ContractRecord contractRecord, Subscription subscription){ 115 | // find invoices in current month to update their fees 116 | List invoicesInThisMonth = invoiceService.findInvoicesOfContractRecordInCurrentMonth( 117 | contractRecord.getId()); 118 | for (Invoice invoice : invoicesInThisMonth){ 119 | invoice.setFee(subscription.getMonthlyFee()); 120 | } 121 | } 122 | 123 | /** 124 | * Helper method to avoid duplicate codes. 125 | * boolean isAddition field checks if it is a favorite or unfavorite method 126 | */ 127 | private User addOrRemoveGameFromUserFavoriteGames(String userId, String gameId, boolean isAddition){ 128 | User user = findById(userId); 129 | Game game = gameService.findGameById(gameId); 130 | List games = user.getFavoriteGames(); 131 | 132 | if (isAddition && !games.contains(game)) games.add(game); 133 | else if (!isAddition && games.contains(game)) games.remove(game); 134 | else return user; // there is no change, don't save anything just return the user 135 | 136 | repository.save(user); 137 | return user; 138 | } 139 | 140 | /** 141 | * Helper method to avoid duplicate codes.
142 | * (TODO still needs a refactor of duplicate codes with addOrRemoveGameFromUserFavoriteGames method)
143 | * To refactor, maybe use ICrudService interface? and give the method of class and its service.
144 | * boolean isFollow field checks if it is a follow or unfollow request 145 | */ 146 | private User followHelper(String userId, String categoryId, boolean isFollow){ 147 | User user = findById(userId); 148 | Category category = categoryService.findCategoryById(categoryId); 149 | 150 | List categories = user.getFollowedCategories(); 151 | 152 | if (isFollow && !categories.contains(category)) categories.add(category); 153 | else if (!isFollow && categories.contains(category)) categories.remove(category); 154 | else return user; // there is no change, don't save anything just return the user 155 | 156 | repository.save(user); 157 | return user; 158 | } 159 | 160 | /** Disable downgrade logic for ADMIN users. Only regular members downgrade when unpaid invoices */ 161 | public void downgradeNonAdminUserRoleToGuest(String userId){ 162 | User user = findById(userId); 163 | if (user == null) throw new EntityNotFoundException(); 164 | 165 | if (user.getRole() != UserRole.ADMIN) { 166 | user.setRole(UserRole.GUEST); 167 | repository.save(user); 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | --------------------------------------------------------------------------------