From 878713923404660519874be5193a294627ca5432 Mon Sep 17 00:00:00 2001 From: tau-bar Date: Sat, 28 May 2022 21:30:13 +0800 Subject: [PATCH 1/4] add send email functionality --- backend/pom.xml | 9 + backend/setup/Swappee.postman_collection.json | 256 +++++++++++++++++- .../user/UserPrivateApiController.java | 1 - .../mail/MailPublicApiController.java | 56 ++++ .../swappee/dao/{ => like}/item/ItemDao.java | 2 +- .../dao/{ => like}/item/ItemDaoImpl.java | 2 +- .../swappee/service/item/ItemServiceImpl.java | 2 +- .../swappee/service/mail/EmailService.java | 8 + .../service/mail/EmailServiceImpl.java | 27 ++ .../configuration/JpaAuditingConfig.java | 2 +- .../utils/exception/UserFriendlyMessage.java | 5 + backend/src/main/resources/application.yml | 14 +- .../com/swappee/dao/item/ItemDaoImplTest.java | 2 +- 13 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java rename backend/src/main/java/com/swappee/dao/{ => like}/item/ItemDao.java (96%) rename backend/src/main/java/com/swappee/dao/{ => like}/item/ItemDaoImpl.java (99%) create mode 100644 backend/src/main/java/com/swappee/service/mail/EmailService.java create mode 100644 backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java diff --git a/backend/pom.xml b/backend/pom.xml index e9058b4..2257d72 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -92,6 +92,15 @@ imgscalr-lib 4.2 + + + org.springframework.boot + spring-boot-starter-mail + 2.7.0 + + + + diff --git a/backend/setup/Swappee.postman_collection.json b/backend/setup/Swappee.postman_collection.json index 8f85ca3..c8eefb8 100644 --- a/backend/setup/Swappee.postman_collection.json +++ b/backend/setup/Swappee.postman_collection.json @@ -1,8 +1,9 @@ { "info": { - "_postman_id": "4614ace9-62a7-4359-8375-1efb42aa8aed", + "_postman_id": "af01e8d4-2ded-4c87-9231-36fbfbddd809", "name": "Swappee", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "17174728" }, "item": [ { @@ -140,6 +141,47 @@ }, "response": [] }, + { + "name": "createAuthenticationToken Tauple", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var data = pm.response.json();\r", + "console.log(data.token);\r", + "pm.globals.set(\"token\", data.token);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"username\": \"tauple\",\r\n \"password\": \"apple\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SwappeeUrl}}/api/login/authenticate", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "login", + "authenticate" + ] + } + }, + "response": [] + }, { "name": "refreshAndGetAuthenticationToken", "request": { @@ -323,7 +365,7 @@ "method": "GET", "header": [], "url": { - "raw": "{{SwappeeUrl}}/api/public/user/MarcusTXK", + "raw": "{{SwappeeUrl}}/api/public/user/lyuzher", "host": [ "{{SwappeeUrl}}" ], @@ -331,7 +373,7 @@ "api", "public", "user", - "MarcusTXK" + "lyuzher" ] } }, @@ -498,13 +540,13 @@ "formdata": [ { "key": "itemDTO", - "value": "{\n\t\"id\": \"null\",\n\t\"userId\": \"1\",\n\t\"status\": \"OPEN\",\n\t\"name\": \"Samsung Galaxy Note 10+\",\n\t\"description\": \"Its big\",\n\t\"brand\": \"Samsung\",\n\t\"new\": \"false\",\n\t\"category\": \"Phones\",\n\t\"strict\": \"true\",\n\t\"likes\": \"2\",\n\t\"preferredCats\": [\"Phone\", \"Game\", \"Car\"],\n\t\"preferredItems\": [{\n\t\t\"name\": \"1\",\n\t\t\"category\": \"1\",\n\t\t\"brand\": \"1\",\n\t\t\"new\": \"true\"\n\t}, {\n\t\t\"name\": \"2\",\n\t\t\"category\": \"2\",\n\t\t\"brand\": \"2\",\n\t\t\"new\": \"true\"\n\t}],\n\t\"itemHistory\": []\n}", + "value": "{\n\t\"id\": \"14\",\n\t\"userId\": \"4\",\n\t\"status\": \"OPEN\",\n\t\"name\": \"Samsung Galaxy Note 10+\",\n\t\"description\": \"Its big\",\n\t\"brand\": \"Samsung\",\n\t\"new\": \"false\",\n\t\"category\": \"Phones\",\n\t\"strict\": \"true\",\n\t\"likes\": \"2\",\n\t\"preferredCats\": [\"Phone\", \"Game\", \"Car\"],\n\t\"preferredItems\": [{\n\t\t\"name\": \"1\",\n\t\t\"category\": \"1\",\n\t\t\"brand\": \"1\",\n\t\t\"new\": \"true\"\n\t}, {\n\t\t\"name\": \"2\",\n\t\t\"category\": \"2\",\n\t\t\"brand\": \"2\",\n\t\t\"new\": \"true\"\n\t}],\n\t\"itemHistory\": []\n}", "type": "text" }, { "key": "photos", "type": "file", - "src": "/C:/Users/Marcus Tang/Pictures/ProfilePics/Charmander original.png" + "src": "/C:/Users/User/Pictures/index.jpg" }, { "key": "descriptions", @@ -514,7 +556,7 @@ { "key": "photos", "type": "file", - "src": "/C:/Users/Marcus Tang/Pictures/ProfilePics/Charmander.jpg" + "src": "/C:/Users/User/Pictures/index.jpg" } ] }, @@ -626,7 +668,7 @@ "method": "GET", "header": [], "url": { - "raw": "{{SwappeeUrl}}/api/private/user/5", + "raw": "{{SwappeeUrl}}/api/private/user/4", "host": [ "{{SwappeeUrl}}" ], @@ -634,7 +676,7 @@ "api", "private", "user", - "5" + "4" ] } }, @@ -713,6 +755,202 @@ "response": [] } ] + }, + { + "name": "RequestPrivateApiController", + "item": [ + { + "name": "getRequestsToUser", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{SwappeeUrl}}/api/private/request/owner", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "private", + "request", + "owner" + ] + } + }, + "response": [] + }, + { + "name": "getRequestsFromUser", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{SwappeeUrl}}/api/private/request/trader", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "private", + "request", + "trader" + ] + } + }, + "response": [] + }, + { + "name": "createRequest", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"10\",\r\n \"ownerId\": \"3\",\r\n \"traderId\": \"4\",\r\n \"ownerItemId\": \"6\",\r\n \"traderItemId\": \"5\",\r\n \"status\": \"PENDING\",\r\n \"remarks\": \"cool shoe\",\r\n \"ownerReviewed\": \"false\",\r\n \"traderReviewed\": \"false\",\r\n \"ownerHidden\": \"false\",\r\n \"traderHidden\": \"false\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SwappeeUrl}}/api/private/request", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "private", + "request" + ] + } + }, + "response": [] + }, + { + "name": "updateRequest", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"5\",\r\n \"ownerId\": \"3\",\r\n \"traderId\": \"4\",\r\n \"ownerItemId\": \"6\",\r\n \"traderItemId\": \"5\",\r\n \"status\": \"PENDING\",\r\n \"remarks\": \"awesome shoe\",\r\n \"ownerReviewed\": \"false\",\r\n \"traderReviewed\": \"false\",\r\n \"ownerHidden\": \"false\",\r\n \"traderHidden\": \"false\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SwappeeUrl}}/api/private/request/5", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "private", + "request", + "5" + ] + } + }, + "response": [] + }, + { + "name": "updateRequestStatus", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SwappeeUrl}}/api/private/request/5/ACCEPTED", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "private", + "request", + "5", + "ACCEPTED" + ] + } + }, + "response": [] + }, + { + "name": "deleteRequest", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"1\",\r\n \"ownerId\": \"3\",\r\n \"traderId\": \"4\",\r\n \"ownerItemId\": \"6\",\r\n \"traderItemId\": \"5\",\r\n \"status\": \"PENDING\",\r\n \"remarks\": \"nice shoe\",\r\n \"ownerReviewed\": \"false\",\r\n \"traderReviewed\": \"false\",\r\n \"ownerHidden\": \"false\",\r\n \"traderHidden\": \"false\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SwappeeUrl}}/api/private/request/5", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "private", + "request", + "5" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Mail", + "item": [ + { + "name": "createEmailRequest", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "to", + "value": "taufiq.b.ar@gmail.com", + "type": "text" + } + ] + }, + "url": { + "raw": "{{SwappeeUrl}}/api/public/mail/reset-password", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "public", + "mail", + "reset-password" + ] + } + }, + "response": [] + } + ] } ], "auth": { diff --git a/backend/src/main/java/com/swappee/controller/privateapi/user/UserPrivateApiController.java b/backend/src/main/java/com/swappee/controller/privateapi/user/UserPrivateApiController.java index f81ef54..631d0da 100644 --- a/backend/src/main/java/com/swappee/controller/privateapi/user/UserPrivateApiController.java +++ b/backend/src/main/java/com/swappee/controller/privateapi/user/UserPrivateApiController.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; -import com.swappee.model.item.ItemDTO; import com.swappee.model.user.UserDTO; import com.swappee.model.wrapper.ContentResult; import com.swappee.service.user.UserService; diff --git a/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java b/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java new file mode 100644 index 0000000..157a904 --- /dev/null +++ b/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java @@ -0,0 +1,56 @@ +package com.swappee.controller.publicapi.mail; + +import com.google.common.base.Preconditions; +import com.swappee.controller.publicapi.item.ItemPublicApiController; +import com.swappee.model.wrapper.ContentResult; +import com.swappee.utils.exception.UserFriendlyMessage; +import com.swappee.service.mail.EmailService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.mail.SendFailedException; + +/** + * Public REST controller for sending emails. + * + */ + +@RestController +@RequestMapping("/api/public/mail") +public class MailPublicApiController { + + public static String DEFAULT_SUBJECT = "Hello from Swappee!"; + public static String DEFAULT_TEXT = "Welcome to Swappee!"; + + private static final Logger logger = LoggerFactory.getLogger(ItemPublicApiController.class); + @Autowired + EmailService emailService; + + @PostMapping("/reset-password") + public ResponseEntity createEmailRequest(@RequestParam("to") String to) { + logger.info("Start createEmailRequest - to: {}", to); + ContentResult contentResult = new ContentResult(); + contentResult.setIsSuccess(true); + contentResult.setMessage(UserFriendlyMessage.EMAIL_SEND_SUCCEED); + HttpStatus httpStatus = HttpStatus.OK; + try { + Preconditions.checkNotNull(to); + emailService.sendEmail(to, DEFAULT_SUBJECT, DEFAULT_TEXT); + } catch (SendFailedException sfe) { + logger.error("Error in sendEmail():", sfe); + contentResult.setIsSuccess(false); + contentResult.setMessage(UserFriendlyMessage.EMAIL_SEND_FAILED); + httpStatus = HttpStatus.FAILED_DEPENDENCY; + } + logger.info("End createEmailRequest"); + return new ResponseEntity<>(contentResult, httpStatus); + } + +} diff --git a/backend/src/main/java/com/swappee/dao/item/ItemDao.java b/backend/src/main/java/com/swappee/dao/like/item/ItemDao.java similarity index 96% rename from backend/src/main/java/com/swappee/dao/item/ItemDao.java rename to backend/src/main/java/com/swappee/dao/like/item/ItemDao.java index 6db5c65..f049429 100644 --- a/backend/src/main/java/com/swappee/dao/item/ItemDao.java +++ b/backend/src/main/java/com/swappee/dao/like/item/ItemDao.java @@ -1,4 +1,4 @@ -package com.swappee.dao.item; +package com.swappee.dao.like.item; import com.swappee.domain.item.Item; import com.swappee.utils.exception.BaseDaoException; diff --git a/backend/src/main/java/com/swappee/dao/item/ItemDaoImpl.java b/backend/src/main/java/com/swappee/dao/like/item/ItemDaoImpl.java similarity index 99% rename from backend/src/main/java/com/swappee/dao/item/ItemDaoImpl.java rename to backend/src/main/java/com/swappee/dao/like/item/ItemDaoImpl.java index a1552c2..5263da4 100644 --- a/backend/src/main/java/com/swappee/dao/item/ItemDaoImpl.java +++ b/backend/src/main/java/com/swappee/dao/like/item/ItemDaoImpl.java @@ -1,4 +1,4 @@ -package com.swappee.dao.item; +package com.swappee.dao.like.item; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; diff --git a/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java b/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java index f43ace7..0a787c5 100644 --- a/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java +++ b/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java @@ -1,7 +1,7 @@ package com.swappee.service.item; import com.google.common.base.Preconditions; -import com.swappee.dao.item.ItemDao; +import com.swappee.dao.like.item.ItemDao; import com.swappee.dao.like.LikeDao; import com.swappee.dao.picture.PictureDao; import com.swappee.dao.request.RequestDao; diff --git a/backend/src/main/java/com/swappee/service/mail/EmailService.java b/backend/src/main/java/com/swappee/service/mail/EmailService.java new file mode 100644 index 0000000..3584579 --- /dev/null +++ b/backend/src/main/java/com/swappee/service/mail/EmailService.java @@ -0,0 +1,8 @@ +package com.swappee.service.mail; + + +import javax.mail.SendFailedException; + +public interface EmailService { + void sendEmail(String to, String subject, String text) throws SendFailedException; +} diff --git a/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java b/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java new file mode 100644 index 0000000..04ae590 --- /dev/null +++ b/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java @@ -0,0 +1,27 @@ +package com.swappee.service.mail; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import javax.mail.SendFailedException; + + +@Service +public class EmailServiceImpl implements EmailService { + @Autowired + private JavaMailSender emailSender; + + + @Override + public void sendEmail(String to, String subject, String text) throws SendFailedException { + SimpleMailMessage message = new SimpleMailMessage(); + + message.setFrom("hello@swappee.org"); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + emailSender.send(message); + } +} diff --git a/backend/src/main/java/com/swappee/utils/configuration/JpaAuditingConfig.java b/backend/src/main/java/com/swappee/utils/configuration/JpaAuditingConfig.java index c7892c6..dc23d2c 100644 --- a/backend/src/main/java/com/swappee/utils/configuration/JpaAuditingConfig.java +++ b/backend/src/main/java/com/swappee/utils/configuration/JpaAuditingConfig.java @@ -25,7 +25,7 @@ public static class SecurityAuditor implements AuditorAware { @Override public Optional getCurrentAuditor() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - return Optional.of(auth != null ? auth.getName() : "UknownUser"); + return Optional.of(auth != null ? auth.getName() : "UnknownUser"); } } } \ No newline at end of file diff --git a/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java b/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java index 7e945a9..df7f1f4 100644 --- a/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java +++ b/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java @@ -76,4 +76,9 @@ public class UserFriendlyMessage { //Authentication Error Message + + // Mail Message + public static final String EMAIL_SEND_SUCCEED = "Successfully sent email"; + public static final String EMAIL_SEND_FAILED = "Error in sending email"; + } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 6594efa..4792384 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -18,6 +18,17 @@ spring: jackson: date-format: com.fasterxml.jackson.databind.util.StdDateFormat default-property-inclusion: non-null + mail: + host: smtp.gmail.com + port: 587 + username: ${SWAPPEE_EMAIL_FROM} + password: ${SWAPPEE_EMAIL_APP_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true server: servlet: @@ -46,4 +57,5 @@ logging: org.springframework.security: INFO org.springframework.cloud.openfeign: INFO org.hibernate: INFO - com.swappee: DEBUG \ No newline at end of file + com.swappee: DEBUG + diff --git a/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java b/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java index 96b3b01..dc444e8 100644 --- a/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java +++ b/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java @@ -1,5 +1,6 @@ package com.swappee.dao.item; +import com.swappee.dao.like.item.ItemDaoImpl; import com.swappee.domain.item.Item; import com.swappee.repository.item.ItemRepository; import com.swappee.utils.exception.BaseDaoException; @@ -13,7 +14,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; @RunWith(MockitoJUnitRunner.class) @SpringBootTest From 04d2197de30fa09627805a89cc7af8830b72e750 Mon Sep 17 00:00:00 2001 From: tau-bar Date: Sat, 28 May 2022 21:32:37 +0800 Subject: [PATCH 2/4] restore original state of dao directories --- .../src/main/java/com/swappee/dao/{like => }/item/ItemDao.java | 2 +- .../main/java/com/swappee/dao/{like => }/item/ItemDaoImpl.java | 2 +- .../src/main/java/com/swappee/service/item/ItemServiceImpl.java | 2 +- backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) rename backend/src/main/java/com/swappee/dao/{like => }/item/ItemDao.java (96%) rename backend/src/main/java/com/swappee/dao/{like => }/item/ItemDaoImpl.java (99%) diff --git a/backend/src/main/java/com/swappee/dao/like/item/ItemDao.java b/backend/src/main/java/com/swappee/dao/item/ItemDao.java similarity index 96% rename from backend/src/main/java/com/swappee/dao/like/item/ItemDao.java rename to backend/src/main/java/com/swappee/dao/item/ItemDao.java index f049429..6db5c65 100644 --- a/backend/src/main/java/com/swappee/dao/like/item/ItemDao.java +++ b/backend/src/main/java/com/swappee/dao/item/ItemDao.java @@ -1,4 +1,4 @@ -package com.swappee.dao.like.item; +package com.swappee.dao.item; import com.swappee.domain.item.Item; import com.swappee.utils.exception.BaseDaoException; diff --git a/backend/src/main/java/com/swappee/dao/like/item/ItemDaoImpl.java b/backend/src/main/java/com/swappee/dao/item/ItemDaoImpl.java similarity index 99% rename from backend/src/main/java/com/swappee/dao/like/item/ItemDaoImpl.java rename to backend/src/main/java/com/swappee/dao/item/ItemDaoImpl.java index 5263da4..a1552c2 100644 --- a/backend/src/main/java/com/swappee/dao/like/item/ItemDaoImpl.java +++ b/backend/src/main/java/com/swappee/dao/item/ItemDaoImpl.java @@ -1,4 +1,4 @@ -package com.swappee.dao.like.item; +package com.swappee.dao.item; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; diff --git a/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java b/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java index 0a787c5..f43ace7 100644 --- a/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java +++ b/backend/src/main/java/com/swappee/service/item/ItemServiceImpl.java @@ -1,7 +1,7 @@ package com.swappee.service.item; import com.google.common.base.Preconditions; -import com.swappee.dao.like.item.ItemDao; +import com.swappee.dao.item.ItemDao; import com.swappee.dao.like.LikeDao; import com.swappee.dao.picture.PictureDao; import com.swappee.dao.request.RequestDao; diff --git a/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java b/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java index dc444e8..9eb3648 100644 --- a/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java +++ b/backend/src/test/java/com/swappee/dao/item/ItemDaoImplTest.java @@ -1,6 +1,5 @@ package com.swappee.dao.item; -import com.swappee.dao.like.item.ItemDaoImpl; import com.swappee.domain.item.Item; import com.swappee.repository.item.ItemRepository; import com.swappee.utils.exception.BaseDaoException; From b18ba41b429d8cc2fd6b79e3c7ee4f357fee4db9 Mon Sep 17 00:00:00 2001 From: tau-bar Date: Mon, 13 Jun 2022 21:44:27 +0800 Subject: [PATCH 3/4] add endpoints for resetting password --- .../mail/MailPublicApiController.java | 2 +- .../securityapi/PasswordController.java | 99 +++++++++++++++++++ .../java/com/swappee/dao/user/UserDao.java | 6 ++ .../com/swappee/dao/user/UserDaoImpl.java | 46 +++++++++ .../java/com/swappee/domain/user/User.java | 13 +++ .../swappee/mapper/user/UserDTOMapper.java | 9 +- .../java/com/swappee/model/user/UserDTO.java | 11 +++ .../repository/user/UserRepository.java | 2 + .../swappee/service/mail/EmailService.java | 3 + .../service/mail/EmailServiceImpl.java | 5 + .../com/swappee/service/user/UserService.java | 4 + .../swappee/service/user/UserServiceImpl.java | 46 ++++++++- .../utils/exception/UserFriendlyMessage.java | 3 + 13 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/com/swappee/controller/securityapi/PasswordController.java diff --git a/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java b/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java index 157a904..80233ec 100644 --- a/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java +++ b/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java @@ -33,7 +33,7 @@ public class MailPublicApiController { @Autowired EmailService emailService; - @PostMapping("/reset-password") + @PostMapping("/send") public ResponseEntity createEmailRequest(@RequestParam("to") String to) { logger.info("Start createEmailRequest - to: {}", to); ContentResult contentResult = new ContentResult(); diff --git a/backend/src/main/java/com/swappee/controller/securityapi/PasswordController.java b/backend/src/main/java/com/swappee/controller/securityapi/PasswordController.java new file mode 100644 index 0000000..7ed4fca --- /dev/null +++ b/backend/src/main/java/com/swappee/controller/securityapi/PasswordController.java @@ -0,0 +1,99 @@ +package com.swappee.controller.securityapi; + +import com.google.common.base.Preconditions; +import com.swappee.controller.publicapi.item.ItemPublicApiController; +import com.swappee.model.user.UserDTO; +import com.swappee.model.wrapper.ContentResult; +import com.swappee.service.mail.EmailService; +import com.swappee.service.user.UserService; +import com.swappee.utils.exception.BaseServiceException; +import com.swappee.utils.exception.UserFriendlyMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.*; + + +import javax.mail.SendFailedException; +import java.util.UUID; + +@RestController +@RequestMapping("/api/public/password") +public class PasswordController { + @Autowired + private UserService userService; + + @Autowired + private EmailService emailService; + + @Autowired + private BCryptPasswordEncoder bCryptPasswordEncoder; + + private static final Logger logger = LoggerFactory.getLogger(ItemPublicApiController.class); + + @Value("${frontend.baseurl}") + private String baseUrl; + + @PostMapping("/reset") + public ResponseEntity resetPasswordRequest(@RequestParam("email") String emailAddress) { + logger.info("Start resetPasswordRequest - from: {}", emailAddress); + ContentResult contentResult = new ContentResult(); + contentResult.setIsSuccess(true); + contentResult.setMessage(UserFriendlyMessage.EMAIL_SEND_SUCCEED); + HttpStatus httpStatus = HttpStatus.OK; + try { + Preconditions.checkNotNull(emailAddress); + UserDTO user = userService.findByEmail(emailAddress); + user.setResetToken(UUID.randomUUID().toString()); + userService.update(user); + UserDTO userAfterSetToken = userService.findByEmail(emailAddress); + SimpleMailMessage passwordResetEmail = new SimpleMailMessage(); + passwordResetEmail.setFrom("hello@swappee.org"); + passwordResetEmail.setTo(userAfterSetToken.getEmail()); + + passwordResetEmail.setSubject("Swappee: Password Reset"); + String url = baseUrl + "/reset_password?token=" + userAfterSetToken.getResetToken(); + passwordResetEmail.setText("To reset your password, click the link below:\n" + url); + emailService.sendEmail(passwordResetEmail); + } catch (BaseServiceException bse) { + logger.error("Error in findByEmail():", bse); + contentResult.setIsSuccess(false); + contentResult.setMessage(UserFriendlyMessage.USER_GET_ONE_FAILED); + httpStatus = HttpStatus.NOT_FOUND; + } catch (SendFailedException sfe) { + logger.error("Error in sendEmail():", sfe); + contentResult.setIsSuccess(false); + contentResult.setMessage(UserFriendlyMessage.EMAIL_SEND_FAILED); + httpStatus = HttpStatus.FAILED_DEPENDENCY; + } + return new ResponseEntity<>(contentResult, httpStatus); + } + + @PostMapping("/reset_password") + public ResponseEntity resetPassword(@RequestParam("token") String token, @RequestParam("password") String newPassword) { + logger.info("Start resetPassword - resetToken: {}", token); + ContentResult contentResult = new ContentResult(); + contentResult.setIsSuccess(true); + contentResult.setMessage(UserFriendlyMessage.PASSWORD_RESET_SUCCEED); + HttpStatus httpStatus = HttpStatus.OK; + try { + Preconditions.checkNotNull(token); + Preconditions.checkNotNull(newPassword); + UserDTO user = userService.findByResetToken(token); + user.setPassword(newPassword); + user.setResetToken(null); + userService.update(user); + } catch (BaseServiceException bse) { + logger.error("Error in findByResetToken():", bse); + contentResult.setIsSuccess(false); + contentResult.setMessage(UserFriendlyMessage.USER_GET_ONE_FAILED); + httpStatus = HttpStatus.NOT_FOUND; + } + return new ResponseEntity<>(contentResult, httpStatus); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/swappee/dao/user/UserDao.java b/backend/src/main/java/com/swappee/dao/user/UserDao.java index ce82e41..161d7e0 100644 --- a/backend/src/main/java/com/swappee/dao/user/UserDao.java +++ b/backend/src/main/java/com/swappee/dao/user/UserDao.java @@ -24,4 +24,10 @@ public interface UserDao { User update(User toUpdate) throws BaseDaoException; User delete(User toDelete) throws BaseDaoException; + + User findUserByResetToken(String resetToken) throws BaseDaoException; + + User findByEmail(String email) throws BaseDaoException; + + User findByResetToken(String token) throws BaseDaoException; } diff --git a/backend/src/main/java/com/swappee/dao/user/UserDaoImpl.java b/backend/src/main/java/com/swappee/dao/user/UserDaoImpl.java index b260ccb..7465b1d 100644 --- a/backend/src/main/java/com/swappee/dao/user/UserDaoImpl.java +++ b/backend/src/main/java/com/swappee/dao/user/UserDaoImpl.java @@ -131,6 +131,7 @@ public User update(User toUpdate) throws BaseDaoException { originalEntity.setRole(toUpdate.getRole()); originalEntity.setScore(toUpdate.getScore()); originalEntity.setTotalTraded(toUpdate.getTotalTraded()); + originalEntity.setResetToken(toUpdate.getResetToken()); return this.userRepository.save(originalEntity); } catch (DataAccessException dae) { @@ -162,4 +163,49 @@ public User delete(User toDelete) throws BaseDaoException { logger.info("End delete"); } } + + @Override + public User findUserByResetToken(String resetToken) throws BaseDaoException { + logger.info("Start findUserByResetToken - resetToken: {}", resetToken); + try { + Preconditions.checkNotNull(resetToken); + return userRepository.findByResetToken(resetToken).orElse(null); + } catch (DataAccessException dae) { + throw new BaseDaoException(ErrorCode.DB_ERROR_GET_ONE_FAILED, dae); + } catch (Exception ex) { + throw new BaseDaoException(ErrorCode.DB_ERROR_GENERIC, ex); + } finally { + logger.info("End findUserByResetToken"); + } + } + + @Override + public User findByEmail(String email) throws BaseDaoException { + logger.info("Start findByEmail - email: {}", email); + try { + Preconditions.checkNotNull(email); + return userRepository.findByEmail(email).orElse(null); + } catch (DataAccessException dae) { + throw new BaseDaoException(ErrorCode.DB_ERROR_GET_ONE_FAILED, dae); + } catch (Exception ex) { + throw new BaseDaoException(ErrorCode.DB_ERROR_GENERIC, ex); + } finally { + logger.info("End findByEmail"); + } + } + + @Override + public User findByResetToken(String token) throws BaseDaoException { + logger.info("Start findByResetToken - token: {}", token); + try { + Preconditions.checkNotNull(token); + return userRepository.findByResetToken(token).orElse(null); + } catch (DataAccessException dae) { + throw new BaseDaoException(ErrorCode.DB_ERROR_GET_ONE_FAILED, dae); + } catch (Exception ex) { + throw new BaseDaoException(ErrorCode.DB_ERROR_GENERIC, ex); + } finally { + logger.info("End findByResetToken"); + } + } } diff --git a/backend/src/main/java/com/swappee/domain/user/User.java b/backend/src/main/java/com/swappee/domain/user/User.java index 4222afb..d062c7c 100644 --- a/backend/src/main/java/com/swappee/domain/user/User.java +++ b/backend/src/main/java/com/swappee/domain/user/User.java @@ -73,6 +73,9 @@ public class User implements Serializable { @Column(name = "deleted", nullable = false) private boolean deleted; + @Column(name = "reset_token", length = 200, nullable = true) + private String resetToken; + public Long getId() { return id; } @@ -201,6 +204,15 @@ public void setDeleted(boolean deleted) { this.deleted = deleted; } + public String getResetToken() { + return resetToken; + } + + public void setResetToken(String resetToken) { + this.resetToken = resetToken; + } + + @Override public String toString() { return "User{" + @@ -218,6 +230,7 @@ public String toString() { ", lastModifiedDate=" + lastModifiedDate + ", version=" + version + ", deleted=" + deleted + + ", resetToken=" + resetToken + '}'; } diff --git a/backend/src/main/java/com/swappee/mapper/user/UserDTOMapper.java b/backend/src/main/java/com/swappee/mapper/user/UserDTOMapper.java index 128cf5a..cb02d6a 100644 --- a/backend/src/main/java/com/swappee/mapper/user/UserDTOMapper.java +++ b/backend/src/main/java/com/swappee/mapper/user/UserDTOMapper.java @@ -3,13 +3,18 @@ import com.swappee.domain.user.User; import com.swappee.mapper.DTOMapper; import com.swappee.model.user.UserDTO; +import com.swappee.service.user.UserServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class UserDTOMapper implements DTOMapper { + private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); @Override public UserDTO mapEntity(User entity) { + logger.info("Start mapping - mapEntity: {}", entity); if (entity == null) { return null; } @@ -33,7 +38,8 @@ public UserDTO mapEntity(User entity) { dto.setRole(entity.getRole().toString()); dto.setScore(entity.getScore()); dto.setTotalTraded(entity.getTotalTraded()); - + dto.setResetToken(entity.getResetToken()); + logger.info("End mapping - mapEntity: {}", entity); return dto; } @@ -61,6 +67,7 @@ public User mapDto(UserDTO dto) { entity.setRole(User.Role.valueOf(dto.getRole())); entity.setScore(dto.getScore()); entity.setTotalTraded(dto.getTotalTraded()); + entity.setResetToken((dto.getResetToken())); return entity; } diff --git a/backend/src/main/java/com/swappee/model/user/UserDTO.java b/backend/src/main/java/com/swappee/model/user/UserDTO.java index 5cc55c7..ba69719 100644 --- a/backend/src/main/java/com/swappee/model/user/UserDTO.java +++ b/backend/src/main/java/com/swappee/model/user/UserDTO.java @@ -46,6 +46,8 @@ public class UserDTO implements UserDetails { private Long totalTraded; + private String resetToken; + @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MMM-yyyy HH:mm:ss") @@ -221,6 +223,14 @@ public boolean isEnabled() { return true; } + public String getResetToken() { + return resetToken; + } + + public void setResetToken(String resetToken) { + this.resetToken = resetToken; + } + @Override public String toString() { return "UserDTO{" + @@ -236,6 +246,7 @@ public String toString() { ", totalTraded=" + totalTraded + ", createdDate=" + createdDate + ", lastModifiedDate=" + lastModifiedDate + + ", resetToken=" + resetToken + '}'; } } diff --git a/backend/src/main/java/com/swappee/repository/user/UserRepository.java b/backend/src/main/java/com/swappee/repository/user/UserRepository.java index a027c7a..250496f 100644 --- a/backend/src/main/java/com/swappee/repository/user/UserRepository.java +++ b/backend/src/main/java/com/swappee/repository/user/UserRepository.java @@ -12,4 +12,6 @@ @Repository public interface UserRepository extends JpaRepository { Optional findByUsername(String username); + Optional findByResetToken(String resetToken); + Optional findByEmail(String email); } diff --git a/backend/src/main/java/com/swappee/service/mail/EmailService.java b/backend/src/main/java/com/swappee/service/mail/EmailService.java index 3584579..7f8c4a5 100644 --- a/backend/src/main/java/com/swappee/service/mail/EmailService.java +++ b/backend/src/main/java/com/swappee/service/mail/EmailService.java @@ -1,8 +1,11 @@ package com.swappee.service.mail; +import org.springframework.mail.SimpleMailMessage; + import javax.mail.SendFailedException; public interface EmailService { void sendEmail(String to, String subject, String text) throws SendFailedException; + void sendEmail(SimpleMailMessage email) throws SendFailedException; } diff --git a/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java b/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java index 04ae590..58cade3 100644 --- a/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java +++ b/backend/src/main/java/com/swappee/service/mail/EmailServiceImpl.java @@ -24,4 +24,9 @@ public void sendEmail(String to, String subject, String text) throws SendFailedE message.setText(text); emailSender.send(message); } + + @Override + public void sendEmail(SimpleMailMessage email) throws SendFailedException { + emailSender.send(email); + } } diff --git a/backend/src/main/java/com/swappee/service/user/UserService.java b/backend/src/main/java/com/swappee/service/user/UserService.java index cf87973..162a3ab 100644 --- a/backend/src/main/java/com/swappee/service/user/UserService.java +++ b/backend/src/main/java/com/swappee/service/user/UserService.java @@ -32,4 +32,8 @@ public interface UserService { UserDTO update(UserDTO toUpdate) throws BaseServiceException; UserDTO delete(UserDTO toDelete) throws BaseServiceException; + + UserDTO findByEmail(String emailAddress) throws BaseServiceException; + + UserDTO findByResetToken(String token) throws BaseServiceException; } diff --git a/backend/src/main/java/com/swappee/service/user/UserServiceImpl.java b/backend/src/main/java/com/swappee/service/user/UserServiceImpl.java index 08c4b79..f347a7a 100644 --- a/backend/src/main/java/com/swappee/service/user/UserServiceImpl.java +++ b/backend/src/main/java/com/swappee/service/user/UserServiceImpl.java @@ -292,7 +292,7 @@ public Boolean reviewUser(ReviewDTO reviewDTO) throws BaseServiceException { @Transactional(rollbackFor = {BaseServiceException.class}) public UserDTO update(UserDTO toUpdate) throws BaseServiceException { try { - logger.info("Start update - toUpdate: {}", toUpdate); + logger.info("Start update service- toUpdate: {}", toUpdate); Preconditions.checkNotNull(toUpdate); //check if a new password was set, and encode it if(!toUpdate.getPassword().isEmpty()) { @@ -306,7 +306,7 @@ public UserDTO update(UserDTO toUpdate) throws BaseServiceException { } catch (Exception ex) { throw new BaseServiceException(ErrorMessage.SVC_ERROR_GENERIC, ex); } finally { - logger.info("End update"); + logger.info("End update service"); } } @@ -342,6 +342,48 @@ public UserDTO delete(UserDTO toDelete) throws BaseServiceException { } } + /** + * Find user by email address + * For password reset + * + * @param emailAddress + * @return UserDTO + * @throws BaseServiceException + */ + @Override + public UserDTO findByEmail(String emailAddress) throws BaseServiceException { + try { + logger.info("Start findByEmail - email: {}", emailAddress); + Preconditions.checkNotNull(emailAddress); + UserDTO userDTO = userDTOMapper.mapEntity(userDao.findByEmail(emailAddress)); + Preconditions.checkNotNull(userDTO); + return userDTO; + } catch (BaseDaoException bde) { + throw new BaseServiceException(ErrorMessage.USER_ERROR_GET_ONE_FAILED, bde); + } catch (Exception ex) { + throw new BaseServiceException(ErrorMessage.SVC_ERROR_GENERIC, ex); + } finally { + logger.info("End findByEmail"); + } + } + + @Override + public UserDTO findByResetToken(String token) throws BaseServiceException { + try { + logger.info("Start findByResetToken - token: {}", token); + Preconditions.checkNotNull(token); + UserDTO userDTO = userDTOMapper.mapEntity(userDao.findByResetToken(token)); + Preconditions.checkNotNull(userDTO); + return userDTO; + } catch (BaseDaoException bde) { + throw new BaseServiceException(ErrorMessage.USER_ERROR_GET_ONE_FAILED, bde); + } catch (Exception ex) { + throw new BaseServiceException(ErrorMessage.SVC_ERROR_GENERIC, ex); + } finally { + logger.info("End findByResetToken"); + } + } + //mapper to map User to UserViewDTO private UserViewDTO userViewDTOMapper(User user) { Preconditions.checkNotNull(user); diff --git a/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java b/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java index df7f1f4..1c385b9 100644 --- a/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java +++ b/backend/src/main/java/com/swappee/utils/exception/UserFriendlyMessage.java @@ -81,4 +81,7 @@ public class UserFriendlyMessage { public static final String EMAIL_SEND_SUCCEED = "Successfully sent email"; public static final String EMAIL_SEND_FAILED = "Error in sending email"; + // Password reset + public static final String PASSWORD_RESET_SUCCEED = "Successfully reset password"; + public static final String PASSWORD_RESET_FAILED = "Error in reset password"; } From 38ce99e6d3936597112f6a47b1397edcde13f127 Mon Sep 17 00:00:00 2001 From: tau-bar Date: Mon, 13 Jun 2022 21:48:42 +0800 Subject: [PATCH 4/4] update postman collection for password reset feature --- backend/setup/Swappee.postman_collection.json | 74 ++++++++++++++++++- .../mail/MailPublicApiController.java | 2 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/backend/setup/Swappee.postman_collection.json b/backend/setup/Swappee.postman_collection.json index c8eefb8..d1c1c8e 100644 --- a/backend/setup/Swappee.postman_collection.json +++ b/backend/setup/Swappee.postman_collection.json @@ -416,13 +416,13 @@ "formdata": [ { "key": "userDTO", - "value": "{\n\t\"id\": \"\",\n\t\"firstName\": \"New Guy\",\n\t\"lastName\": \"Tang\",\n\t\"username\": \"NewGuy\",\n\t\"password\": \"test\",\n\t\"email\": \"Guy@gmail\",\n\t\"avatar\": null,\n\t\"phone\": 98787397,\n\t\"emailOnly\": false,\n\t\"score\": 0,\n\t\"totalTraded\": 0\n}", + "value": "{\n\t\"id\": \"\",\n\t\"firstName\": \"Taufiq\",\n\t\"lastName\": \"Abdul Rahman\",\n\t\"username\": \"taubar\",\n\t\"password\": \"password\",\n\t\"email\": \"taufiq.b.ar@gmail.com\",\n\t\"avatar\": null,\n\t\"phone\": 91919191,\n\t\"emailOnly\": false,\n\t\"score\": 0,\n\t\"totalTraded\": 0\n\n}", "type": "text" }, { "key": "avatar", "type": "file", - "src": "/C:/Users/Marcus Tang/Pictures/ProfilePics/Charmander.jpg" + "src": "/C:/Users/User/Pictures/index.jpg" } ] }, @@ -951,6 +951,76 @@ "response": [] } ] + }, + { + "name": "Password Reset", + "item": [ + { + "name": "sendPasswordResetRequest", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "email", + "value": "tau.bar.coding@gmail.com", + "type": "text" + } + ] + }, + "url": { + "raw": "{{SwappeeUrl}}/api/public/password/reset", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "public", + "password", + "reset" + ] + } + }, + "response": [] + }, + { + "name": "resetPassword", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "password", + "value": "apple", + "type": "text" + }, + { + "key": "token", + "value": "30fb03e8-23c7-4fac-915c-796d390daca9", + "type": "text" + } + ] + }, + "url": { + "raw": "{{SwappeeUrl}}/api/public/password/reset_password", + "host": [ + "{{SwappeeUrl}}" + ], + "path": [ + "api", + "public", + "password", + "reset_password" + ] + } + }, + "response": [] + } + ] } ], "auth": { diff --git a/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java b/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java index 80233ec..6773c38 100644 --- a/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java +++ b/backend/src/main/java/com/swappee/controller/publicapi/mail/MailPublicApiController.java @@ -18,7 +18,7 @@ import javax.mail.SendFailedException; /** - * Public REST controller for sending emails. + * Public REST controller for sending emails. Kept here as an example. * */