diff --git a/.gitignore b/.gitignore index c2065bc..39064d5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +# oauth keys +src/main/resources/oauth2.txt diff --git a/build.gradle b/build.gradle index a1a6d59..47d61c5 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'org.slf4j:slf4j-api:2.0.9' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' diff --git a/src/main/java/com/quizapp/service/QuizServiceApplication.java b/src/main/java/com/quizapp/service/QuizServiceApplication.java index a95164f..879a28e 100644 --- a/src/main/java/com/quizapp/service/QuizServiceApplication.java +++ b/src/main/java/com/quizapp/service/QuizServiceApplication.java @@ -2,8 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; @SpringBootApplication public class QuizServiceApplication { @@ -12,12 +10,3 @@ public static void main(String[] args) { SpringApplication.run(QuizServiceApplication.class, args); } } - -@RestController -class TestQuizController { - - @GetMapping("/") - public String getQuiz() { - return "Hello World"; - } -} diff --git a/src/main/java/com/quizapp/service/QuizServiceConstants.java b/src/main/java/com/quizapp/service/QuizServiceConstants.java new file mode 100644 index 0000000..16576ed --- /dev/null +++ b/src/main/java/com/quizapp/service/QuizServiceConstants.java @@ -0,0 +1,10 @@ +package com.quizapp.service; + +public final class QuizServiceConstants { + + private QuizServiceConstants() { + throw new IllegalStateException("Utility class"); + } + + public static final String SECRET_KEY = System.getenv("SECRET_KEY_QUIZ_SERVICE"); +} diff --git a/src/main/java/com/quizapp/service/data/configuration/AppConfig.java b/src/main/java/com/quizapp/service/data/configuration/AppConfig.java new file mode 100644 index 0000000..e6fdcbd --- /dev/null +++ b/src/main/java/com/quizapp/service/data/configuration/AppConfig.java @@ -0,0 +1,24 @@ +package com.quizapp.service.data.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class AppConfig { + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("http://localhost:3000"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/src/main/java/com/quizapp/service/data/controller/QuestionController.java b/src/main/java/com/quizapp/service/data/controller/QuestionController.java index 6e9e4d9..244c77c 100644 --- a/src/main/java/com/quizapp/service/data/controller/QuestionController.java +++ b/src/main/java/com/quizapp/service/data/controller/QuestionController.java @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin +@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") @RestController @RequestMapping("/api/questions") public class QuestionController { diff --git a/src/main/java/com/quizapp/service/data/controller/QuizController.java b/src/main/java/com/quizapp/service/data/controller/QuizController.java index 54252bb..4ad1e3e 100644 --- a/src/main/java/com/quizapp/service/data/controller/QuizController.java +++ b/src/main/java/com/quizapp/service/data/controller/QuizController.java @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin +@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") @RestController @RequestMapping("api/quizzes") public class QuizController { diff --git a/src/main/java/com/quizapp/service/data/controller/SessionController.java b/src/main/java/com/quizapp/service/data/controller/SessionController.java new file mode 100644 index 0000000..e607bbb --- /dev/null +++ b/src/main/java/com/quizapp/service/data/controller/SessionController.java @@ -0,0 +1,70 @@ +package com.quizapp.service.data.controller; + +import com.quizapp.service.QuizServiceConstants; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Date; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") +@RestController +@RequestMapping("/api/session") +public class SessionController { + + private static final Logger logger = LoggerFactory.getLogger(SessionController.class); + + @GetMapping("/validate") + public ResponseEntity validateSession(HttpServletRequest request) { + boolean isValidSession = false; + String token = null; + + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("authToken".equals(cookie.getName())) { + token = cookie.getValue(); + break; + } + } + } + + if (token != null && !token.isEmpty()) { + try { + Claims claims = + Jwts.parser() + .setSigningKey(QuizServiceConstants.SECRET_KEY) + .parseClaimsJws(token) + .getBody(); + + if (claims.getExpiration().after(new Date())) { + isValidSession = true; + } + } catch (Exception e) { + logger.error("Error validating session: {}", e.getMessage()); + } + } + + return ResponseEntity.ok().body("{\"isValidSession\":" + isValidSession + "}"); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletResponse response) { + Cookie cookie = new Cookie("authToken", null); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(0); + response.addCookie(cookie); + + return ResponseEntity.ok().body("{\"message\":\"Successfully logged out.\"}"); + } +} diff --git a/src/main/java/com/quizapp/service/data/controller/UserController.java b/src/main/java/com/quizapp/service/data/controller/UserController.java new file mode 100644 index 0000000..a780026 --- /dev/null +++ b/src/main/java/com/quizapp/service/data/controller/UserController.java @@ -0,0 +1,48 @@ +package com.quizapp.service.data.controller; + +import com.quizapp.service.data.dto.UserDataDTO; +import com.quizapp.service.data.entity.User; +import com.quizapp.service.data.service.UserService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.transaction.Transactional; +import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") +@RestController +@RequestMapping("/api/users") +public class UserController { + + private final UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @Transactional + @PostMapping("/login") + public ResponseEntity createUser( + @NotNull @RequestBody UserDataDTO userData, HttpServletResponse response) { + + User user = + userService.createOrUpdateUser( + userData.getEmail(), userData.getToken(), userData.getExpiresAt()); + + Cookie cookie = new Cookie("token", userData.getToken()); + cookie.setHttpOnly(true); + cookie.setMaxAge(60 * 60 * 24 * 365); // 1 year + cookie.setSecure(true); + cookie.setPath("/"); + response.addCookie(cookie); + + return ResponseEntity.ok(user); + } +} diff --git a/src/main/java/com/quizapp/service/data/dto/UserDataDTO.java b/src/main/java/com/quizapp/service/data/dto/UserDataDTO.java new file mode 100644 index 0000000..bdc7a1f --- /dev/null +++ b/src/main/java/com/quizapp/service/data/dto/UserDataDTO.java @@ -0,0 +1,15 @@ +package com.quizapp.service.data.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserDataDTO { + + private String token; + private Long expiresAt; + private String email; +} diff --git a/src/main/java/com/quizapp/service/data/entity/User.java b/src/main/java/com/quizapp/service/data/entity/User.java index 42338e5..e68e1a0 100644 --- a/src/main/java/com/quizapp/service/data/entity/User.java +++ b/src/main/java/com/quizapp/service/data/entity/User.java @@ -6,8 +6,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; import jakarta.persistence.Table; import java.util.Set; import lombok.AllArgsConstructor; @@ -15,7 +13,6 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Entity @Table(name = "\"User\"") @@ -23,21 +20,19 @@ @AllArgsConstructor @NoArgsConstructor public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; - private String username; - private String password; + @Column(unique = true, nullable = false) private String email; - @PrePersist - @PreUpdate - private void hashPassword() { - BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); - this.password = passwordEncoder.encode(this.password); - } + @Column(unique = true) + private String token; + + private Long expiresAt; @ToString.Exclude @EqualsAndHashCode.Exclude diff --git a/src/main/java/com/quizapp/service/data/repository/UserRepository.java b/src/main/java/com/quizapp/service/data/repository/UserRepository.java new file mode 100644 index 0000000..23fc591 --- /dev/null +++ b/src/main/java/com/quizapp/service/data/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.quizapp.service.data.repository; + +import com.quizapp.service.data.entity.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/quizapp/service/data/service/UserService.java b/src/main/java/com/quizapp/service/data/service/UserService.java new file mode 100644 index 0000000..65d831b --- /dev/null +++ b/src/main/java/com/quizapp/service/data/service/UserService.java @@ -0,0 +1,33 @@ +package com.quizapp.service.data.service; + +import com.quizapp.service.data.entity.User; +import com.quizapp.service.data.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + private final UserRepository userRepository; + + @Autowired + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User createOrUpdateUser(String email, String token, Long expiresAt) { + User existingUser = userRepository.findByEmail(email).orElse(null); + + if (existingUser != null) { + existingUser.setToken(token); + existingUser.setExpiresAt(expiresAt); + return userRepository.save(existingUser); + } else { + User user = new User(); + user.setEmail(email); + user.setToken(token); + user.setExpiresAt(expiresAt); + + return userRepository.save(user); + } + } +}