From 7640719967c7e05f1d40998c22d9f5cab887c594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Thu, 14 Dec 2023 21:12:18 +0100 Subject: [PATCH 01/11] Setup infobip hardcoded email test. --- backend/pom.xml | 5 ++ .../controller/UserController.java | 29 +++++++++++ .../rimatchbackend/lib/SendEmailLib.java | 50 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 10 ++++ .../src/main/resources/application.properties | 2 + 5 files changed, 96 insertions(+) create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java diff --git a/backend/pom.xml b/backend/pom.xml index e9540cd..249a2ad 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -52,6 +52,11 @@ 0.12.3 runtime + + com.infobip + infobip-api-java-client + 4.0.0 + org.springframework.security diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java index 23e44ef..a4c1be4 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java @@ -1,10 +1,13 @@ package com.rimatch.rimatchbackend.controller; import com.rimatch.rimatchbackend.dto.SetupDto; +import com.rimatch.rimatchbackend.lib.SendEmailLib; import com.rimatch.rimatchbackend.model.User; import com.rimatch.rimatchbackend.service.UserService; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import lombok.Getter; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,13 +15,21 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + @RestController @RequestMapping("api/users") public class UserController { + @Autowired + private SendEmailLib sendEmailLib; + @Autowired private UserService userService; @@ -88,4 +99,22 @@ public Map handleValidationException(MethodArgumentNotValidExcept return errors; } + @Getter + private static class EmailTestBody { + private String message; + } + + @PostMapping("/email-test") + public ResponseEntity postMethodName(@RequestBody EmailTestBody body) { + var recepients = new ArrayList<>(List.of("andrej.bozic6@gmail.com")); + try { + sendEmailLib.sendEmail(recepients, "RiMatchTest", body.getMessage()); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + + } + + } \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java new file mode 100644 index 0000000..0b75328 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java @@ -0,0 +1,50 @@ +package com.rimatch.rimatchbackend.lib; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.infobip.ApiClient; +import com.infobip.ApiException; +import com.infobip.ApiKey; +import com.infobip.BaseUrl; +import com.infobip.api.EmailApi; + +@Component +public class SendEmailLib { + + @Value("${infobip.api-key}") + private String API_KEY; + + @Value("${infobip.base-url}") + private String BASE_URL; + + private static final String SENDER_EMAIL_ADDRESS = "RiMatchApp "; + + public void sendEmail(List recepientEmailAddress, String subject, String text) throws ApiException { + var sendEmailApi = initEmailApi(); + try { + var emailResponse = sendEmailApi.sendEmail(recepientEmailAddress) + .from(SENDER_EMAIL_ADDRESS) + .subject(subject) + .text(text) + .execute(); + + System.out.println("Response body: " + emailResponse); + + var reportsResponse = sendEmailApi.getEmailDeliveryReports().execute(); + System.out.println(reportsResponse.getResults()); + } catch (ApiException e) { + System.out.println("HTTP status code: " + e.responseStatusCode()); + System.out.println("Response body: " + e.rawResponseBody()); + throw e; + } + } + + private EmailApi initEmailApi() { + var apiClient = ApiClient.forApiKey(ApiKey.from(API_KEY)).withBaseUrl(BaseUrl.from(BASE_URL)).build(); + + return new EmailApi(apiClient); + } +} diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1e32b1c..d199b96 100644 --- a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -13,5 +13,15 @@ "name": "cors.allowed-origins", "type": "java.lang.String[]", "description": "A description for 'cors.allowed-origins'" + }, + { + "name": "infobip.base-url", + "type": "java.lang.String", + "description": "A description for 'infobip.base-url'" + }, + { + "name": "infobip.api-key", + "type": "java.lang.String", + "description": "A description for 'infobip.api-key'" } ]} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 9543e1d..cdd873d 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -4,6 +4,8 @@ spring.data.mongodb.auto-index-creation=true jwt.secret.access=${JWT_SECRET_ACCESS} jwt.secret.refresh=${JWT_SECRET_REFRESH} cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173} +infobip.api-key=${INFOBIP_API_KEY} +infobip.base-url=${INFOBIP_BASE_URL:https://3gzl6n.api.infobip.com} springdoc.packages-to-scan=com.rimatch.rimatchbackend springdoc.paths-to-match=/api/** \ No newline at end of file From bece3c4cde78a7f99d4fcd6e4c1c3fdb47166054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Sat, 16 Dec 2023 10:46:08 +0100 Subject: [PATCH 02/11] Add infobip api test to separate controller. --- .../controller/TestInfobipApiController.java | 53 +++++++++++++++++++ .../controller/UserController.java | 25 --------- .../rimatchbackend/lib/SendEmailLib.java | 10 +++- ...itional-spring-configuration-metadata.json | 5 ++ .../src/main/resources/application.properties | 3 +- 5 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java new file mode 100644 index 0000000..7626b28 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java @@ -0,0 +1,53 @@ +package com.rimatch.rimatchbackend.controller; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.rimatch.rimatchbackend.lib.SendEmailLib; +import com.rimatch.rimatchbackend.util.converter.ToLowerCaseConverter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@RestController +@RequestMapping("api/infobip") +public class TestInfobipApiController { + + @Autowired + private SendEmailLib sendEmailLib; + + @Getter + private static class EmailTestBody { + @NotNull(message = "Message field cannot be null!") + @NotBlank(message = "Message field cannot be blank!") + private String message; + @NotNull(message = "Recipient field cannot be null!") + @NotBlank(message = "Recipient field cannot be blank!") + @JsonSerialize(converter = ToLowerCaseConverter.class) + private String recipient; + @NotNull(message = "Subject field cannot be null!") + @NotBlank(message = "Subject field cannot be blank!") + private String subject; + } + + @PostMapping("/email-test") + public ResponseEntity postMethodName(@RequestBody EmailTestBody body) { + var recepients = new ArrayList<>(List.of(body.getRecipient())); + try { + sendEmailLib.sendEmail(recepients, body.getSubject(), body.getMessage()); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java index a4c1be4..22429d7 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java @@ -1,12 +1,10 @@ package com.rimatch.rimatchbackend.controller; import com.rimatch.rimatchbackend.dto.SetupDto; -import com.rimatch.rimatchbackend.lib.SendEmailLib; import com.rimatch.rimatchbackend.model.User; import com.rimatch.rimatchbackend.service.UserService; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; -import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -15,9 +13,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,9 +23,6 @@ @RequestMapping("api/users") public class UserController { - @Autowired - private SendEmailLib sendEmailLib; - @Autowired private UserService userService; @@ -99,22 +92,4 @@ public Map handleValidationException(MethodArgumentNotValidExcept return errors; } - @Getter - private static class EmailTestBody { - private String message; - } - - @PostMapping("/email-test") - public ResponseEntity postMethodName(@RequestBody EmailTestBody body) { - var recepients = new ArrayList<>(List.of("andrej.bozic6@gmail.com")); - try { - sendEmailLib.sendEmail(recepients, "RiMatchTest", body.getMessage()); - return ResponseEntity.ok().build(); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - - } - - } \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java index 0b75328..2463d1f 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java @@ -20,13 +20,19 @@ public class SendEmailLib { @Value("${infobip.base-url}") private String BASE_URL; - private static final String SENDER_EMAIL_ADDRESS = "RiMatchApp "; + // Sender email address must be verified on the infobip portal for your account + @Value("${infobip.sender-email}") + private String SENDER_EMAIL_ADDRESS; + + private String getSenderEmail() { + return String.format("RiMatchApp <%s>", SENDER_EMAIL_ADDRESS); + } public void sendEmail(List recepientEmailAddress, String subject, String text) throws ApiException { var sendEmailApi = initEmailApi(); try { var emailResponse = sendEmailApi.sendEmail(recepientEmailAddress) - .from(SENDER_EMAIL_ADDRESS) + .from(getSenderEmail()) .subject(subject) .text(text) .execute(); diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json index d199b96..0a2848d 100644 --- a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -23,5 +23,10 @@ "name": "infobip.api-key", "type": "java.lang.String", "description": "A description for 'infobip.api-key'" + }, + { + "name": "infobip.sender-email", + "type": "java.lang.String", + "description": "A description for 'infobip.sender-email'" } ]} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index cdd873d..270e28e 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -5,7 +5,8 @@ jwt.secret.access=${JWT_SECRET_ACCESS} jwt.secret.refresh=${JWT_SECRET_REFRESH} cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173} infobip.api-key=${INFOBIP_API_KEY} -infobip.base-url=${INFOBIP_BASE_URL:https://3gzl6n.api.infobip.com} +infobip.base-url=${INFOBIP_BASE_URL} +infobip.sender-email=${INFOBIP_SENDER_EMAIL} springdoc.packages-to-scan=com.rimatch.rimatchbackend springdoc.paths-to-match=/api/** \ No newline at end of file From c698024aa02dab408fc431006ab0205be298f05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Tue, 19 Dec 2023 17:24:04 +0100 Subject: [PATCH 03/11] Fetch and display potential users (no like / rejection logic) --- .../controller/UserController.java | 14 +-- .../rimatchbackend/service/UserService.java | 6 +- frontend/index.html | 2 +- frontend/src/api/users.ts | 9 ++ frontend/src/components/MatchCard.tsx | 56 ++++++--- frontend/src/components/Navbar.tsx | 110 +++++++++--------- frontend/src/context/CurrentUserProvider.tsx | 2 +- frontend/src/index.css | 2 +- 8 files changed, 119 insertions(+), 82 deletions(-) diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java index 23e44ef..c483a99 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java @@ -22,17 +22,17 @@ public class UserController { @Autowired private UserService userService; - @GetMapping("/potentional") - public ResponseEntity getPotentinalMatch(HttpServletRequest request){ + @GetMapping("/potential") + public ResponseEntity getPotentinalMatch(HttpServletRequest request) { String authToken = request.getHeader("Authorization"); /* - Replace with something that doesn't query a database but ok for now - */ + * Replace with something that doesn't query a database but ok for now + */ User user = userService.getUserByToken(authToken); - User randomUser = userService.getRandomUser(user.getEmail(),user.getPreferences().getPartnerGender()); + var randomUsers = userService.getRandomUser(user.getEmail(), user.getPreferences().getPartnerGender()); - if (randomUser != null) { - return new ResponseEntity<>(randomUser, HttpStatus.OK); + if (randomUsers != null) { + return new ResponseEntity<>(randomUsers, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java index 583b073..9bae791 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java @@ -134,17 +134,17 @@ public User getUserByEmail(String email){ return userRepository.findByEmail(email); } - public User getRandomUser(String currentUserEmail,char genderPreference) { + public List getRandomUser(String currentUserEmail,char genderPreference) { // Exclude the current user List users = mongoTemplate.aggregate( Aggregation.newAggregation( Aggregation.match(Criteria.where("email").ne(currentUserEmail)), Aggregation.match(Criteria.where("gender").is(genderPreference)), Aggregation.match(Criteria.where("active").is(true)), - Aggregation.sample(1) + Aggregation.sample(10) ), "users", User.class).getMappedResults(); - return users.isEmpty() ? null : users.get(0); + return users.isEmpty() ? null : users; } // add more methods as per your requirements } diff --git a/frontend/index.html b/frontend/index.html index 56bae30..7963a1e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ RiMatch -
+
diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index c6f0217..bfec9e9 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -68,4 +68,13 @@ export const UsersService = { ...mutationOptions, }); }, + + useGetPotentailUsers: () => { + const axios = useAxiosPrivate(); + return useQuery({ + queryKey: ["UsersService.getPotentialUsers"], + queryFn: () => axios.get("/users/potential").then((res) => res.data), + staleTime: Infinity, + }); + }, }; diff --git a/frontend/src/components/MatchCard.tsx b/frontend/src/components/MatchCard.tsx index ffc8eb4..6fc2ebb 100644 --- a/frontend/src/components/MatchCard.tsx +++ b/frontend/src/components/MatchCard.tsx @@ -1,36 +1,66 @@ import ClearIcon from "@mui/icons-material/Clear"; import CheckIcon from "@mui/icons-material/Check"; +import { UsersService } from "@/api/users"; +import cx from "classnames"; +import { useState } from "react"; + const MatchCard = () => { + const [currentUser, setCurrentUser] = useState(0); + const result = UsersService.useGetPotentailUsers(); + + if (result.isLoading) { + return null; + } + + if (result.isError || !result.isSuccess) { + return
Error: {result.error?.message}
; + } + + const user = result.data[currentUser]; + + const nextUser = () => { + setCurrentUser((prev) => (prev + 1) % result.data.length); + }; + return (
-
+
-
+

- Adela, 23 + {`${user.firstName}, ${user.age}`}

-

- Zagreb, Hrvatska -

+

{user.location}

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. - Laudantium recusandae iure -

+

{user.description}

- -
diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index fc09c96..a2f5f7e 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -18,64 +18,62 @@ const Navbar: React.FunctionComponent = () => { setIsDropdownOpen(!isDropdownOpen); }; return ( -
-
+ ); }; diff --git a/frontend/src/context/CurrentUserProvider.tsx b/frontend/src/context/CurrentUserProvider.tsx index 264480b..eeb7c8f 100644 --- a/frontend/src/context/CurrentUserProvider.tsx +++ b/frontend/src/context/CurrentUserProvider.tsx @@ -10,7 +10,7 @@ const CurrentUserContextProvider = ({ }) => { const currentUserQuery = UsersService.useGetCurrentUser(); if (currentUserQuery.isLoading) { - return

Loading...

; + return null; } if (currentUserQuery.isError) { diff --git a/frontend/src/index.css b/frontend/src/index.css index f2896ee..f47d6df 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -43,7 +43,7 @@ } -html, body { +html, body, #root { height: 100%; margin: 0; From af70c23f2de2b16b87eb09e3a5e18ee39dcbe8f3 Mon Sep 17 00:00:00 2001 From: dominikkovacevic Date: Tue, 9 Jan 2024 00:12:05 +0100 Subject: [PATCH 04/11] Initial matching functionality. --- .../configuration/AppConfiguration.java | 5 +- .../controller/MatchController.java | 71 +++++++++++++++++ .../rimatchbackend/dto/DisplayUserDto.java | 33 ++++++++ .../rimatch/rimatchbackend/dto/MatchDto.java | 15 ++++ .../rimatchbackend/dto/RegisterDto.java | 1 - .../rimatch/rimatchbackend/model/Match.java | 28 +++++++ .../rimatchbackend/model/Preferences.java | 2 - .../rimatch/rimatchbackend/model/User.java | 9 ++- .../repository/MatchRepository.java | 12 +++ .../rimatchbackend/service/MatchService.java | 78 +++++++++++++++++++ .../rimatchbackend/service/UserService.java | 15 +++- .../util/DisplayUserConverter.java | 29 +++++++ 12 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/dto/MatchDto.java create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/repository/MatchRepository.java create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/configuration/AppConfiguration.java b/backend/src/main/java/com/rimatch/rimatchbackend/configuration/AppConfiguration.java index f81145a..115c539 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/configuration/AppConfiguration.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/configuration/AppConfiguration.java @@ -8,6 +8,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.EnableMongoAuditing; + + +import java.util.Arrays; import java.util.Collections; @Configuration @@ -25,7 +28,7 @@ FilterRegistrationBean jwtFilterRegistrationBean(){ final FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(jwtAuthenticationFilter); - registrationBean.setUrlPatterns(Collections.singletonList("/api/users/*")); + registrationBean.setUrlPatterns(Arrays.asList("/api/users/*","/api/match/*")); registrationBean.setOrder(1); return registrationBean; diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java new file mode 100644 index 0000000..8005473 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java @@ -0,0 +1,71 @@ +package com.rimatch.rimatchbackend.controller; + +import com.rimatch.rimatchbackend.dto.DisplayUserDto; +import com.rimatch.rimatchbackend.dto.MatchDto; +import com.rimatch.rimatchbackend.model.Match; +import com.rimatch.rimatchbackend.model.User; +import com.rimatch.rimatchbackend.service.MatchService; +import com.rimatch.rimatchbackend.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("api/match") +public class MatchController { + + @Autowired + MatchService matchService; + + @Autowired + UserService userService; + + @GetMapping("/potential") + public List getPotentinalMatch(HttpServletRequest request){ + String authToken = request.getHeader("Authorization"); + User user = userService.getUserByToken(authToken); + List list = matchService.findPotentialMatches(user); + return list; + } + + @PostMapping("/accept") + public ResponseEntity accept(HttpServletRequest request, @Valid @RequestBody MatchDto matchDto){ + String authToken = request.getHeader("Authorization"); + User user = userService.getUserByToken(authToken); + userService.insertToSeenUserIds(user,matchDto.getUserId()); + + Optional matchedUser = userService.getUserById(matchDto.getUserId()); + + Match match = matchService.findMatch(user.getId(),matchedUser.get().getId()); + + if(match != null){ + return ResponseEntity.ok(matchService.finishMatch(match,true)); + } + + return ResponseEntity.ok(matchService.saveMatch(user.getId(),matchDto.getUserId())); + } + + @PostMapping("/reject") + public ResponseEntity reject(HttpServletRequest request, @Valid @RequestBody MatchDto matchDto){ + String authToken = request.getHeader("Authorization"); + User user = userService.getUserByToken(authToken); + + userService.insertToSeenUserIds(user,matchDto.getUserId()); + Optional matchedUser = userService.getUserById(matchDto.getUserId()); + matchedUser.ifPresent(value -> userService.insertToSeenUserIds(value, user.getId())); + + Match match = matchService.findMatch(user.getId(),matchedUser.get().getId()); + + if(match != null){ + return ResponseEntity.ok(matchService.finishMatch(match,false)); + } + + return ResponseEntity.ok("Test"); + } +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java b/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java new file mode 100644 index 0000000..9f37994 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java @@ -0,0 +1,33 @@ +package com.rimatch.rimatchbackend.dto; + +import com.rimatch.rimatchbackend.model.User; +import lombok.Data; + +@Data +public class DisplayUserDto { + + private String id; + + private String firstName; + + private String lastName; + + private String description; + + private String profileImageUrl; + + private String location; + + private Character gender; + + public void initDisplayUser(User user){ + this.id = user.getId(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.description = user.getDescription(); + this.profileImageUrl = user.getProfileImageUrl(); + this.location = user.getLocation(); + this.gender = user.getGender(); + } + +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/dto/MatchDto.java b/backend/src/main/java/com/rimatch/rimatchbackend/dto/MatchDto.java new file mode 100644 index 0000000..d1980f3 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/dto/MatchDto.java @@ -0,0 +1,15 @@ +package com.rimatch.rimatchbackend.dto; + +import jakarta.validation.Valid; +import lombok.Getter; +import lombok.NonNull; + +@Getter +public class MatchDto { + + @Valid + + @NonNull + private String userId; + +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/dto/RegisterDto.java b/backend/src/main/java/com/rimatch/rimatchbackend/dto/RegisterDto.java index a36205c..fc3438e 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/dto/RegisterDto.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/dto/RegisterDto.java @@ -7,7 +7,6 @@ import lombok.Data; import lombok.Getter; -@Getter @Data public class RegisterDto { @NotNull(message = "Email field cannot be null!") diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java new file mode 100644 index 0000000..a977ac1 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java @@ -0,0 +1,28 @@ +package com.rimatch.rimatchbackend.model; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "matches") +@Data +public class Match { + + @Id + private String id; + + @NonNull + @NotBlank + private String firstUserId; + + @NonNull + @NotBlank + private String secondUserId; + + private boolean accepted = false; + + private boolean finished = false; +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/Preferences.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/Preferences.java index b017748..d48008f 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/model/Preferences.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/Preferences.java @@ -2,8 +2,6 @@ import lombok.*; @Data -@Getter -@Setter public class Preferences { private int ageGroupMin; diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/User.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/User.java index 5025488..cd16c77 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/model/User.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/User.java @@ -10,12 +10,12 @@ import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; +import java.util.ArrayList; import java.util.Date; +import java.util.List; @Document(collection = "users") @Data -@Getter -@Setter public class User { @Valid @@ -62,6 +62,8 @@ public class User { private Preferences preferences; + private List seenUserIds = new ArrayList<>(); + private Date lastSeen; @CreatedDate @@ -81,5 +83,4 @@ public User(@NonNull String email, @NonNull String firstName, @NonNull String la this.age = age; } -} - +} \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/repository/MatchRepository.java b/backend/src/main/java/com/rimatch/rimatchbackend/repository/MatchRepository.java new file mode 100644 index 0000000..1740c52 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/repository/MatchRepository.java @@ -0,0 +1,12 @@ +package com.rimatch.rimatchbackend.repository; + +import com.rimatch.rimatchbackend.model.Match; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +public interface MatchRepository extends MongoRepository { + + Optional findByFirstUserIdAndSecondUserId(String firstUserId,String secondUserId); + +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java new file mode 100644 index 0000000..a7e8fe6 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java @@ -0,0 +1,78 @@ +package com.rimatch.rimatchbackend.service; + +import com.rimatch.rimatchbackend.dto.DisplayUserDto; +import com.rimatch.rimatchbackend.model.Match; +import com.rimatch.rimatchbackend.model.User; +import com.rimatch.rimatchbackend.repository.MatchRepository; +import com.rimatch.rimatchbackend.repository.UserRepository; +import com.rimatch.rimatchbackend.util.DisplayUserConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class MatchService { + + private final MongoTemplate mongoTemplate; + + @Autowired + UserRepository userRepository; + + @Autowired + MatchRepository matchRepository; + + @Autowired + public MatchService(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + public Match saveMatch(String id1, String id2){ + return matchRepository.save(new Match(id1,id2)); + } + + public Match findMatch(String user1, String user2) { + Optional match1 = matchRepository.findByFirstUserIdAndSecondUserId(user1, user2); + + if(match1.isPresent()){ + return match1.get(); + } + + Optional match2 = matchRepository.findByFirstUserIdAndSecondUserId(user2, user1); + + return match2.orElse(null); + + } + + public Match finishMatch(Match match,boolean status){ + match.setFinished(true); + match.setAccepted(status); + return matchRepository.save(match); + } + + public List findPotentialMatches(User user){ + + return DisplayUserConverter.convertToDtoList(mongoTemplate.aggregate( + Aggregation.newAggregation( + + Aggregation.match( + Criteria.where("active").is(true) + .and("_id").nin(user.getSeenUserIds()) + .and("age").lte(user.getPreferences().getAgeGroupMax()) + .and("gender").is(user.getPreferences().getPartnerGender()) + .and("location").is(user.getLocation()) + .and("email").ne(user.getEmail()) + ), + //Second age parametar needs to defined separatenly because of limitations of the org.bson.Document + Aggregation.match(Criteria.where("age").gte(user.getPreferences().getAgeGroupMin())), + + Aggregation.limit(10) + ), + "users", User.class).getMappedResults()); + } + +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java index 583b073..c794da5 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/UserService.java @@ -12,8 +12,6 @@ import jakarta.servlet.http.Cookie; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; @@ -29,6 +27,7 @@ @Service public class UserService { private final MongoTemplate mongoTemplate; + @Autowired private UserRepository userRepository; /* @@ -120,6 +119,8 @@ public User getUserByToken(String token){ return userRepository.findByEmail(jwtUtils.extractSubject(token, TokenType.ACCESS)); } + + public String refreshAccessToken(String token)throws IllegalArgumentException,JwtException { try { Claims claims = jwtUtils.validateToken(token, TokenType.REFRESH); @@ -134,6 +135,10 @@ public User getUserByEmail(String email){ return userRepository.findByEmail(email); } + public Optional getUserById(String id){ + return userRepository.findById(id); + } + public User getRandomUser(String currentUserEmail,char genderPreference) { // Exclude the current user List users = mongoTemplate.aggregate( @@ -146,5 +151,11 @@ public User getRandomUser(String currentUserEmail,char genderPreference) { "users", User.class).getMappedResults(); return users.isEmpty() ? null : users.get(0); } + + public void insertToSeenUserIds(User user, String id){ + user.getSeenUserIds().add(id); + userRepository.save(user); + } + // add more methods as per your requirements } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java b/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java new file mode 100644 index 0000000..36546c7 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java @@ -0,0 +1,29 @@ +package com.rimatch.rimatchbackend.util; + +import com.rimatch.rimatchbackend.dto.DisplayUserDto; +import com.rimatch.rimatchbackend.model.User; + +import java.util.List; +import java.util.stream.Collectors; + +public class DisplayUserConverter { + + public static DisplayUserDto convertToDto(User user) { + DisplayUserDto displayUserDto = new DisplayUserDto(); + displayUserDto.setId(user.getId()); + displayUserDto.setFirstName(user.getFirstName()); + displayUserDto.setLastName(user.getLastName()); + displayUserDto.setDescription(user.getDescription()); + displayUserDto.setProfileImageUrl(user.getProfileImageUrl()); + displayUserDto.setLocation(user.getLocation()); + displayUserDto.setGender(user.getGender()); + + return displayUserDto; + } + + public static List convertToDtoList(List users) { + return users.stream() + .map(DisplayUserConverter::convertToDto) + .collect(Collectors.toList()); + } +} From 9dfbdaa1b05bb38861451c0289e98847bf37f4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Thu, 11 Jan 2024 10:13:48 +0100 Subject: [PATCH 05/11] Connect matching with backend. --- .../controller/MatchController.java | 3 +- .../rimatchbackend/dto/DisplayUserDto.java | 3 + .../rimatch/rimatchbackend/model/Match.java | 1 - .../util/DisplayUserConverter.java | 1 + frontend/src/api/users.ts | 58 ++++++++++++++++++- frontend/src/components/MatchCard.tsx | 20 ++++--- 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java index 8005473..04f3fa4 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java @@ -9,7 +9,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -35,7 +34,7 @@ public List getPotentinalMatch(HttpServletRequest request){ } @PostMapping("/accept") - public ResponseEntity accept(HttpServletRequest request, @Valid @RequestBody MatchDto matchDto){ + public ResponseEntity accept(HttpServletRequest request, @Valid @RequestBody MatchDto matchDto){ String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); userService.insertToSeenUserIds(user,matchDto.getUserId()); diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java b/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java index 9f37994..c9c50a2 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/dto/DisplayUserDto.java @@ -20,6 +20,8 @@ public class DisplayUserDto { private Character gender; + private int age; + public void initDisplayUser(User user){ this.id = user.getId(); this.firstName = user.getFirstName(); @@ -28,6 +30,7 @@ public void initDisplayUser(User user){ this.profileImageUrl = user.getProfileImageUrl(); this.location = user.getLocation(); this.gender = user.getGender(); + this.age = user.getAge(); } } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java index a977ac1..bbc7bd5 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java @@ -1,7 +1,6 @@ package com.rimatch.rimatchbackend.model; import jakarta.validation.constraints.NotBlank; -import lombok.Builder; import lombok.Data; import lombok.NonNull; import org.springframework.data.annotation.Id; diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java b/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java index 36546c7..6848502 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/util/DisplayUserConverter.java @@ -17,6 +17,7 @@ public static DisplayUserDto convertToDto(User user) { displayUserDto.setProfileImageUrl(user.getProfileImageUrl()); displayUserDto.setLocation(user.getLocation()); displayUserDto.setGender(user.getGender()); + displayUserDto.setAge(user.getAge()); return displayUserDto; } diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index bfec9e9..7a9dd91 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -4,6 +4,7 @@ import { UseMutationOptions, useMutation, useQuery, + useQueryClient, } from "@tanstack/react-query"; export interface User { @@ -39,6 +40,17 @@ export interface PreferencesInitData { preferences: UserPreferences; } +interface Match { + id: string; + firstUserId: string; + secondUserId: string; + accepted?: boolean; + finished?: boolean; +} +interface MatchData { + userId: string; +} + export const UsersService = { useGetCurrentUser() { const { auth } = useAuth(); @@ -73,8 +85,52 @@ export const UsersService = { const axios = useAxiosPrivate(); return useQuery({ queryKey: ["UsersService.getPotentialUsers"], - queryFn: () => axios.get("/users/potential").then((res) => res.data), + queryFn: () => axios.get("/match/potential").then((res) => res.data), staleTime: Infinity, }); }, + + useAcceptMatch: ( + mutationOptions?: Omit< + UseMutationOptions, + "mutationFn" + > + ) => { + const axios = useAxiosPrivate(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data) => { + const response = await axios.post("/match/accept", data); + return response.data; + }, + onSuccess: () => { + return queryClient.invalidateQueries({ + queryKey: ["UsersService.getPotentialUsers"], + }); + }, + ...mutationOptions, + }); + }, + + useRejectMatch: ( + mutationOptions?: Omit< + UseMutationOptions, + "mutationFn" + > + ) => { + const axios = useAxiosPrivate(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data) => { + const response = await axios.post("/match/reject", data); + return response.data; + }, + onSuccess: () => { + return queryClient.invalidateQueries({ + queryKey: ["UsersService.getPotentialUsers"], + }); + }, + ...mutationOptions, + }); + }, }; diff --git a/frontend/src/components/MatchCard.tsx b/frontend/src/components/MatchCard.tsx index 6fc2ebb..2b966a2 100644 --- a/frontend/src/components/MatchCard.tsx +++ b/frontend/src/components/MatchCard.tsx @@ -2,13 +2,14 @@ import ClearIcon from "@mui/icons-material/Clear"; import CheckIcon from "@mui/icons-material/Check"; import { UsersService } from "@/api/users"; import cx from "classnames"; -import { useState } from "react"; const MatchCard = () => { - const [currentUser, setCurrentUser] = useState(0); const result = UsersService.useGetPotentailUsers(); + const acceptMatch = UsersService.useAcceptMatch(); + const rejectMatch = UsersService.useRejectMatch(); - if (result.isLoading) { + // TODO: Add loading spinner or something + if (result.isLoading || acceptMatch.isPending || rejectMatch.isPending) { return null; } @@ -16,11 +17,12 @@ const MatchCard = () => { return
Error: {result.error?.message}
; } - const user = result.data[currentUser]; + // TODO: Nicer message when there are no more users + if (result.data.length === 0) { + return
No more users
; + } - const nextUser = () => { - setCurrentUser((prev) => (prev + 1) % result.data.length); - }; + const user = result.data[0]; return (
@@ -53,13 +55,13 @@ const MatchCard = () => {
From 02cec4bc71a2b9105e1090d43f83116ae3af6900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Thu, 11 Jan 2024 10:33:23 +0100 Subject: [PATCH 06/11] Put types in separate files. --- frontend/src/api/auth.ts | 22 +--------- frontend/src/api/users.ts | 46 +------------------- frontend/src/context/AuthProvider.tsx | 24 +++------- frontend/src/context/CurrentUserProvider.tsx | 4 +- frontend/src/types/Auth.d.ts | 33 ++++++++++++++ frontend/src/types/Match.d.ts | 10 +++++ frontend/src/types/User.d.ts | 32 ++++++++++++++ frontend/src/views/Preferences.tsx | 3 +- 8 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 frontend/src/types/Auth.d.ts create mode 100644 frontend/src/types/Match.d.ts create mode 100644 frontend/src/types/User.d.ts diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index e48c39e..0021f51 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -1,25 +1,7 @@ +import type { User } from "@/types/User"; +import type { LoginData, RegisterData, TokenResponse } from "@/types/Auth"; import { axiosPublic } from "./config/axios"; import { UseMutationOptions, useMutation } from "@tanstack/react-query"; -import { User } from "./users"; - -interface TokenResponse { - token: string; - active: boolean; -} - -interface RegisterData { - email: string; - password: string; - firstName: string; - lastName: string; - gender: string; - age: number; -} - -interface LoginData { - email: string; - password: string; -} const AuthService = { useLogin: ( diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 7a9dd91..04e71fd 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -1,5 +1,7 @@ import useAuth from "@/hooks/useAuth"; import useAxiosPrivate from "@/hooks/useAxiosPrivate"; +import type { Match, MatchData } from "@/types/Match"; +import type { PreferencesInitData, User } from "@/types/User"; import { UseMutationOptions, useMutation, @@ -7,50 +9,6 @@ import { useQueryClient, } from "@tanstack/react-query"; -export interface User { - id: string; - email: string; - firstName: string; - lastName: string; - hashedPassword: string; - gender: string; - age: number; - active: boolean; - description: string; - profileImageUrl: string; - phoneNumber: string; - location: string; - preferences: UserPreferences; - lastSeen: string; - createdAt: string; - updatedAt: string; -} - -interface UserPreferences { - ageGroupMin: number; - ageGroupMax: number; - partnerGender: string; -} - -export interface PreferencesInitData { - description: string; - profileImageUrl: string; - phoneNumber: string; - location: string; - preferences: UserPreferences; -} - -interface Match { - id: string; - firstUserId: string; - secondUserId: string; - accepted?: boolean; - finished?: boolean; -} -interface MatchData { - userId: string; -} - export const UsersService = { useGetCurrentUser() { const { auth } = useAuth(); diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index ce7715f..2745822 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -1,26 +1,14 @@ +import { + AuthContextData, + AuthContextProps, + AuthContextType, +} from "@/types/Auth"; import { createContext, useState } from "react"; -interface StateContextType { - auth: T; - setAuth: React.Dispatch>; -} - -interface AuthObject { - user: Record; - accessToken: string; - active: boolean; -} - -type AuthContextType = StateContextType; - const AuthContext = createContext({} as AuthContextType); -interface AuthContextProps { - children: React.ReactNode; -} - export const AuthProvider = ({ children }: AuthContextProps) => { - const [auth, setAuth] = useState(null); + const [auth, setAuth] = useState(null); return ( {children} diff --git a/frontend/src/context/CurrentUserProvider.tsx b/frontend/src/context/CurrentUserProvider.tsx index eeb7c8f..5998ab6 100644 --- a/frontend/src/context/CurrentUserProvider.tsx +++ b/frontend/src/context/CurrentUserProvider.tsx @@ -1,6 +1,6 @@ -import { User, UsersService } from "@/api/users"; +import { UsersService } from "@/api/users"; import { createContext } from "react"; - +import type { User } from "@/types/User"; export const CurrentUserContext = createContext(null); const CurrentUserContextProvider = ({ diff --git a/frontend/src/types/Auth.d.ts b/frontend/src/types/Auth.d.ts new file mode 100644 index 0000000..aa7c39a --- /dev/null +++ b/frontend/src/types/Auth.d.ts @@ -0,0 +1,33 @@ +interface StateContextType { + auth: T; + setAuth: React.Dispatch>; +} +export interface AuthContextData { + user: Record; + accessToken: string; + active: boolean; +} +export type AuthContextType = StateContextType; + +export interface AuthContextProps { + children: React.ReactNode; +} + +export interface TokenResponse { + token: string; + active: boolean; +} + +export interface RegisterData { + email: string; + password: string; + firstName: string; + lastName: string; + gender: string; + age: number; +} + +export interface LoginData { + email: string; + password: string; +} diff --git a/frontend/src/types/Match.d.ts b/frontend/src/types/Match.d.ts new file mode 100644 index 0000000..ff5f43f --- /dev/null +++ b/frontend/src/types/Match.d.ts @@ -0,0 +1,10 @@ +export interface Match { + id: string; + firstUserId: string; + secondUserId: string; + accepted: boolean; + finished: boolean; +} +export interface MatchData { + userId: string; +} diff --git a/frontend/src/types/User.d.ts b/frontend/src/types/User.d.ts new file mode 100644 index 0000000..c2da98d --- /dev/null +++ b/frontend/src/types/User.d.ts @@ -0,0 +1,32 @@ +export interface User { + id: string; + email: string; + firstName: string; + lastName: string; + hashedPassword: string; + gender: string; + age: number; + active: boolean; + description: string; + profileImageUrl: string; + phoneNumber: string; + location: string; + preferences: UserPreferences; + lastSeen: string; + createdAt: string; + updatedAt: string; +} + +interface UserPreferences { + ageGroupMin: number; + ageGroupMax: number; + partnerGender: string; +} + +export interface PreferencesInitData { + description: string; + profileImageUrl: string; + phoneNumber: string; + location: string; + preferences: UserPreferences; +} diff --git a/frontend/src/views/Preferences.tsx b/frontend/src/views/Preferences.tsx index 376e03d..c3ef6f6 100644 --- a/frontend/src/views/Preferences.tsx +++ b/frontend/src/views/Preferences.tsx @@ -7,7 +7,8 @@ import "aos/dist/aos.css"; import MovmentButtons from "../components/MovmentButtons"; import { useNavigate } from "react-router-dom"; import useAuth from "@/hooks/useAuth"; -import { PreferencesInitData, UsersService } from "@/api/users"; +import { UsersService } from "@/api/users"; +import type { PreferencesInitData } from "@/types/User"; import ScrollToFieldError from "@/components/ScrollToFieldError"; const preferenceSchema = Yup.object({ From fdf3d58821e21ec21ae1da8ebb289a5e86b54414 Mon Sep 17 00:00:00 2001 From: dominikkovacevic Date: Fri, 12 Jan 2024 16:28:25 +0100 Subject: [PATCH 07/11] Merged infobip branch , merged responsivness branch and added some matching features --- .github/workflows/deploy-backend.yml | 4 + .../controller/MatchController.java | 17 +++- .../controller/TestInfobipApiController.java | 6 +- .../rimatchbackend/lib/InfobipClient.java | 94 +++++++++++++++++++ .../rimatchbackend/lib/SendEmailLib.java | 2 + .../rimatchbackend/service/MatchService.java | 2 +- .../src/main/resources/application.properties | 1 + 7 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/lib/InfobipClient.java diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 8db618d..a6d3029 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -51,4 +51,8 @@ jobs: -e CORS_ALLOWED_ORIGINS="${{vars.FRONTEND_URL}}" -e JWT_SECRET_ACCESS="${{ secrets.JWT_SECRET_ACCESS }}" -e JWT_SECRET_REFRESH="${{ secrets.JWT_SECRET_REFRESH }}" + -e INFOBIP_API_KEY = "${{ secrets.INFOBIP_API_KEY }}" + -e INFOBIP_BASE_URL = "${{ secrets.INFOBIP_BASE_URL }}" + -e INFOBIP_SENDER_EMAIL = "${{ secrets.INFOBIP_SENDER_EMAIL }}" + -e INFOBIP_PHONE_NUMBER = "${{ secrets.INFOBIP_PHONE_NUMBER }}" --name rimatch-backend-container dominikkovacevic/rimatch diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java index 04f3fa4..456dea0 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java @@ -1,7 +1,10 @@ package com.rimatch.rimatchbackend.controller; +import com.infobip.ApiException; import com.rimatch.rimatchbackend.dto.DisplayUserDto; import com.rimatch.rimatchbackend.dto.MatchDto; +import com.rimatch.rimatchbackend.lib.InfobipClient; +import com.rimatch.rimatchbackend.lib.SendEmailLib; import com.rimatch.rimatchbackend.model.Match; import com.rimatch.rimatchbackend.model.User; import com.rimatch.rimatchbackend.service.MatchService; @@ -12,6 +15,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -25,6 +29,12 @@ public class MatchController { @Autowired UserService userService; + /*@Autowired + private SendEmailLib sendEmailLib;*/ + + @Autowired + private InfobipClient infobipClient; + @GetMapping("/potential") public List getPotentinalMatch(HttpServletRequest request){ String authToken = request.getHeader("Authorization"); @@ -34,7 +44,7 @@ public List getPotentinalMatch(HttpServletRequest request){ } @PostMapping("/accept") - public ResponseEntity accept(HttpServletRequest request, @Valid @RequestBody MatchDto matchDto){ + public ResponseEntity accept(HttpServletRequest request, @Valid @RequestBody MatchDto matchDto) throws ApiException { String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); userService.insertToSeenUserIds(user,matchDto.getUserId()); @@ -44,6 +54,11 @@ public ResponseEntity accept(HttpServletRequest request, @Valid @RequestB Match match = matchService.findMatch(user.getId(),matchedUser.get().getId()); if(match != null){ + + var recepients = new ArrayList<>(List.of("dominikkovacevic6@gmail.com")); + var string = String.format("Hello %s you just matched with %s %s!",matchedUser.get().getFirstName(),user.getFirstName(),user.getLastName()); + infobipClient.sendEmail(recepients,"You got a match!", string); + infobipClient.sendSms(string); return ResponseEntity.ok(matchService.finishMatch(match,true)); } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java index 7626b28..1b783d3 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java @@ -41,13 +41,13 @@ private static class EmailTestBody { } @PostMapping("/email-test") - public ResponseEntity postMethodName(@RequestBody EmailTestBody body) { + public ResponseEntity postMethodName(@RequestBody EmailTestBody body) { var recepients = new ArrayList<>(List.of(body.getRecipient())); try { sendEmailLib.sendEmail(recepients, body.getSubject(), body.getMessage()); return ResponseEntity.ok().build(); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/lib/InfobipClient.java b/backend/src/main/java/com/rimatch/rimatchbackend/lib/InfobipClient.java new file mode 100644 index 0000000..2d9fd38 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/lib/InfobipClient.java @@ -0,0 +1,94 @@ +package com.rimatch.rimatchbackend.lib; + +import com.infobip.*; +import com.infobip.api.EmailApi; +import com.infobip.api.SmsApi; +import com.infobip.model.SmsAdvancedTextualRequest; +import com.infobip.model.SmsDestination; +import com.infobip.model.SmsResponse; +import com.infobip.model.SmsTextualMessage; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +@Component +public class InfobipClient { + + @Value("${infobip.api-key}") + private String API_KEY; + + @Value("${infobip.base-url}") + private String BASE_URL; + + // Sender email address must be verified on the infobip portal for your account + @Value("${infobip.sender-email}") + private String SENDER_EMAIL_ADDRESS; + + @Value("${infobip.phone-number}") + private String PHONE_NUMBER; + + private String getSenderEmail() { + return String.format("RiMatchApp <%s>", SENDER_EMAIL_ADDRESS); + } + + private EmailApi initEmailApi() { + return new EmailApi(initApiClient()); + } + + private SmsApi initSmsApi(){ + return new SmsApi(initApiClient()); + } + + private ApiClient initApiClient(){ + return ApiClient.forApiKey(ApiKey.from(API_KEY)).withBaseUrl(BaseUrl.from(BASE_URL)).build(); + } + + public void sendSms(String text){ + + var smsApi = initSmsApi(); + + SmsTextualMessage smsMessage = new SmsTextualMessage() + .from("InfoSMS") + .addDestinationsItem(new SmsDestination().to(PHONE_NUMBER)) + .text(text); + + SmsAdvancedTextualRequest smsMessageRequest = new SmsAdvancedTextualRequest() + .messages(List.of(smsMessage)); + + smsApi.sendSmsMessage(smsMessageRequest) + .executeAsync(new ApiCallback() { + @Override + public void onSuccess(SmsResponse result, int responseStatusCode, Map> responseHeaders) { + + } + @Override + public void onFailure(ApiException exception, int responseStatusCode, Map> responseHeaders) { + } + }); + } + + public void sendEmail(List recepientEmailAddress, String subject, String text) throws ApiException { + var sendEmailApi = initEmailApi(); + + + + try { + var emailResponse = sendEmailApi.sendEmail(recepientEmailAddress) + .from(getSenderEmail()) + .subject(subject) + .text(text) + .execute(); + + System.out.println("Response body: " + emailResponse); + + var reportsResponse = sendEmailApi.getEmailDeliveryReports().execute(); + System.out.println(reportsResponse.getResults()); + } catch (ApiException e) { + System.out.println("HTTP status code: " + e.responseStatusCode()); + System.out.println("Response body: " + e.rawResponseBody()); + throw e; + } + } +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java index 2463d1f..c7c85e8 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java @@ -48,6 +48,8 @@ public void sendEmail(List recepientEmailAddress, String subject, String } } + + private EmailApi initEmailApi() { var apiClient = ApiClient.forApiKey(ApiKey.from(API_KEY)).withBaseUrl(BaseUrl.from(BASE_URL)).build(); diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java index a7e8fe6..ae2ed4e 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.Fields; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.stereotype.Service; @@ -69,7 +70,6 @@ public List findPotentialMatches(User user){ ), //Second age parametar needs to defined separatenly because of limitations of the org.bson.Document Aggregation.match(Criteria.where("age").gte(user.getPreferences().getAgeGroupMin())), - Aggregation.limit(10) ), "users", User.class).getMappedResults()); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 270e28e..cc55412 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -7,6 +7,7 @@ cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173} infobip.api-key=${INFOBIP_API_KEY} infobip.base-url=${INFOBIP_BASE_URL} infobip.sender-email=${INFOBIP_SENDER_EMAIL} +infobip.phone-number=${INFOBIP_PHONE_NUMBER} springdoc.packages-to-scan=com.rimatch.rimatchbackend springdoc.paths-to-match=/api/** \ No newline at end of file From c3ba486d9ab237eb9d86a39ea54ceeb6aab8d167 Mon Sep 17 00:00:00 2001 From: dominikkovacevic Date: Fri, 12 Jan 2024 16:32:10 +0100 Subject: [PATCH 08/11] Fixed deploy-backend.yml --- .github/workflows/deploy-backend.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index a6d3029..5e4da4c 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -51,8 +51,8 @@ jobs: -e CORS_ALLOWED_ORIGINS="${{vars.FRONTEND_URL}}" -e JWT_SECRET_ACCESS="${{ secrets.JWT_SECRET_ACCESS }}" -e JWT_SECRET_REFRESH="${{ secrets.JWT_SECRET_REFRESH }}" - -e INFOBIP_API_KEY = "${{ secrets.INFOBIP_API_KEY }}" - -e INFOBIP_BASE_URL = "${{ secrets.INFOBIP_BASE_URL }}" - -e INFOBIP_SENDER_EMAIL = "${{ secrets.INFOBIP_SENDER_EMAIL }}" - -e INFOBIP_PHONE_NUMBER = "${{ secrets.INFOBIP_PHONE_NUMBER }}" + -e INFOBIP_API_KEY="${{ secrets.INFOBIP_API_KEY }}" + -e INFOBIP_BASE_URL="${{ secrets.INFOBIP_BASE_URL }}" + -e INFOBIP_SENDER_EMAIL="${{ secrets.INFOBIP_SENDER_EMAIL }}" + -e INFOBIP_PHONE_NUMBER="${{ secrets.INFOBIP_PHONE_NUMBER }}" --name rimatch-backend-container dominikkovacevic/rimatch From 0dae89eb8aeab1791d281d3e1ed856e9fa88fb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Tue, 16 Jan 2024 21:29:03 +0100 Subject: [PATCH 09/11] Display matches WIP. --- .../controller/MatchController.java | 9 ++++++ .../rimatchbackend/service/MatchService.java | 29 ++++++++++++++----- frontend/src/api/users.ts | 9 ++++++ frontend/src/components/MyMatches.tsx | 5 ++++ frontend/src/main.tsx | 10 ++++++- 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/MyMatches.tsx diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java index 456dea0..49b4e6f 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java @@ -82,4 +82,13 @@ public ResponseEntity reject(HttpServletRequest request, @Valid @RequestBody return ResponseEntity.ok("Test"); } + + // all matches for user sending the reuqest + @GetMapping("/all") + public List getAllMatches(HttpServletRequest request){ + String authToken = request.getHeader("Authorization"); + User user = userService.getUserByToken(authToken); + List list = matchService.getAllSuccessfulMatches(user); + return list; + } } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java index ae2ed4e..599d286 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java @@ -32,14 +32,14 @@ public MatchService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } - public Match saveMatch(String id1, String id2){ - return matchRepository.save(new Match(id1,id2)); + public Match saveMatch(String id1, String id2) { + return matchRepository.save(new Match(id1, id2)); } public Match findMatch(String user1, String user2) { Optional match1 = matchRepository.findByFirstUserIdAndSecondUserId(user1, user2); - if(match1.isPresent()){ + if (match1.isPresent()) { return match1.get(); } @@ -49,13 +49,13 @@ public Match findMatch(String user1, String user2) { } - public Match finishMatch(Match match,boolean status){ + public Match finishMatch(Match match, boolean status) { match.setFinished(true); match.setAccepted(status); return matchRepository.save(match); } - public List findPotentialMatches(User user){ + public List findPotentialMatches(User user) { return DisplayUserConverter.convertToDtoList(mongoTemplate.aggregate( Aggregation.newAggregation( @@ -66,13 +66,26 @@ public List findPotentialMatches(User user){ .and("age").lte(user.getPreferences().getAgeGroupMax()) .and("gender").is(user.getPreferences().getPartnerGender()) .and("location").is(user.getLocation()) - .and("email").ne(user.getEmail()) - ), - //Second age parametar needs to defined separatenly because of limitations of the org.bson.Document + .and("email").ne(user.getEmail())), + // Second age parametar needs to defined separatenly because of limitations of + // the org.bson.Document Aggregation.match(Criteria.where("age").gte(user.getPreferences().getAgeGroupMin())), Aggregation.limit(10) ), "users", User.class).getMappedResults()); } + public List getAllSuccessfulMatches(User user) { + Criteria criteria = new Criteria().andOperator( + Criteria.where("finished").is(true), + Criteria.where("accepted").is(true), + new Criteria().orOperator( + Criteria.where("firstUserId").is(user.getId()), + Criteria.where("secondUserId").is(user.getId()))); + + return mongoTemplate.aggregate( + Aggregation.newAggregation( + Aggregation.match(criteria)), + "matches", Match.class).getMappedResults(); + } } diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 04e71fd..2f31c79 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -91,4 +91,13 @@ export const UsersService = { ...mutationOptions, }); }, + + useGetMatches: () => { + const axios = useAxiosPrivate(); + return useQuery({ + queryKey: ["UsersService.getMatches"], + queryFn: () => axios.get("/match/all").then((res) => res.data), + staleTime: Infinity, + }); + }, }; diff --git a/frontend/src/components/MyMatches.tsx b/frontend/src/components/MyMatches.tsx new file mode 100644 index 0000000..6c18963 --- /dev/null +++ b/frontend/src/components/MyMatches.tsx @@ -0,0 +1,5 @@ +const MyMatches = () => { + return
MyMatches
; +}; + +export default MyMatches; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 334c70b..79be35f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -12,13 +12,21 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import Root from "./views/Root.tsx"; import Preferences from "./views/Preferences.tsx"; +import MyMatches from "./components/MyMatches.tsx"; const router = createBrowserRouter([ { path: "/", element: } />, errorElement: , - children: [{ index: true, element: }], + children: [ + { index: true, element: }, + { + path: "matches", + element: , + errorElement: , + }, + ], }, { path: "/init/preferences", From 6b238f16730b1a7b56883e6ceeaf088f57f72a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Tue, 16 Jan 2024 23:00:25 +0100 Subject: [PATCH 10/11] Fetching matches for current user, smiple my matches page design. --- .../controller/MatchController.java | 8 +-- .../rimatchbackend/service/MatchService.java | 54 ++++++++++++++----- ...itional-spring-configuration-metadata.json | 5 ++ frontend/src/api/users.ts | 4 +- frontend/src/components/MyMatches.tsx | 44 ++++++++++++++- frontend/src/components/Navbar.tsx | 11 +++- frontend/src/types/User.d.ts | 12 +++++ 7 files changed, 116 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java index 49b4e6f..8dc7b13 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java @@ -4,11 +4,11 @@ import com.rimatch.rimatchbackend.dto.DisplayUserDto; import com.rimatch.rimatchbackend.dto.MatchDto; import com.rimatch.rimatchbackend.lib.InfobipClient; -import com.rimatch.rimatchbackend.lib.SendEmailLib; import com.rimatch.rimatchbackend.model.Match; import com.rimatch.rimatchbackend.model.User; import com.rimatch.rimatchbackend.service.MatchService; import com.rimatch.rimatchbackend.service.UserService; + import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; @@ -85,10 +85,10 @@ public ResponseEntity reject(HttpServletRequest request, @Valid @RequestBody // all matches for user sending the reuqest @GetMapping("/all") - public List getAllMatches(HttpServletRequest request){ + public ResponseEntity> getAllMatches(HttpServletRequest request){ String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); - List list = matchService.getAllSuccessfulMatches(user); - return list; + List list = matchService.getAllSuccessfulMatchedUsers(user); + return ResponseEntity.ok(list); } } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java index 599d286..01fb929 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java @@ -6,15 +6,26 @@ import com.rimatch.rimatchbackend.repository.MatchRepository; import com.rimatch.rimatchbackend.repository.UserRepository; import com.rimatch.rimatchbackend.util.DisplayUserConverter; + +import lombok.Builder; +import lombok.Getter; + +import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.ComparisonOperators; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators; import org.springframework.data.mongodb.core.aggregation.Fields; +import org.springframework.data.mongodb.core.aggregation.LookupOperation; +import org.springframework.data.mongodb.core.aggregation.MatchOperation; +import org.springframework.data.mongodb.core.aggregation.ProjectionOperation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service public class MatchService { @@ -70,22 +81,39 @@ public List findPotentialMatches(User user) { // Second age parametar needs to defined separatenly because of limitations of // the org.bson.Document Aggregation.match(Criteria.where("age").gte(user.getPreferences().getAgeGroupMin())), - Aggregation.limit(10) - ), + Aggregation.limit(10)), "users", User.class).getMappedResults()); } - public List getAllSuccessfulMatches(User user) { - Criteria criteria = new Criteria().andOperator( - Criteria.where("finished").is(true), - Criteria.where("accepted").is(true), - new Criteria().orOperator( - Criteria.where("firstUserId").is(user.getId()), - Criteria.where("secondUserId").is(user.getId()))); + public List getAllSuccessfulMatchedUsers(User user) { + MatchOperation matchOperation = Aggregation.match( + Criteria.where("finished").is(true) + .and("accepted").is(true) + .andOperator( + new Criteria().orOperator( + Criteria.where("firstUserId").is(user.getId().toString()), + Criteria.where("secondUserId").is(user.getId().toString())))); - return mongoTemplate.aggregate( - Aggregation.newAggregation( - Aggregation.match(criteria)), - "matches", Match.class).getMappedResults(); + ProjectionOperation projectionOperation = Aggregation.project().and( + ConditionalOperators + .when(ComparisonOperators.Eq.valueOf("firstUserId").equalToValue(user.getId().toString())) + .thenValueOf("secondUserId") + .otherwiseValueOf("firstUserId")) + .as("matchedUserId"); + + Aggregation aggregation = Aggregation.newAggregation(matchOperation, projectionOperation); + + List matchedIds = mongoTemplate.aggregate(aggregation, "matches", MatchedUserIdDTO.class).getMappedResults(); + + List matchedUserIds = matchedIds.stream() + .map(matchedUserDTO -> matchedUserDTO.getMatchedUserId()) + .collect(Collectors.toList()); + + return DisplayUserConverter.convertToDtoList(userRepository.findAllById(matchedUserIds)); + } + + @Getter + private static class MatchedUserIdDTO { + private String matchedUserId; } } diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0a2848d..b08e306 100644 --- a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -28,5 +28,10 @@ "name": "infobip.sender-email", "type": "java.lang.String", "description": "A description for 'infobip.sender-email'" + }, + { + "name": "infobip.phone-number", + "type": "java.lang.String", + "description": "A description for 'infobip.phone-number'" } ]} \ No newline at end of file diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 2f31c79..d7600e3 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -1,7 +1,7 @@ import useAuth from "@/hooks/useAuth"; import useAxiosPrivate from "@/hooks/useAxiosPrivate"; import type { Match, MatchData } from "@/types/Match"; -import type { PreferencesInitData, User } from "@/types/User"; +import type { PreferencesInitData, ProjectedUser, User } from "@/types/User"; import { UseMutationOptions, useMutation, @@ -94,7 +94,7 @@ export const UsersService = { useGetMatches: () => { const axios = useAxiosPrivate(); - return useQuery({ + return useQuery({ queryKey: ["UsersService.getMatches"], queryFn: () => axios.get("/match/all").then((res) => res.data), staleTime: Infinity, diff --git a/frontend/src/components/MyMatches.tsx b/frontend/src/components/MyMatches.tsx index 6c18963..4cf26ec 100644 --- a/frontend/src/components/MyMatches.tsx +++ b/frontend/src/components/MyMatches.tsx @@ -1,5 +1,47 @@ +import { UsersService } from "@/api/users"; + const MyMatches = () => { - return
MyMatches
; + const query = UsersService.useGetMatches(); + + if (query.isLoading) { + return
Loading...
; + } + + if (query.isError || !query.isSuccess) { + return
Error
; + } + + const matches = query.data; + + return ( +
+
+

+ My matches +

+ {matches.map((match) => ( +
+
+ profile_picture +
+
+

+ {match.firstName} {match.lastName}, {match.age} +

+

{match.location}

+
+
+ ))} +
+
+ ); }; export default MyMatches; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index a2f5f7e..315e66c 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -8,6 +8,7 @@ import { } from "@/assets"; import useLogout from "@/hooks/useLogout"; import useCurrentUserContext from "@/hooks/useCurrentUser"; +import { Link } from "react-router-dom"; const Navbar: React.FunctionComponent = () => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -22,10 +23,16 @@ const Navbar: React.FunctionComponent = () => {
-

+ RiMatch -

+
+ + My matches +

{user.firstName}

; + interface UserPreferences { ageGroupMin: number; ageGroupMax: number; From 7e42a73d06745b0e65e721a7d8c365c1d8086d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Sat, 27 Jan 2024 10:55:21 +0100 Subject: [PATCH 11/11] Remove test infobip stuff. --- .../controller/TestInfobipApiController.java | 53 ----------------- .../rimatchbackend/lib/SendEmailLib.java | 58 ------------------- 2 files changed, 111 deletions(-) delete mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java delete mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java deleted file mode 100644 index 1b783d3..0000000 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/TestInfobipApiController.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.rimatch.rimatchbackend.controller; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -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; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.rimatch.rimatchbackend.lib.SendEmailLib; -import com.rimatch.rimatchbackend.util.converter.ToLowerCaseConverter; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; - -@RestController -@RequestMapping("api/infobip") -public class TestInfobipApiController { - - @Autowired - private SendEmailLib sendEmailLib; - - @Getter - private static class EmailTestBody { - @NotNull(message = "Message field cannot be null!") - @NotBlank(message = "Message field cannot be blank!") - private String message; - @NotNull(message = "Recipient field cannot be null!") - @NotBlank(message = "Recipient field cannot be blank!") - @JsonSerialize(converter = ToLowerCaseConverter.class) - private String recipient; - @NotNull(message = "Subject field cannot be null!") - @NotBlank(message = "Subject field cannot be blank!") - private String subject; - } - - @PostMapping("/email-test") - public ResponseEntity postMethodName(@RequestBody EmailTestBody body) { - var recepients = new ArrayList<>(List.of(body.getRecipient())); - try { - sendEmailLib.sendEmail(recepients, body.getSubject(), body.getMessage()); - return ResponseEntity.ok().build(); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java b/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java deleted file mode 100644 index c7c85e8..0000000 --- a/backend/src/main/java/com/rimatch/rimatchbackend/lib/SendEmailLib.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.rimatch.rimatchbackend.lib; - -import java.util.List; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import com.infobip.ApiClient; -import com.infobip.ApiException; -import com.infobip.ApiKey; -import com.infobip.BaseUrl; -import com.infobip.api.EmailApi; - -@Component -public class SendEmailLib { - - @Value("${infobip.api-key}") - private String API_KEY; - - @Value("${infobip.base-url}") - private String BASE_URL; - - // Sender email address must be verified on the infobip portal for your account - @Value("${infobip.sender-email}") - private String SENDER_EMAIL_ADDRESS; - - private String getSenderEmail() { - return String.format("RiMatchApp <%s>", SENDER_EMAIL_ADDRESS); - } - - public void sendEmail(List recepientEmailAddress, String subject, String text) throws ApiException { - var sendEmailApi = initEmailApi(); - try { - var emailResponse = sendEmailApi.sendEmail(recepientEmailAddress) - .from(getSenderEmail()) - .subject(subject) - .text(text) - .execute(); - - System.out.println("Response body: " + emailResponse); - - var reportsResponse = sendEmailApi.getEmailDeliveryReports().execute(); - System.out.println(reportsResponse.getResults()); - } catch (ApiException e) { - System.out.println("HTTP status code: " + e.responseStatusCode()); - System.out.println("Response body: " + e.rawResponseBody()); - throw e; - } - } - - - - private EmailApi initEmailApi() { - var apiClient = ApiClient.forApiKey(ApiKey.from(API_KEY)).withBaseUrl(BaseUrl.from(BASE_URL)).build(); - - return new EmailApi(apiClient); - } -}