From dca7784bdf7b49960937c667e7909622bbc7fb5d Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 2 May 2022 10:47:29 +0200 Subject: [PATCH 001/425] feat: entries of inactive editions can only be viewed and deleted --- .../backend/controllers/ControllersUtil.kt | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index 797ea2966..cc29d2430 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -1,9 +1,22 @@ package be.osoc.team1.backend.controllers +import be.osoc.team1.backend.exceptions.InvalidIdException +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService +import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component +import org.springframework.web.servlet.HandlerInterceptor +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter import org.springframework.web.servlet.support.ServletUriComponentsBuilder +import java.security.Principal +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse /** * Utility method that takes an object that was just created by a service method and it's [id], @@ -27,3 +40,57 @@ fun getObjectCreatedResponse(id: ID, createdObject: T, status: HttpStatu .header(HttpHeaders.LOCATION, pathWithIdAdded) .body(createdObject) } + +@Component +class TestingInterceptor(val editionService: EditionService, val userDetailService: OsocUserDetailService) : HandlerInterceptor { + @Throws(Exception::class) + @Override + override fun preHandle( + request: HttpServletRequest, + response: HttpServletResponse, + handler: Any + ): Boolean { + if (request.requestURI != "/api/error") { + println(request.requestURI) + println("ese") + println(handler) + response.addHeader("WIE", "WOO") + response.sendError(411, "test") + println(editionService.getActiveEdition()) + return false + } + return super.preHandle(request, response, handler) + + // set few parameters to handle ajax request from different host + response.addHeader("Access-Control-Allow-Origin", "*") + response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + response.addHeader("Access-Control-Max-Age", "1000") + response.addHeader("Access-Control-Allow-Headers", "Content-Type") + response.addHeader("Cache-Control", "private") + val reqUri = request.requestURI + val serviceName = reqUri.substring( + reqUri.lastIndexOf("/") + 1, + reqUri.length + ) + if (serviceName == "SOMETHING") { + } + } + + @Throws(Exception::class) + override fun postHandle( + request: HttpServletRequest, + response: HttpServletResponse, handler: Any, + modelAndView: ModelAndView? + ) { + super.postHandle(request, response, handler, modelAndView) + } +} + +@Component +class ProductServiceInterceptorAppConfig : WebMvcConfigurer { + @Autowired + lateinit var testServiceInterceptor: TestingInterceptor + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(testServiceInterceptor) + } +} \ No newline at end of file From eaa93312a4cfac9fa563f896808201d9917effb8 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Tue, 3 May 2022 11:20:18 +0200 Subject: [PATCH 002/425] chore: forgot to commit this --- .../backend/controllers/ControllersUtil.kt | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index cc29d2430..ac24969bf 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -1,10 +1,10 @@ package be.osoc.team1.backend.controllers -import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Component @@ -12,9 +12,8 @@ import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.ModelAndView import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler import org.springframework.web.servlet.support.ServletUriComponentsBuilder -import java.security.Principal import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -32,7 +31,11 @@ import javax.servlet.http.HttpServletResponse * * `/api/students/(INSERT ID)` */ -fun getObjectCreatedResponse(id: ID, createdObject: T, status: HttpStatus = HttpStatus.CREATED): ResponseEntity { +fun getObjectCreatedResponse( + id: ID, + createdObject: T, + status: HttpStatus = HttpStatus.CREATED +): ResponseEntity { val postRequestPath = ServletUriComponentsBuilder.fromCurrentRequest() val pathWithIdAdded = postRequestPath.path("/{id}").buildAndExpand(id).toUriString() return ResponseEntity @@ -42,7 +45,8 @@ fun getObjectCreatedResponse(id: ID, createdObject: T, status: HttpStatu } @Component -class TestingInterceptor(val editionService: EditionService, val userDetailService: OsocUserDetailService) : HandlerInterceptor { +class TestingInterceptor(val editionService: EditionService, val userDetailService: OsocUserDetailService) : + HandlerInterceptor { @Throws(Exception::class) @Override override fun preHandle( @@ -50,30 +54,21 @@ class TestingInterceptor(val editionService: EditionService, val userDetailServi response: HttpServletResponse, handler: Any ): Boolean { - if (request.requestURI != "/api/error") { + // URLs that are always allowed + val regex = + Regex("^.*/api/(error|editions|login|communications|users|assignments|positions|statusSuggestions|answers|skills|logout|token).*$") + // this !is check is here so invalid requests (such as gets to endpoints that don't exist) still get handled regularly + if (!regex.matches(request.requestURI) && handler !is ResourceHttpRequestHandler) { println(request.requestURI) - println("ese") println(handler) - response.addHeader("WIE", "WOO") - response.sendError(411, "test") println(editionService.getActiveEdition()) - return false + val editionName = request.requestURI.split("/")[2] + if (editionService.getActiveEdition()?.name != editionName && request.method != "GET" && request.method != "DELETE") { + response.sendError(405, "Entries from inactive editions can only be viewed or deleted") + return false + } } return super.preHandle(request, response, handler) - - // set few parameters to handle ajax request from different host - response.addHeader("Access-Control-Allow-Origin", "*") - response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS") - response.addHeader("Access-Control-Max-Age", "1000") - response.addHeader("Access-Control-Allow-Headers", "Content-Type") - response.addHeader("Cache-Control", "private") - val reqUri = request.requestURI - val serviceName = reqUri.substring( - reqUri.lastIndexOf("/") + 1, - reqUri.length - ) - if (serviceName == "SOMETHING") { - } } @Throws(Exception::class) From 3da6cce02e5a2cf832381a347db837d647641447 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 3 May 2022 11:24:44 +0200 Subject: [PATCH 003/425] feat: create reset password token --- .../controllers/ResetPasswordController.kt | 14 +++++++++++++ .../backend/controllers/UserController.kt | 6 ++++++ .../osoc/team1/backend/security/TokenUtil.kt | 21 +++++++++++++++++++ .../backend/services/ResetPasswordService.kt | 8 +++++++ .../team1/backend/services/UserService.kt | 11 ++++++++++ 5 files changed, 60 insertions(+) create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt new file mode 100644 index 000000000..4aa3fda68 --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt @@ -0,0 +1,14 @@ +package be.osoc.team1.backend.controllers + +import be.osoc.team1.backend.services.ResetPasswordService +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 + +@RestController +@RequestMapping("/resetpassword") +class ResetPasswordController(private val service: ResetPasswordService) { + + +} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index 6fc221dba..301ee0833 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -78,4 +78,10 @@ class UserController(private val service: UserService) { @Secured("ROLE_ADMIN") fun postUserRole(@PathVariable id: UUID, @RequestBody role: Role) = service.changeRole(id, role) + + /** + * Reset password + */ + @PostMapping("/resetpassword") + fun postEmail(@RequestBody email: String) = service.getTokenByMail(email) } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt index ca3198ec7..4d182942e 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt @@ -37,6 +37,12 @@ object TokenUtil { */ private val validRefreshTokens: MutableMap = mutableMapOf() + /** + * This map holds the latest resetPasswordToken per email, so the keys are emails and the values are + * resetPasswordTokens. + */ + private val validResetPasswordTokens: MutableMap = mutableMapOf() + /** * Create a JSON web token. The token contains email, an id, expiration date of token, whether the token is an * access token and the authorities of the user. Set [isAccessToken] to true when making an access token, set it to @@ -62,6 +68,21 @@ object TokenUtil { .sign(hashingAlgorithm) } + /** + * Create a JSON web token. The token contains email and expiration date of token. resetPasswordTokens are valid for + * 15 minutes. + * The created token gets signed using above hashing algorithm and secret. + */ + fun createResetPasswordToken(email: String): String { + return JWT.create() + .withSubject(email) + .withJWTId(nextInt().toString()) + .withExpiresAt( + Date(System.currentTimeMillis() + 15 * 60 * 1000) + ) + .sign(hashingAlgorithm) + } + /** * Extract access token from request header. return null when there is no access token given. */ diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt new file mode 100644 index 000000000..a2d63eed4 --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt @@ -0,0 +1,8 @@ +package be.osoc.team1.backend.services + +import be.osoc.team1.backend.repositories.UserRepository +import org.springframework.stereotype.Service + +@Service +class ResetPasswordService(private val repository: UserRepository) { +} \ No newline at end of file diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 4091a1a4b..c94f864fd 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -5,6 +5,7 @@ import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.ForbiddenOperationException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.UserRepository +import be.osoc.team1.backend.security.TokenUtil import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull import org.springframework.security.crypto.password.PasswordEncoder @@ -82,4 +83,14 @@ class UserService(private val repository: UserRepository, private val passwordEn return repository.save(updatedUser) } + + /** + * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. + */ + fun getTokenByMail(email: String) { + if (repository.findByEmail(email) != null) { + val resetPasswordToken = TokenUtil.createResetPasswordToken(email) + // send mail to [email] containing resetpasswordtoken + } + } } From 4a83271c206c26a8cbf38903bd85592669838979 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Tue, 3 May 2022 11:57:01 +0200 Subject: [PATCH 004/425] feat: added checking for adminrole --- .../backend/controllers/ControllersUtil.kt | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index ac24969bf..4ec9f5c60 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -4,9 +4,9 @@ import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.ModelAndView @@ -45,7 +45,7 @@ fun getObjectCreatedResponse( } @Component -class TestingInterceptor(val editionService: EditionService, val userDetailService: OsocUserDetailService) : +class EditionInterceptor(val editionService: EditionService) : HandlerInterceptor { @Throws(Exception::class) @Override @@ -57,15 +57,19 @@ class TestingInterceptor(val editionService: EditionService, val userDetailServi // URLs that are always allowed val regex = Regex("^.*/api/(error|editions|login|communications|users|assignments|positions|statusSuggestions|answers|skills|logout|token).*$") - // this !is check is here so invalid requests (such as gets to endpoints that don't exist) still get handled regularly + // this !is check is here so invalid requests (such as GETs to endpoints that don't exist) still get handled regularly if (!regex.matches(request.requestURI) && handler !is ResourceHttpRequestHandler) { - println(request.requestURI) - println(handler) - println(editionService.getActiveEdition()) + val roles = SecurityContextHolder.getContext().authentication.authorities.map { it.toString() } val editionName = request.requestURI.split("/")[2] - if (editionService.getActiveEdition()?.name != editionName && request.method != "GET" && request.method != "DELETE") { - response.sendError(405, "Entries from inactive editions can only be viewed or deleted") - return false + if (editionService.getActiveEdition()?.name != editionName) { + if (!roles.contains("ROLE_ADMIN")) { + response.sendError(401, "Inactive editions can only be accessed by admins") + return false + } + if (request.method != "GET" && request.method != "DELETE") { + response.sendError(405, "Entries from inactive editions can only be viewed or deleted") + return false + } } } return super.preHandle(request, response, handler) @@ -82,10 +86,10 @@ class TestingInterceptor(val editionService: EditionService, val userDetailServi } @Component -class ProductServiceInterceptorAppConfig : WebMvcConfigurer { +class InterceptorConfig : WebMvcConfigurer { @Autowired - lateinit var testServiceInterceptor: TestingInterceptor + lateinit var editionInterceptor: EditionInterceptor override fun addInterceptors(registry: InterceptorRegistry) { - registry.addInterceptor(testServiceInterceptor) + registry.addInterceptor(editionInterceptor) } } \ No newline at end of file From d6bf9a6f14fd8e57164617b1189d514ae982d4cf Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 3 May 2022 12:14:23 +0200 Subject: [PATCH 005/425] feat: add email implementation --- backend/pom.xml | 10 ++++ .../controllers/ResetPasswordController.kt | 14 ------ .../osoc/team1/backend/security/EmailUtil.kt | 46 +++++++++++++++++++ .../osoc/team1/backend/security/TokenUtil.kt | 2 +- .../backend/services/ResetPasswordService.kt | 8 ---- .../team1/backend/services/UserService.kt | 3 +- 6 files changed, 59 insertions(+), 24 deletions(-) delete mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt delete mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt diff --git a/backend/pom.xml b/backend/pom.xml index ae904f27b..86fd2a144 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -113,6 +113,16 @@ hibernate-validator 7.0.4.Final + + org.springframework + spring-context-support + 5.3.19 + + + org.springframework.boot + spring-boot-starter-mail + 2.6.7 + diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt deleted file mode 100644 index 4aa3fda68..000000000 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ResetPasswordController.kt +++ /dev/null @@ -1,14 +0,0 @@ -package be.osoc.team1.backend.controllers - -import be.osoc.team1.backend.services.ResetPasswordService -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 - -@RestController -@RequestMapping("/resetpassword") -class ResetPasswordController(private val service: ResetPasswordService) { - - -} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt new file mode 100644 index 000000000..011d37ce1 --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -0,0 +1,46 @@ +package be.osoc.team1.backend.security + +import org.springframework.mail.SimpleMailMessage +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.JavaMailSenderImpl +import java.util.* + +/** + * This object contains every function needed to create and send emails. + */ +object EmailUtil { + private fun getResetPasswordEmailBody( + contextPath: String, token: String + ): String { + val url = "$contextPath/users/resetpassword?$token" + val message: String = "Hallo, \r\n dit is een testtt.tt. \n mvg\r\ntestymen" + return "$message \r\n$url" + } + + private fun getJavaMailSender(): JavaMailSender { + val mailSender = JavaMailSenderImpl() + mailSender.host = "smtp.gmail.com" + mailSender.port = 587 + mailSender.username = "my.gmail@gmail.com" + mailSender.password = "password" + val props: Properties = mailSender.javaMailProperties + props["mail.transport.protocol"] = "smtp" + props["mail.smtp.auth"] = "true" + props["mail.smtp.starttls.enable"] = "true" + props["mail.debug"] = "true" + return mailSender + } + + + fun sendEmail(emailaddress: String, resetPasswordToken: String) { + val mailSender = getJavaMailSender() + + val email = SimpleMailMessage() + email.setSubject("Reset Password") + email.setText(getResetPasswordEmailBody("/api", resetPasswordToken)) + email.setTo(emailaddress) + email.setFrom("osoc.support@mail.com") + + mailSender.send(email) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt index 4d182942e..14c37c59d 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt @@ -16,7 +16,7 @@ import kotlin.random.Random.Default.nextBytes import kotlin.random.Random.Default.nextInt /** - * This object contains every function needed to create and process a token (works with both access and refresh tokens). + * This object contains every function needed to create and process a token. */ object TokenUtil { /** diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt deleted file mode 100644 index a2d63eed4..000000000 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/ResetPasswordService.kt +++ /dev/null @@ -1,8 +0,0 @@ -package be.osoc.team1.backend.services - -import be.osoc.team1.backend.repositories.UserRepository -import org.springframework.stereotype.Service - -@Service -class ResetPasswordService(private val repository: UserRepository) { -} \ No newline at end of file diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index c94f864fd..ab4427954 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -5,6 +5,7 @@ import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.ForbiddenOperationException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.UserRepository +import be.osoc.team1.backend.security.EmailUtil import be.osoc.team1.backend.security.TokenUtil import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull @@ -90,7 +91,7 @@ class UserService(private val repository: UserRepository, private val passwordEn fun getTokenByMail(email: String) { if (repository.findByEmail(email) != null) { val resetPasswordToken = TokenUtil.createResetPasswordToken(email) - // send mail to [email] containing resetpasswordtoken + EmailUtil.sendEmail(email, resetPasswordToken) } } } From af33d57f9d409bac9d3a36eea1b727024344d398 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 3 May 2022 17:10:40 +0200 Subject: [PATCH 006/425] feat: sending an email works --- .../backend/controllers/UserController.kt | 4 +-- .../osoc/team1/backend/security/ConfigUtil.kt | 2 +- .../osoc/team1/backend/security/EmailUtil.kt | 27 +++++++++++-------- .../team1/backend/services/UserService.kt | 8 +++--- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index 301ee0833..a3914e422 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -82,6 +82,6 @@ class UserController(private val service: UserService) { /** * Reset password */ - @PostMapping("/resetpassword") - fun postEmail(@RequestBody email: String) = service.getTokenByMail(email) + @PostMapping("/resetPassword") + fun postEmail(/*@RequestBody email: String*/) = service.getTokenByMail("tymenvanhimme@gmail.com") } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt index 525a7873e..c8abce1c5 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt @@ -6,6 +6,6 @@ package be.osoc.team1.backend.security */ object ConfigUtil { val urlsOpenToAll: Array = arrayOf("/", "/login", "/logout", "/error") - val urlsOpenToAllToPostTo: Array = arrayOf("/users", "/token/refresh", "/*/students") + val urlsOpenToAllToPostTo: Array = arrayOf("/users", "/users/resetPassword", "/token/refresh", "/*/students") val allowedCorsOrigins: List = listOf("http://localhost:3000", "https://sel2-1.ugent.be") } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 011d37ce1..7f70e5bd8 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -3,16 +3,14 @@ package be.osoc.team1.backend.security import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.JavaMailSenderImpl -import java.util.* +import java.util.Properties /** * This object contains every function needed to create and send emails. */ object EmailUtil { - private fun getResetPasswordEmailBody( - contextPath: String, token: String - ): String { - val url = "$contextPath/users/resetpassword?$token" + private fun getResetPasswordEmailBody(resetPasswordToken: String): String { + val url = "api/users/resetPassword?$resetPasswordToken" val message: String = "Hallo, \r\n dit is een testtt.tt. \n mvg\r\ntestymen" return "$message \r\n$url" } @@ -21,25 +19,32 @@ object EmailUtil { val mailSender = JavaMailSenderImpl() mailSender.host = "smtp.gmail.com" mailSender.port = 587 - mailSender.username = "my.gmail@gmail.com" - mailSender.password = "password" + mailSender.username = "tymenvanhimme@gmail.com" + mailSender.password = "secret" val props: Properties = mailSender.javaMailProperties props["mail.transport.protocol"] = "smtp" - props["mail.smtp.auth"] = "true" + // props["mail.smtp.auth"] = "true" props["mail.smtp.starttls.enable"] = "true" props["mail.debug"] = "true" return mailSender } - fun sendEmail(emailaddress: String, resetPasswordToken: String) { + println(">>>>>>>") + println(">>>>>>>") + println(">>>>>>>") + println(">>>>>>>") + println(">>>>>>>") + println(">>>>>>>") + println(">>>>>>>") + println(">>>>>>>") val mailSender = getJavaMailSender() val email = SimpleMailMessage() email.setSubject("Reset Password") - email.setText(getResetPasswordEmailBody("/api", resetPasswordToken)) + email.setText(getResetPasswordEmailBody(resetPasswordToken)) email.setTo(emailaddress) - email.setFrom("osoc.support@mail.com") + email.setFrom("tymenvanhimme@gmail.com") mailSender.send(email) } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index ab4427954..80f9e035c 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -89,9 +89,9 @@ class UserService(private val repository: UserRepository, private val passwordEn * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. */ fun getTokenByMail(email: String) { - if (repository.findByEmail(email) != null) { - val resetPasswordToken = TokenUtil.createResetPasswordToken(email) - EmailUtil.sendEmail(email, resetPasswordToken) - } + // if (repository.findByEmail(email) != null) { + val resetPasswordToken = TokenUtil.createResetPasswordToken(email) + EmailUtil.sendEmail(email, resetPasswordToken) + // } } } From b3199b1276ca9ab664ad53b0984c816759623ad2 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 3 May 2022 18:11:47 +0200 Subject: [PATCH 007/425] feat: cleanup of email implementation --- .../backend/controllers/UserController.kt | 4 +- .../osoc/team1/backend/security/EmailUtil.kt | 41 +++++++++++-------- .../team1/backend/services/UserService.kt | 11 ++--- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index a3914e422..b48ac6530 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -80,8 +80,8 @@ class UserController(private val service: UserService) { service.changeRole(id, role) /** - * Reset password + * Request a resetPasswordToken for [email]. */ @PostMapping("/resetPassword") - fun postEmail(/*@RequestBody email: String*/) = service.getTokenByMail("tymenvanhimme@gmail.com") + fun postEmail(@RequestBody email: String) = service.getResetPasswordTokenByMail(email) } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 7f70e5bd8..a81f15574 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -9,43 +9,48 @@ import java.util.Properties * This object contains every function needed to create and send emails. */ object EmailUtil { + private const val emailAddressSender = "noreply@osoc.com" + private const val passwordSender = "insert.password.here" + private const val baseUrl = "http://localhost:8080/api" + private fun getResetPasswordEmailBody(resetPasswordToken: String): String { - val url = "api/users/resetPassword?$resetPasswordToken" - val message: String = "Hallo, \r\n dit is een testtt.tt. \n mvg\r\ntestymen" - return "$message \r\n$url" + val url = "$baseUrl/users/resetPassword?$resetPasswordToken" + return """ + Hello, + Use the link below to set your new password. + $url + """.trimIndent() } private fun getJavaMailSender(): JavaMailSender { val mailSender = JavaMailSenderImpl() mailSender.host = "smtp.gmail.com" mailSender.port = 587 - mailSender.username = "tymenvanhimme@gmail.com" - mailSender.password = "secret" + mailSender.username = emailAddressSender + mailSender.password = passwordSender val props: Properties = mailSender.javaMailProperties props["mail.transport.protocol"] = "smtp" - // props["mail.smtp.auth"] = "true" + props["mail.smtp.auth"] = "true" props["mail.smtp.starttls.enable"] = "true" props["mail.debug"] = "true" return mailSender } - fun sendEmail(emailaddress: String, resetPasswordToken: String) { - println(">>>>>>>") - println(">>>>>>>") - println(">>>>>>>") - println(">>>>>>>") - println(">>>>>>>") - println(">>>>>>>") - println(">>>>>>>") - println(">>>>>>>") + fun sendEmail(emailaddressReceiver: String, resetPasswordToken: String) { val mailSender = getJavaMailSender() val email = SimpleMailMessage() email.setSubject("Reset Password") email.setText(getResetPasswordEmailBody(resetPasswordToken)) - email.setTo(emailaddress) - email.setFrom("tymenvanhimme@gmail.com") + email.setTo(emailaddressReceiver) + email.setFrom(emailAddressSender) - mailSender.send(email) + println(">>>>>>>") + println("To: $emailaddressReceiver") + println("From: $emailAddressSender") + println(email.subject) + println(email.text) + println(">>>>>>>") + // mailSender.send(email) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 80f9e035c..40a6656ad 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -88,10 +88,11 @@ class UserService(private val repository: UserRepository, private val passwordEn /** * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. */ - fun getTokenByMail(email: String) { - // if (repository.findByEmail(email) != null) { - val resetPasswordToken = TokenUtil.createResetPasswordToken(email) - EmailUtil.sendEmail(email, resetPasswordToken) - // } + fun getResetPasswordTokenByMail(email: String) { + println("\"$email\"") + if (repository.findByEmail(email) != null) { + val resetPasswordToken = TokenUtil.createResetPasswordToken(email) + EmailUtil.sendEmail(email, resetPasswordToken) + } } } From 138bd8532a0b67a7979ac0f112fb0b32bad12fa4 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Wed, 4 May 2022 11:15:37 +0200 Subject: [PATCH 008/425] feat: reset password flow works --- .../team1/backend/controllers/UserController.kt | 7 +++++++ .../be/osoc/team1/backend/entities/User.kt | 2 +- .../osoc/team1/backend/security/ConfigUtil.kt | 8 ++++++-- .../be/osoc/team1/backend/security/EmailUtil.kt | 2 +- .../osoc/team1/backend/services/UserService.kt | 17 +++++++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index b48ac6530..eb8bacaed 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -84,4 +84,11 @@ class UserController(private val service: UserService) { */ @PostMapping("/resetPassword") fun postEmail(@RequestBody email: String) = service.getResetPasswordTokenByMail(email) + + /** + * Reset password using [resetPasswordToken]. + */ + @PatchMapping("/resetPassword/{resetPasswordToken}") + fun patchPassword(@PathVariable resetPasswordToken: String, @RequestBody newPassword: String) = + service.changePassword(resetPasswordToken, newPassword) } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/entities/User.kt b/backend/src/main/kotlin/be/osoc/team1/backend/entities/User.kt index ed09e260e..5e84374d9 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/entities/User.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/entities/User.kt @@ -44,7 +44,7 @@ class User( var role: Role = Role.Disabled, @field:JsonView(EntityViews.Hidden::class) - val password: String, + var password: String, ) { @Id @field:JsonView(EntityViews.Public::class) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt index c8abce1c5..b6a920dcd 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt @@ -5,7 +5,11 @@ package be.osoc.team1.backend.security * in [SecurityConfig]. */ object ConfigUtil { - val urlsOpenToAll: Array = arrayOf("/", "/login", "/logout", "/error") - val urlsOpenToAllToPostTo: Array = arrayOf("/users", "/users/resetPassword", "/token/refresh", "/*/students") + val urlsOpenToAll: Array = arrayOf( + "/", "/login", "/logout", "/error", "/users/resetPassword/*" + ) + val urlsOpenToAllToPostTo: Array = arrayOf( + "/users", "/users/resetPassword", "/token/refresh", "/*/students" + ) val allowedCorsOrigins: List = listOf("http://localhost:3000", "https://sel2-1.ugent.be") } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index a81f15574..30f0fbc1a 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -14,7 +14,7 @@ object EmailUtil { private const val baseUrl = "http://localhost:8080/api" private fun getResetPasswordEmailBody(resetPasswordToken: String): String { - val url = "$baseUrl/users/resetPassword?$resetPasswordToken" + val url = "$baseUrl/users/resetPassword/$resetPasswordToken" return """ Hello, Use the link below to set your new password. diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 40a6656ad..b8f93d0bd 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -3,6 +3,7 @@ package be.osoc.team1.backend.services import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.ForbiddenOperationException +import be.osoc.team1.backend.exceptions.InvalidTokenException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.UserRepository import be.osoc.team1.backend.security.EmailUtil @@ -95,4 +96,20 @@ class UserService(private val repository: UserRepository, private val passwordEn EmailUtil.sendEmail(email, resetPasswordToken) } } + + /** + * change the password of [email]. + */ + fun changePassword(resetPasswordToken: String, newPassword: String) { + try { + val email = TokenUtil.decodeAndVerifyToken(resetPasswordToken).subject + val user: User = repository.findByEmail(email)!! + user.password = passwordEncoder.encode(newPassword) + repository.save(user) + } catch (_: NullPointerException) { + throw InvalidTokenException("ResetPasswordToken contains invalid email.") + } catch (_: Exception) { + throw InvalidTokenException("invalid resetPasswordToken given.") + } + } } From 8c2419fe0d58c67288be272114958a67b6180687 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Wed, 4 May 2022 11:30:38 +0200 Subject: [PATCH 009/425] style: fix koltin error --- .../src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt | 2 +- .../main/kotlin/be/osoc/team1/backend/services/UserService.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 30f0fbc1a..5e991a87b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -53,4 +53,4 @@ object EmailUtil { println(">>>>>>>") // mailSender.send(email) } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index b8f93d0bd..183e1b34b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -90,7 +90,6 @@ class UserService(private val repository: UserRepository, private val passwordEn * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. */ fun getResetPasswordTokenByMail(email: String) { - println("\"$email\"") if (repository.findByEmail(email) != null) { val resetPasswordToken = TokenUtil.createResetPasswordToken(email) EmailUtil.sendEmail(email, resetPasswordToken) From 637b93177254ad8632676ef3a9fba3fa351efaeb Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 11:59:22 +0200 Subject: [PATCH 010/425] feat: page to enter emailaddress --- frontend/components/HotToast.tsx | 44 ++++++++++++++++ frontend/lib/endpoints.ts | 1 + frontend/pages/login.tsx | 5 ++ frontend/pages/resetPassword.tsx | 89 ++++++++++++++++++++++++++++++++ frontend/styles/line.css | 12 +++++ 5 files changed, 151 insertions(+) create mode 100644 frontend/components/HotToast.tsx create mode 100644 frontend/pages/resetPassword.tsx diff --git a/frontend/components/HotToast.tsx b/frontend/components/HotToast.tsx new file mode 100644 index 000000000..194c8c67a --- /dev/null +++ b/frontend/components/HotToast.tsx @@ -0,0 +1,44 @@ +import { Toast, useToaster } from "react-hot-toast"; + +const HotToastNotifications = () => { + const { toasts, handlers } = useToaster(); + const { startPause, endPause } = handlers; + + return ( +
+ //
+ // {/* {toasts.map((toast) => { + // return ( + // // <> + //
+ // Dit is hot + // {/* {toast.message} */} + //
+ // ); + // })} */} + //
+ ); + }; + +export default HotToastNotifications; + \ No newline at end of file diff --git a/frontend/lib/endpoints.ts b/frontend/lib/endpoints.ts index 1d85e7a84..dc55aaa9a 100644 --- a/frontend/lib/endpoints.ts +++ b/frontend/lib/endpoints.ts @@ -11,6 +11,7 @@ enum Endpoints { PROJECTS = '/projects', STUDENTS = '/students', SKILLS = '/skills', + RESETPASSWORD = '/users/resetPassword' } export default Endpoints; diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index ecc66e525..177a068cf 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -120,6 +120,11 @@ const Login = () => { no account yet?
register here!

+ +

+ password forgotten? +

+

Or log in using

diff --git a/frontend/pages/resetPassword.tsx b/frontend/pages/resetPassword.tsx new file mode 100644 index 000000000..9044077af --- /dev/null +++ b/frontend/pages/resetPassword.tsx @@ -0,0 +1,89 @@ +import { NextPage } from 'next'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { FormEventHandler, useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import FormContainer from '../components/FormContainer'; +import useTokens from '../hooks/useTokens'; +import useUser from '../hooks/useUser'; +import { Edition, UserRole } from '../lib/types'; +import axios from '../lib/axios'; +import Endpoints from '../lib/endpoints'; +import usePersistentInput from '../hooks/usePersistentInput'; +import useEdition from '../hooks/useEdition'; +import HotToastNotifications from '../components/HotToast'; +import StudentTile from '../components/students/StudentTile'; + +const ResetPassword: NextPage = () => { + const emailRef = useRef(null); + + /* eslint-disable */ + const [email, resetEmail, emailProps] = usePersistentInput('email', ''); + + useEffect(() => { + emailRef?.current?.focus(); + }, []); + + const router = useRouter(); + + const doSubmit: FormEventHandler = async (e) => { + e.preventDefault(); + + if (email) { + try { + const response = await axios.post( + Endpoints.RESETPASSWORD, + new URLSearchParams({email}), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + // router.push('/'); + toast.success("An email has been sent to "+email+".\nMake sure this is your email"); + toast.success( + (t) => ( + + Email sent
+ An email has been sent to email.
+ +
+ ), + { duration: 12000 } + ); + } catch (err) { + console.log(err); + toast.error('An error occurred while trying to reset password.'); + } + } + }; + + return ( + <> + +
+ + +
+
+ + ) +} + + +export default ResetPassword; diff --git a/frontend/styles/line.css b/frontend/styles/line.css index fced32181..fe7a31c48 100644 --- a/frontend/styles/line.css +++ b/frontend/styles/line.css @@ -72,3 +72,15 @@ .i-inline > svg { display: inline; } + +.closeButton { + all: initial; + border: solid 2px #efefff; + border-radius: 0.4em; + line-height: 1.4em; + width: 1.5em; + padding-bottom: 0.1em; + text-align: center; + margin-left: 0.5em; + cursor: pointer; +} From 0d655c9456b2c9804f0abb1cd21578021ebfd24d Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 13:24:37 +0200 Subject: [PATCH 011/425] chore: style email toast --- frontend/pages/resetPassword.tsx | 5 ++--- frontend/styles/line.css | 9 ++------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/frontend/pages/resetPassword.tsx b/frontend/pages/resetPassword.tsx index 9044077af..7a0e66bda 100644 --- a/frontend/pages/resetPassword.tsx +++ b/frontend/pages/resetPassword.tsx @@ -41,13 +41,12 @@ const ResetPassword: NextPage = () => { } ); // router.push('/'); - toast.success("An email has been sent to "+email+".\nMake sure this is your email"); toast.success( (t) => ( Email sent
- An email has been sent to email.
- + An email has been sent to {email}
+
), { duration: 12000 } diff --git a/frontend/styles/line.css b/frontend/styles/line.css index fe7a31c48..d6fd3f6db 100644 --- a/frontend/styles/line.css +++ b/frontend/styles/line.css @@ -73,14 +73,9 @@ display: inline; } -.closeButton { - all: initial; +.okButton { border: solid 2px #efefff; border-radius: 0.4em; - line-height: 1.4em; - width: 1.5em; - padding-bottom: 0.1em; - text-align: center; - margin-left: 0.5em; + width: 2.4em; cursor: pointer; } From b96c87af652758ea495ab81a937b844b18664006 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 14:07:32 +0200 Subject: [PATCH 012/425] feat: clicking button in frontend sends mail from backend --- .../be/osoc/team1/backend/security/EmailUtil.kt | 3 +-- frontend/pages/resetPassword.tsx | 14 +++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 5e991a87b..152e74970 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -11,10 +11,9 @@ import java.util.Properties object EmailUtil { private const val emailAddressSender = "noreply@osoc.com" private const val passwordSender = "insert.password.here" - private const val baseUrl = "http://localhost:8080/api" private fun getResetPasswordEmailBody(resetPasswordToken: String): String { - val url = "$baseUrl/users/resetPassword/$resetPasswordToken" + val url = "http://localhost:3000/resetPassword/$resetPasswordToken" return """ Hello, Use the link below to set your new password. diff --git a/frontend/pages/resetPassword.tsx b/frontend/pages/resetPassword.tsx index 7a0e66bda..30447fae6 100644 --- a/frontend/pages/resetPassword.tsx +++ b/frontend/pages/resetPassword.tsx @@ -1,18 +1,11 @@ import { NextPage } from 'next'; -import Link from 'next/link'; import { useRouter } from 'next/router'; import { FormEventHandler, useEffect, useRef, useState } from 'react'; import toast from 'react-hot-toast'; import FormContainer from '../components/FormContainer'; -import useTokens from '../hooks/useTokens'; -import useUser from '../hooks/useUser'; -import { Edition, UserRole } from '../lib/types'; import axios from '../lib/axios'; import Endpoints from '../lib/endpoints'; import usePersistentInput from '../hooks/usePersistentInput'; -import useEdition from '../hooks/useEdition'; -import HotToastNotifications from '../components/HotToast'; -import StudentTile from '../components/students/StudentTile'; const ResetPassword: NextPage = () => { const emailRef = useRef(null); @@ -33,13 +26,12 @@ const ResetPassword: NextPage = () => { try { const response = await axios.post( Endpoints.RESETPASSWORD, - new URLSearchParams({email}), + email, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, + headers: { 'Content-Type' : 'text/plain' } } ); + console.log(response) // router.push('/'); toast.success( (t) => ( From 25dee6afcc00eede871bdc1ed0bdb2ae030a3dc6 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 14:44:00 +0200 Subject: [PATCH 013/425] feat: show token in frontend --- .../{resetPassword.tsx => forgotPassword.tsx} | 4 +- .../resetPassword/[resetPasswordToken].tsx | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) rename frontend/pages/{resetPassword.tsx => forgotPassword.tsx} (96%) create mode 100644 frontend/pages/resetPassword/[resetPasswordToken].tsx diff --git a/frontend/pages/resetPassword.tsx b/frontend/pages/forgotPassword.tsx similarity index 96% rename from frontend/pages/resetPassword.tsx rename to frontend/pages/forgotPassword.tsx index 30447fae6..eefc81078 100644 --- a/frontend/pages/resetPassword.tsx +++ b/frontend/pages/forgotPassword.tsx @@ -7,7 +7,7 @@ import axios from '../lib/axios'; import Endpoints from '../lib/endpoints'; import usePersistentInput from '../hooks/usePersistentInput'; -const ResetPassword: NextPage = () => { +const ForgotPassword: NextPage = () => { const emailRef = useRef(null); /* eslint-disable */ @@ -77,4 +77,4 @@ const ResetPassword: NextPage = () => { } -export default ResetPassword; +export default ForgotPassword; diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx new file mode 100644 index 000000000..6f86f0af6 --- /dev/null +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -0,0 +1,42 @@ +import type { NextPage } from 'next'; +import Header from '../../components/Header'; +import StudentSidebar from '../../components/StudentSidebar'; +import { Icon } from '@iconify/react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { useEffect, useState } from 'react'; +import { + ProjectBase, + ProjectData, + StudentBase, + UserRole, +} from '../../lib/types'; +import { axiosAuthenticated } from '../../lib/axios'; +import Endpoints from '../../lib/endpoints'; +import useAxiosAuth from '../../hooks/useAxiosAuth'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import Popup from 'reactjs-popup'; +import ProjectTile from '../../components/projects/ProjectTile'; +import ProjectPopup, { + defaultprojectForm, +} from '../../components/projects/ProjectPopup'; +import FlatList from 'flatlist-react'; +import useUser from '../../hooks/useUser'; +import { SpinnerCircular } from 'spinners-react'; +import Error from '../../components/Error'; +import { parseError } from '../../lib/requestUtils'; +import RouteProtection from '../../components/RouteProtection'; +import { useRouter } from 'next/router'; +import { NextRouter } from 'next/dist/client/router'; + +const ResetPassword: NextPage = () => { + const router = useRouter(); + const token = router.query.resetPasswordToken as string; + + return ( +
{token}
+ ); +}; + +export default ResetPassword; From ef44db7e8ca699922b0db92b9e57fe1d154595d6 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 14:59:37 +0200 Subject: [PATCH 014/425] feat: page to enter new password --- .../resetPassword/[resetPasswordToken].tsx | 91 +++++++++++++------ 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index 6f86f0af6..ac9e9cf96 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -1,41 +1,72 @@ import type { NextPage } from 'next'; -import Header from '../../components/Header'; -import StudentSidebar from '../../components/StudentSidebar'; -import { Icon } from '@iconify/react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; -import { useEffect, useState } from 'react'; -import { - ProjectBase, - ProjectData, - StudentBase, - UserRole, -} from '../../lib/types'; -import { axiosAuthenticated } from '../../lib/axios'; -import Endpoints from '../../lib/endpoints'; -import useAxiosAuth from '../../hooks/useAxiosAuth'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import Popup from 'reactjs-popup'; -import ProjectTile from '../../components/projects/ProjectTile'; -import ProjectPopup, { - defaultprojectForm, -} from '../../components/projects/ProjectPopup'; -import FlatList from 'flatlist-react'; -import useUser from '../../hooks/useUser'; -import { SpinnerCircular } from 'spinners-react'; -import Error from '../../components/Error'; -import { parseError } from '../../lib/requestUtils'; -import RouteProtection from '../../components/RouteProtection'; import { useRouter } from 'next/router'; -import { NextRouter } from 'next/dist/client/router'; +import { FormEventHandler, useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import FormContainer from '../../components/FormContainer'; +import axios from '../../lib/axios'; +import Endpoints from '../../lib/endpoints'; +import usePersistentInput from '../../hooks/usePersistentInput'; const ResetPassword: NextPage = () => { const router = useRouter(); const token = router.query.resetPasswordToken as string; + const [password, setPassword] = useState(''); + + const doSubmit: FormEventHandler = async (e) => { + e.preventDefault(); + + if (password) { + try { + const response = await axios.post( + Endpoints.RESETPASSWORD, + password, + { + headers: { 'Content-Type' : 'text/plain' } + } + ); + console.log(response) + // router.push('/'); + toast.success( + (t) => ( + + Email sent
+ Password has been reset to {password}
+ +
+ ), + { duration: 12000 } + ); + } catch (err) { + console.log(err); + toast.error('An error occurred while trying to reset password.'); + } + } + }; + return ( -
{token}
+ <> + +
+ + +
+
+ ); }; From 49e9ba68b69623a58144cbf9276e4e7fc6a5f05a Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 15:58:45 +0200 Subject: [PATCH 015/425] style: fix eslint errors --- .../team1/backend/services/UserService.kt | 1 + frontend/components/HotToast.tsx | 44 ------------------- frontend/pages/forgotPassword.tsx | 2 +- .../resetPassword/[resetPasswordToken].tsx | 28 ++++++------ 4 files changed, 17 insertions(+), 58 deletions(-) delete mode 100644 frontend/components/HotToast.tsx diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 183e1b34b..8c32f40ca 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -106,6 +106,7 @@ class UserService(private val repository: UserRepository, private val passwordEn user.password = passwordEncoder.encode(newPassword) repository.save(user) } catch (_: NullPointerException) { + println("invalid email") throw InvalidTokenException("ResetPasswordToken contains invalid email.") } catch (_: Exception) { throw InvalidTokenException("invalid resetPasswordToken given.") diff --git a/frontend/components/HotToast.tsx b/frontend/components/HotToast.tsx deleted file mode 100644 index 194c8c67a..000000000 --- a/frontend/components/HotToast.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Toast, useToaster } from "react-hot-toast"; - -const HotToastNotifications = () => { - const { toasts, handlers } = useToaster(); - const { startPause, endPause } = handlers; - - return ( -
- //
- // {/* {toasts.map((toast) => { - // return ( - // // <> - //
- // Dit is hot - // {/* {toast.message} */} - //
- // ); - // })} */} - //
- ); - }; - -export default HotToastNotifications; - \ No newline at end of file diff --git a/frontend/pages/forgotPassword.tsx b/frontend/pages/forgotPassword.tsx index eefc81078..08a0d987a 100644 --- a/frontend/pages/forgotPassword.tsx +++ b/frontend/pages/forgotPassword.tsx @@ -1,6 +1,6 @@ import { NextPage } from 'next'; import { useRouter } from 'next/router'; -import { FormEventHandler, useEffect, useRef, useState } from 'react'; +import { FormEventHandler, useEffect, useRef } from 'react'; import toast from 'react-hot-toast'; import FormContainer from '../components/FormContainer'; import axios from '../lib/axios'; diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index ac9e9cf96..8c56f76d1 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -18,25 +18,27 @@ const ResetPassword: NextPage = () => { if (password) { try { - const response = await axios.post( - Endpoints.RESETPASSWORD, + const response = await axios.patch( + Endpoints.RESETPASSWORD + "/" + token, password, { headers: { 'Content-Type' : 'text/plain' } } ); - console.log(response) // router.push('/'); - toast.success( - (t) => ( - - Email sent
- Password has been reset to {password}
- -
- ), - { duration: 12000 } - ); + if (response?.data) { + toast.success( + (t) => ( + + Password reset
+ Password has been reset to {password}
+ +
+ ), + { duration: 12000 } + ); + } + console.log(response) } catch (err) { console.log(err); toast.error('An error occurred while trying to reset password.'); From 3b861e729585355191178b192b47df62c013632c Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 17:33:25 +0200 Subject: [PATCH 016/425] refactor: don't use JWT tokens for password reset --- .../backend/security/ResetPasswordUtil.kt | 38 +++++++++++++++++++ .../osoc/team1/backend/security/TokenUtil.kt | 6 --- .../team1/backend/services/UserService.kt | 23 +++++------ .../resetPassword/[resetPasswordToken].tsx | 1 - 4 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt new file mode 100644 index 000000000..d6f38ba23 --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -0,0 +1,38 @@ +package be.osoc.team1.backend.security + +import org.springframework.security.crypto.password.PasswordEncoder +import kotlin.random.Random + +object ResetPasswordUtil { + private val resetTokens: MutableMap = mutableMapOf() + + fun newToken(email: String, passwordEncoder: PasswordEncoder): String { + val uuid: String = Random.nextBytes(64).toString() + val hashedUuid = passwordEncoder.encode(uuid) + resetTokens[hashedUuid] = ResetToken(email) + println("uuid: $uuid") + println("hashed: $hashedUuid") + return uuid + } + + private fun isTokenValid(hashedUuid: String): Boolean { + val resetToken: ResetToken? = resetTokens[hashedUuid] + return (hashedUuid in resetTokens && !resetTokens[hashedUuid]!!.isExpired()) + } + + fun getEmailFromToken(hashedUuid: String): String? { + if (isTokenValid(hashedUuid)) { + return resetTokens[hashedUuid]!!.email + } + return null + } +} + +data class ResetToken( + val email: String, + val ttl: Long = System.currentTimeMillis() + 30 * 60 * 1000 +) { + fun isExpired(): Boolean { + return ttl < System.currentTimeMillis() + } +} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt index 14c37c59d..fedda4db6 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt @@ -37,12 +37,6 @@ object TokenUtil { */ private val validRefreshTokens: MutableMap = mutableMapOf() - /** - * This map holds the latest resetPasswordToken per email, so the keys are emails and the values are - * resetPasswordTokens. - */ - private val validResetPasswordTokens: MutableMap = mutableMapOf() - /** * Create a JSON web token. The token contains email, an id, expiration date of token, whether the token is an * access token and the authorities of the user. Set [isAccessToken] to true when making an access token, set it to diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 8c32f40ca..09f15634e 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -7,6 +7,7 @@ import be.osoc.team1.backend.exceptions.InvalidTokenException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.UserRepository import be.osoc.team1.backend.security.EmailUtil +import be.osoc.team1.backend.security.ResetPasswordUtil import be.osoc.team1.backend.security.TokenUtil import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull @@ -82,7 +83,6 @@ class UserService(private val repository: UserRepository, private val passwordEn if (oldUser.password != updatedUser.password) { throw ForbiddenOperationException("Not allowed to update password field of users") } - return repository.save(updatedUser) } @@ -91,25 +91,20 @@ class UserService(private val repository: UserRepository, private val passwordEn */ fun getResetPasswordTokenByMail(email: String) { if (repository.findByEmail(email) != null) { - val resetPasswordToken = TokenUtil.createResetPasswordToken(email) + val resetPasswordToken = ResetPasswordUtil.newToken(email, passwordEncoder) EmailUtil.sendEmail(email, resetPasswordToken) } } /** - * change the password of [email]. + * Get the email address from [resetPasswordToken] and set its password to [newPassword]. */ fun changePassword(resetPasswordToken: String, newPassword: String) { - try { - val email = TokenUtil.decodeAndVerifyToken(resetPasswordToken).subject - val user: User = repository.findByEmail(email)!! - user.password = passwordEncoder.encode(newPassword) - repository.save(user) - } catch (_: NullPointerException) { - println("invalid email") - throw InvalidTokenException("ResetPasswordToken contains invalid email.") - } catch (_: Exception) { - throw InvalidTokenException("invalid resetPasswordToken given.") - } + val email = ResetPasswordUtil.getEmailFromToken(resetPasswordToken) + ?: throw InvalidTokenException("ResetPasswordToken is invalid.") + val user: User = repository.findByEmail(email) + ?: throw InvalidTokenException("ResetPasswordToken contains invalid email.") + user.password = passwordEncoder.encode(newPassword) + repository.save(user) } } diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index 8c56f76d1..ffe0446fe 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -5,7 +5,6 @@ import toast from 'react-hot-toast'; import FormContainer from '../../components/FormContainer'; import axios from '../../lib/axios'; import Endpoints from '../../lib/endpoints'; -import usePersistentInput from '../../hooks/usePersistentInput'; const ResetPassword: NextPage = () => { const router = useRouter(); From 5984a6e9f4e914b61132248291da6daf02586249 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 5 May 2022 17:45:09 +0200 Subject: [PATCH 017/425] fix: use uuid instead of bytearray --- .../be/osoc/team1/backend/security/ResetPasswordUtil.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index d6f38ba23..9f276c037 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -1,14 +1,14 @@ package be.osoc.team1.backend.security import org.springframework.security.crypto.password.PasswordEncoder -import kotlin.random.Random +import java.util.UUID object ResetPasswordUtil { private val resetTokens: MutableMap = mutableMapOf() - fun newToken(email: String, passwordEncoder: PasswordEncoder): String { - val uuid: String = Random.nextBytes(64).toString() - val hashedUuid = passwordEncoder.encode(uuid) + fun newToken(email: String, passwordEncoder: PasswordEncoder): UUID { + val uuid: UUID = UUID.randomUUID() + val hashedUuid = passwordEncoder.encode(uuid.toString()) resetTokens[hashedUuid] = ResetToken(email) println("uuid: $uuid") println("hashed: $hashedUuid") @@ -16,7 +16,6 @@ object ResetPasswordUtil { } private fun isTokenValid(hashedUuid: String): Boolean { - val resetToken: ResetToken? = resetTokens[hashedUuid] return (hashedUuid in resetTokens && !resetTokens[hashedUuid]!!.isExpired()) } From fc83d270379bea870aab56c75be56658f6790476 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 5 May 2022 19:12:42 +0200 Subject: [PATCH 018/425] feat: added @EditionSecurity annotation that can be used on controllers with edition parameters (Interceptor was not removed yet but just disabled by making it return early) --- backend/pom.xml | 5 +- .../backend/controllers/BaseController.kt | 21 ++++++-- .../backend/controllers/ControllersUtil.kt | 49 ++++++++++++++++++- .../backend/controllers/StudentController.kt | 6 +++ .../be/osoc/team1/backend/entities/Edition.kt | 2 + 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index ae904f27b..9e198db60 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -113,7 +113,10 @@ hibernate-validator 7.0.4.Final - + + org.springframework.boot + spring-boot-starter-aop + diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt index 198297029..75c7f4da4 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt @@ -8,9 +8,12 @@ import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.services.AnswerService import be.osoc.team1.backend.services.AssignmentService import be.osoc.team1.backend.services.BaseService +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.PositionService import be.osoc.team1.backend.services.SkillService import be.osoc.team1.backend.services.StatusSuggestionService +import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.access.annotation.Secured import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -18,7 +21,19 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.util.UUID -abstract class BaseController(open val service: BaseService) { +abstract class BaseController(open val service: BaseService) { + @Autowired + lateinit var editionService: EditionService + + @Autowired + lateinit var userDetailService: OsocUserDetailService + + private fun attemptAccess(entity: T): T { + val editionName = entity::class.java.getDeclaredField("edition").get(entity) as String + attemptEditionAccess(editionName, editionService, userDetailService) + + return entity + } /** * Returns the [T] with the corresponding [id]. If no such [T] exists, returns a @@ -26,10 +41,10 @@ abstract class BaseController(open val service: BaseService) { */ @GetMapping("/{id}") @Secured("ROLE_COACH") - fun getById(@PathVariable id: K): T = service.getById(id) + open fun getById(@PathVariable id: K): T = attemptAccess(service.getById(id)) } -abstract class BaseAllController(service: BaseService) : BaseController(service) { +abstract class BaseAllController(service: BaseService) : BaseController(service) { /** * Returns all objects of type [T]. diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index 4ec9f5c60..ddb443dac 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -1,7 +1,12 @@ package be.osoc.team1.backend.controllers +import be.osoc.team1.backend.exceptions.UnauthorizedOperationException import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.aspectj.lang.reflect.MethodSignature import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -14,6 +19,8 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.resource.ResourceHttpRequestHandler import org.springframework.web.servlet.support.ServletUriComponentsBuilder +import java.lang.annotation.Inherited +import java.security.Principal import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -44,6 +51,18 @@ fun getObjectCreatedResponse( .body(createdObject) } +fun attemptEditionAccess( + editionName: String, + editionService: EditionService, + userDetailService: OsocUserDetailService +) { + val edition = editionService.getEdition(editionName) + val authentication = SecurityContextHolder.getContext().authentication + val user = userDetailService.getUserFromPrincipal(authentication) + if (!edition.accessibleBy(user)) + throw UnauthorizedOperationException("Inactive editions can only be accessed by admins!") +} + @Component class EditionInterceptor(val editionService: EditionService) : HandlerInterceptor { @@ -54,12 +73,14 @@ class EditionInterceptor(val editionService: EditionService) : response: HttpServletResponse, handler: Any ): Boolean { + return super.preHandle(request, response, handler) // URLs that are always allowed val regex = Regex("^.*/api/(error|editions|login|communications|users|assignments|positions|statusSuggestions|answers|skills|logout|token).*$") // this !is check is here so invalid requests (such as GETs to endpoints that don't exist) still get handled regularly if (!regex.matches(request.requestURI) && handler !is ResourceHttpRequestHandler) { val roles = SecurityContextHolder.getContext().authentication.authorities.map { it.toString() } + SecurityContextHolder.getContext().authentication as Principal val editionName = request.requestURI.split("/")[2] if (editionService.getActiveEdition()?.name != editionName) { if (!roles.contains("ROLE_ADMIN")) { @@ -92,4 +113,30 @@ class InterceptorConfig : WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(editionInterceptor) } -} \ No newline at end of file +} + +@Aspect +@Component +class EditionSecurityAspect(val editionService: EditionService, val userDetailService: OsocUserDetailService) { + + @Before(value = "@annotation(SecuredEdition)") + @Throws(Throwable::class) + fun validateEditionArgument(joinPoint: JoinPoint): Any { + val method = (joinPoint.signature as MethodSignature).method + val securedEdition = method.annotations.find { SecuredEdition::class.java.isInstance(it) } as SecuredEdition + val editionFieldIndex = method.parameters.indexOfFirst { it.name == securedEdition.editionArgument } + if (editionFieldIndex < 0) + throw IllegalStateException("The specified @SecuredEdition editionArgument was not found!") + val editionName = joinPoint.args[editionFieldIndex] as String + + attemptEditionAccess(editionName, editionService, userDetailService) + + return joinPoint + } +} + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Inherited //Doesn't work in kotlin... +annotation class SecuredEdition(val editionArgument: String) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index c43abcb5f..f76b7d703 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -57,6 +57,7 @@ class StudentController( */ @GetMapping @Secured("ROLE_COACH") + @SecuredEdition("edition") fun getAllStudents( @RequestParam(defaultValue = "0") pageNumber: Int, @RequestParam(defaultValue = "50") pageSize: Int, @@ -98,6 +99,7 @@ class StudentController( */ @GetMapping("/{studentId}") @Secured("ROLE_COACH") + @SecuredEdition("edition") fun getStudentById(@PathVariable studentId: UUID, @PathVariable edition: String): Student = service.getStudentById(studentId, edition) @@ -108,6 +110,7 @@ class StudentController( @DeleteMapping("/{studentId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") + @SecuredEdition("edition") fun deleteStudentById(@PathVariable studentId: UUID, @PathVariable edition: String) = service.deleteStudentById(studentId) @@ -158,6 +161,7 @@ class StudentController( @PostMapping("/{studentId}/status") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") + @SecuredEdition("edition") fun setStudentStatus(@PathVariable studentId: UUID, @RequestBody status: StatusEnum, @PathVariable edition: String) = service.setStudentStatus(studentId, status, edition) @@ -185,6 +189,7 @@ class StudentController( @PostMapping("/{studentId}/suggestions") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_COACH") + @SecuredEdition("edition") fun addStudentStatusSuggestion( @PathVariable studentId: UUID, @RequestBody statusSuggestion: StatusSuggestion, @@ -211,6 +216,7 @@ class StudentController( @DeleteMapping("/{studentId}/suggestions/{coachId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_COACH") + @SecuredEdition("edition") fun deleteStudentStatusSuggestion( @PathVariable studentId: UUID, @PathVariable coachId: UUID, diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Edition.kt b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Edition.kt index f6154ea7f..dc0fc9173 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Edition.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Edition.kt @@ -13,4 +13,6 @@ class Edition(@Id val name: String, var isActive: Boolean) { // Needed for tests override fun equals(other: Any?): Boolean = other is Edition && name == other.name && isActive == other.isActive + + fun accessibleBy(user: User): Boolean = this.isActive || user.role.hasPermissionLevel(Role.Admin) } From d102e46e8e9beac54769a866274e97104057a0d4 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Fri, 6 May 2022 10:07:43 +0200 Subject: [PATCH 019/425] feat: SecuredEdition annotation works as intended --- .../backend/controllers/BaseController.kt | 13 +- .../backend/controllers/ControllersUtil.kt | 129 +++++++++--------- .../backend/controllers/ProjectController.kt | 4 + .../backend/controllers/StudentController.kt | 13 +- 4 files changed, 87 insertions(+), 72 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt index 75c7f4da4..cc9f433b4 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt @@ -5,6 +5,7 @@ import be.osoc.team1.backend.entities.Assignment import be.osoc.team1.backend.entities.Position import be.osoc.team1.backend.entities.Skill import be.osoc.team1.backend.entities.StatusSuggestion +import be.osoc.team1.backend.exceptions.UnauthorizedOperationException import be.osoc.team1.backend.services.AnswerService import be.osoc.team1.backend.services.AssignmentService import be.osoc.team1.backend.services.BaseService @@ -19,7 +20,8 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import java.util.UUID +import java.util.* +import javax.servlet.http.HttpServletRequest abstract class BaseController(open val service: BaseService) { @Autowired @@ -28,9 +30,16 @@ abstract class BaseController(open val service: BaseService) { @Autowired lateinit var userDetailService: OsocUserDetailService + @Autowired + private lateinit var request: HttpServletRequest + + /** + * Checks if this [entity] can be accessed by the requesting user. + * If the edition of the [entity] isn't active and the requesting user isn't admin [attemptEditionAccess] will throw an [UnauthorizedOperationException] + */ private fun attemptAccess(entity: T): T { val editionName = entity::class.java.getDeclaredField("edition").get(entity) as String - attemptEditionAccess(editionName, editionService, userDetailService) + attemptEditionAccess(editionName, editionService, userDetailService, request) return entity } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index ddb443dac..2b0e02331 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -1,5 +1,6 @@ package be.osoc.team1.backend.controllers +import be.osoc.team1.backend.exceptions.ForbiddenOperationException import be.osoc.team1.backend.exceptions.UnauthorizedOperationException import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService @@ -13,16 +14,9 @@ import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component -import org.springframework.web.servlet.HandlerInterceptor -import org.springframework.web.servlet.ModelAndView -import org.springframework.web.servlet.config.annotation.InterceptorRegistry -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer -import org.springframework.web.servlet.resource.ResourceHttpRequestHandler import org.springframework.web.servlet.support.ServletUriComponentsBuilder import java.lang.annotation.Inherited -import java.security.Principal import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse /** * Utility method that takes an object that was just created by a service method and it's [id], @@ -54,82 +48,89 @@ fun getObjectCreatedResponse( fun attemptEditionAccess( editionName: String, editionService: EditionService, - userDetailService: OsocUserDetailService + userDetailService: OsocUserDetailService, + httpServletRequest: HttpServletRequest ) { val edition = editionService.getEdition(editionName) val authentication = SecurityContextHolder.getContext().authentication val user = userDetailService.getUserFromPrincipal(authentication) - if (!edition.accessibleBy(user)) - throw UnauthorizedOperationException("Inactive editions can only be accessed by admins!") -} -@Component -class EditionInterceptor(val editionService: EditionService) : - HandlerInterceptor { - @Throws(Exception::class) - @Override - override fun preHandle( - request: HttpServletRequest, - response: HttpServletResponse, - handler: Any - ): Boolean { - return super.preHandle(request, response, handler) - // URLs that are always allowed - val regex = - Regex("^.*/api/(error|editions|login|communications|users|assignments|positions|statusSuggestions|answers|skills|logout|token).*$") - // this !is check is here so invalid requests (such as GETs to endpoints that don't exist) still get handled regularly - if (!regex.matches(request.requestURI) && handler !is ResourceHttpRequestHandler) { - val roles = SecurityContextHolder.getContext().authentication.authorities.map { it.toString() } - SecurityContextHolder.getContext().authentication as Principal - val editionName = request.requestURI.split("/")[2] - if (editionService.getActiveEdition()?.name != editionName) { - if (!roles.contains("ROLE_ADMIN")) { - response.sendError(401, "Inactive editions can only be accessed by admins") - return false - } - if (request.method != "GET" && request.method != "DELETE") { - response.sendError(405, "Entries from inactive editions can only be viewed or deleted") - return false - } - } - } - return super.preHandle(request, response, handler) - } + if (!edition.accessibleBy(user)) + throw UnauthorizedOperationException("Entries of inactive editions can only be accessed by admins!") - @Throws(Exception::class) - override fun postHandle( - request: HttpServletRequest, - response: HttpServletResponse, handler: Any, - modelAndView: ModelAndView? - ) { - super.postHandle(request, response, handler, modelAndView) - } + if (httpServletRequest.method != "GET" && httpServletRequest.method != "DELETE") + throw ForbiddenOperationException("Entries of inactive editions can only be viewed or delete (Allowed methods: GET, DELETE)") } -@Component -class InterceptorConfig : WebMvcConfigurer { - @Autowired - lateinit var editionInterceptor: EditionInterceptor - override fun addInterceptors(registry: InterceptorRegistry) { - registry.addInterceptor(editionInterceptor) - } -} +//@Component +//class EditionInterceptor(val editionService: EditionService) : +// HandlerInterceptor { +// @Throws(Exception::class) +// @Override +// override fun preHandle( +// request: HttpServletRequest, +// response: HttpServletResponse, +// handler: Any +// ): Boolean { +// return super.preHandle(request, response, handler) +// // URLs that are always allowed +// val regex = +// Regex("^.*/api/(error|editions|login|communications|users|assignments|positions|statusSuggestions|answers|skills|logout|token).*$") +// // this !is check is here so invalid requests (such as GETs to endpoints that don't exist) still get handled regularly +// if (!regex.matches(request.requestURI) && handler !is ResourceHttpRequestHandler) { +// val roles = SecurityContextHolder.getContext().authentication.authorities.map { it.toString() } +// SecurityContextHolder.getContext().authentication as Principal +// val editionName = request.requestURI.split("/")[2] +// if (editionService.getActiveEdition()?.name != editionName) { +// if (!roles.contains("ROLE_ADMIN")) { +// response.sendError(401, "Inactive editions can only be accessed by admins") +// return false +// } +// if (request.method != "GET" && request.method != "DELETE") { +// response.sendError(405, "Entries from inactive editions can only be viewed or deleted") +// return false +// } +// } +// } +// return super.preHandle(request, response, handler) +// } +// +// @Throws(Exception::class) +// override fun postHandle( +// request: HttpServletRequest, +// response: HttpServletResponse, handler: Any, +// modelAndView: ModelAndView? +// ) { +// super.postHandle(request, response, handler, modelAndView) +// } +//} +// +//@Component +//class InterceptorConfig : WebMvcConfigurer { +// @Autowired +// lateinit var editionInterceptor: EditionInterceptor +// override fun addInterceptors(registry: InterceptorRegistry) { +// registry.addInterceptor(editionInterceptor) +// } +//} @Aspect @Component class EditionSecurityAspect(val editionService: EditionService, val userDetailService: OsocUserDetailService) { + @Autowired + private lateinit var request: HttpServletRequest + @Before(value = "@annotation(SecuredEdition)") @Throws(Throwable::class) fun validateEditionArgument(joinPoint: JoinPoint): Any { val method = (joinPoint.signature as MethodSignature).method - val securedEdition = method.annotations.find { SecuredEdition::class.java.isInstance(it) } as SecuredEdition - val editionFieldIndex = method.parameters.indexOfFirst { it.name == securedEdition.editionArgument } + val editionFieldIndex = method.parameters.indexOfFirst { it.name == "edition" } if (editionFieldIndex < 0) - throw IllegalStateException("The specified @SecuredEdition editionArgument was not found!") + throw IllegalStateException("The @SecuredEdition edition argument was not found! (With this annotation the function needs an edition argument)") val editionName = joinPoint.args[editionFieldIndex] as String - attemptEditionAccess(editionName, editionService, userDetailService) + attemptEditionAccess(editionName, editionService, userDetailService, request) return joinPoint } @@ -139,4 +140,4 @@ class EditionSecurityAspect(val editionService: EditionService, val userDetailSe @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented @Inherited //Doesn't work in kotlin... -annotation class SecuredEdition(val editionArgument: String) +annotation class SecuredEdition diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt index 33f9428c4..96149973f 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt @@ -35,6 +35,7 @@ class ProjectController(private val service: ProjectService) { */ @GetMapping @Secured("ROLE_COACH") + @SecuredEdition fun getAllProjects( @RequestParam(defaultValue = "") name: String, @PathVariable edition: String, @@ -51,6 +52,7 @@ class ProjectController(private val service: ProjectService) { */ @GetMapping("/{projectId}") @Secured("ROLE_COACH") + @SecuredEdition fun getProjectById(@PathVariable projectId: UUID, @PathVariable edition: String): Project = service.getProjectById(projectId, edition) @@ -61,6 +63,7 @@ class ProjectController(private val service: ProjectService) { @DeleteMapping("/{projectId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") + @SecuredEdition fun deleteProjectById(@PathVariable projectId: UUID, @PathVariable edition: String) = service.deleteProjectById(projectId, edition) @@ -70,6 +73,7 @@ class ProjectController(private val service: ProjectService) { */ @PostMapping @Secured("ROLE_ADMIN") + @SecuredEdition fun postProject( @RequestBody projectRegistration: Project, @PathVariable edition: String diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index f76b7d703..28c535dba 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -57,7 +57,7 @@ class StudentController( */ @GetMapping @Secured("ROLE_COACH") - @SecuredEdition("edition") + @SecuredEdition fun getAllStudents( @RequestParam(defaultValue = "0") pageNumber: Int, @RequestParam(defaultValue = "50") pageSize: Int, @@ -99,7 +99,7 @@ class StudentController( */ @GetMapping("/{studentId}") @Secured("ROLE_COACH") - @SecuredEdition("edition") + @SecuredEdition fun getStudentById(@PathVariable studentId: UUID, @PathVariable edition: String): Student = service.getStudentById(studentId, edition) @@ -110,7 +110,7 @@ class StudentController( @DeleteMapping("/{studentId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") - @SecuredEdition("edition") + @SecuredEdition fun deleteStudentById(@PathVariable studentId: UUID, @PathVariable edition: String) = service.deleteStudentById(studentId) @@ -124,6 +124,7 @@ class StudentController( * verification is the responsibility of the caller. */ @PostMapping + @SecuredEdition fun addStudent( @RequestBody studentRegistration: Student, @PathVariable edition: String @@ -161,7 +162,7 @@ class StudentController( @PostMapping("/{studentId}/status") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") - @SecuredEdition("edition") + @SecuredEdition fun setStudentStatus(@PathVariable studentId: UUID, @RequestBody status: StatusEnum, @PathVariable edition: String) = service.setStudentStatus(studentId, status, edition) @@ -189,7 +190,7 @@ class StudentController( @PostMapping("/{studentId}/suggestions") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_COACH") - @SecuredEdition("edition") + @SecuredEdition fun addStudentStatusSuggestion( @PathVariable studentId: UUID, @RequestBody statusSuggestion: StatusSuggestion, @@ -216,7 +217,7 @@ class StudentController( @DeleteMapping("/{studentId}/suggestions/{coachId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_COACH") - @SecuredEdition("edition") + @SecuredEdition fun deleteStudentStatusSuggestion( @PathVariable studentId: UUID, @PathVariable coachId: UUID, From 4a0aeb7d588245466fc1584fe5e11a40e274c01f Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Fri, 6 May 2022 10:22:36 +0200 Subject: [PATCH 020/425] test: added the necessary mockkbean --- .../team1/backend/unittests/AssignmentControllerTests.kt | 8 ++++++++ .../backend/unittests/CommunicationControllerTests.kt | 8 ++++++++ .../team1/backend/unittests/PositionControllerTests.kt | 8 ++++++++ .../osoc/team1/backend/unittests/SkillControllerTests.kt | 8 ++++++++ .../backend/unittests/StatusSuggestionControllerTests.kt | 8 ++++++++ 5 files changed, 40 insertions(+) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt index 5a82d0ef9..fce2c788c 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt @@ -9,6 +9,8 @@ import be.osoc.team1.backend.entities.Student import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.AssignmentService +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.util.PositionSerializer import be.osoc.team1.backend.util.StudentSerializer import be.osoc.team1.backend.util.UserSerializer @@ -34,6 +36,12 @@ class AssignmentControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var assignmentService: AssignmentService + // These MockkBean are necessary because the BaseController uses these under the hood + @MockkBean + private lateinit var editionService: EditionService + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + private val testId = UUID.randomUUID() private val testStudent = Student("Jitse", "Willaert", "testEdition") private val testSkill = Skill("Test") diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt index c900a4926..89f98f473 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt @@ -5,6 +5,8 @@ import be.osoc.team1.backend.entities.Communication import be.osoc.team1.backend.entities.CommunicationTypeEnum import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.CommunicationService +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.StudentService import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean @@ -30,6 +32,12 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var studentService: StudentService + // These MockkBean are necessary because the BaseController uses these under the hood + @MockkBean + private lateinit var editionService: EditionService + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + private val testId = UUID.randomUUID() private val testEdition = "testEdition" private val editionUrl = "/$testEdition/communications" diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt index bb8c6c68d..f7be13e4a 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt @@ -4,6 +4,8 @@ import be.osoc.team1.backend.controllers.PositionController import be.osoc.team1.backend.entities.Position import be.osoc.team1.backend.entities.Skill import be.osoc.team1.backend.exceptions.InvalidIdException +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.PositionService import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean @@ -26,6 +28,12 @@ class PositionControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var positionService: PositionService + // These MockkBean are necessary because the BaseController uses these under the hood + @MockkBean + private lateinit var editionService: EditionService + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + private val testId = UUID.randomUUID() private val testSkill = Skill("Test") private val testPosition = Position(testSkill, 2) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SkillControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SkillControllerTests.kt index 17f86a17b..053ddb93c 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SkillControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SkillControllerTests.kt @@ -2,6 +2,8 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.SkillController import be.osoc.team1.backend.entities.Skill +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.SkillService import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean @@ -17,6 +19,12 @@ class SkillControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var skillService: SkillService + // These MockkBean are necessary because the BaseController uses these under the hood + @MockkBean + private lateinit var editionService: EditionService + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + @Test fun `getAll succeeds`() { val skill = Skill("Back-end") diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt index e9079096b..fee02beae 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt @@ -4,6 +4,8 @@ import be.osoc.team1.backend.controllers.StatusSuggestionController import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.entities.SuggestionEnum import be.osoc.team1.backend.exceptions.InvalidIdException +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.StatusSuggestionService import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean @@ -26,6 +28,12 @@ class StatusSuggestionControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var statusSuggestionService: StatusSuggestionService + // These MockkBean are necessary because the BaseController uses these under the hood + @MockkBean + private lateinit var editionService: EditionService + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + private val testId = UUID.randomUUID() private val coachId = UUID.randomUUID() private val testStatusSuggestion = StatusSuggestion(coachId, SuggestionEnum.Yes, "motivation") From 348a71baf08e6bec53a02530544efa29fc81b45f Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Fri, 6 May 2022 10:37:26 +0200 Subject: [PATCH 021/425] refactor: use sha 256 to hash instead of password encoder --- .../backend/controllers/UserController.kt | 8 ++--- .../osoc/team1/backend/security/EmailUtil.kt | 5 ++-- .../backend/security/ResetPasswordUtil.kt | 29 +++++++++++-------- .../team1/backend/services/UserService.kt | 11 ++++--- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index eb8bacaed..86129d42c 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -86,9 +86,9 @@ class UserController(private val service: UserService) { fun postEmail(@RequestBody email: String) = service.getResetPasswordTokenByMail(email) /** - * Reset password using [resetPasswordToken]. + * Reset password using [resetPasswordUUID]. */ - @PatchMapping("/resetPassword/{resetPasswordToken}") - fun patchPassword(@PathVariable resetPasswordToken: String, @RequestBody newPassword: String) = - service.changePassword(resetPasswordToken, newPassword) + @PatchMapping("/resetPassword/{resetPasswordUUID}") + fun patchPassword(@PathVariable resetPasswordUUID: UUID, @RequestBody newPassword: String) = + service.changePassword(resetPasswordUUID, newPassword) } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 152e74970..b49436b99 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -4,6 +4,7 @@ import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.JavaMailSenderImpl import java.util.Properties +import java.util.UUID /** * This object contains every function needed to create and send emails. @@ -12,7 +13,7 @@ object EmailUtil { private const val emailAddressSender = "noreply@osoc.com" private const val passwordSender = "insert.password.here" - private fun getResetPasswordEmailBody(resetPasswordToken: String): String { + private fun getResetPasswordEmailBody(resetPasswordToken: UUID): String { val url = "http://localhost:3000/resetPassword/$resetPasswordToken" return """ Hello, @@ -35,7 +36,7 @@ object EmailUtil { return mailSender } - fun sendEmail(emailaddressReceiver: String, resetPasswordToken: String) { + fun sendEmail(emailaddressReceiver: String, resetPasswordToken: UUID) { val mailSender = getJavaMailSender() val email = SimpleMailMessage() diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index 9f276c037..b9bbe1fd9 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -1,27 +1,32 @@ package be.osoc.team1.backend.security -import org.springframework.security.crypto.password.PasswordEncoder +import java.security.MessageDigest import java.util.UUID object ResetPasswordUtil { - private val resetTokens: MutableMap = mutableMapOf() + private val resetTokens: MutableMap = mutableMapOf() - fun newToken(email: String, passwordEncoder: PasswordEncoder): UUID { + private val sha256: MessageDigest = MessageDigest.getInstance("SHA256") + + private fun hash(uuid: UUID): ByteArray { + return sha256.digest(uuid.toString().toByteArray()) + } + + fun newToken(email: String): UUID { val uuid: UUID = UUID.randomUUID() - val hashedUuid = passwordEncoder.encode(uuid.toString()) - resetTokens[hashedUuid] = ResetToken(email) - println("uuid: $uuid") - println("hashed: $hashedUuid") + val hashedUUID: ByteArray = hash(uuid) + resetTokens[hashedUUID] = ResetToken(email) return uuid } - private fun isTokenValid(hashedUuid: String): Boolean { - return (hashedUuid in resetTokens && !resetTokens[hashedUuid]!!.isExpired()) + private fun isTokenValid(hashedUUID: ByteArray): Boolean { + return (hashedUUID in resetTokens && !resetTokens[hashedUUID]!!.isExpired()) } - fun getEmailFromToken(hashedUuid: String): String? { - if (isTokenValid(hashedUuid)) { - return resetTokens[hashedUuid]!!.email + fun getEmailFromUUID(uuid: UUID): String? { + val hashedUUID = hash(uuid) + if (isTokenValid(hashedUUID)) { + return resetTokens[hashedUUID]!!.email } return null } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 09f15634e..6d020f6ac 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -8,7 +8,6 @@ import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.UserRepository import be.osoc.team1.backend.security.EmailUtil import be.osoc.team1.backend.security.ResetPasswordUtil -import be.osoc.team1.backend.security.TokenUtil import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull import org.springframework.security.crypto.password.PasswordEncoder @@ -91,17 +90,17 @@ class UserService(private val repository: UserRepository, private val passwordEn */ fun getResetPasswordTokenByMail(email: String) { if (repository.findByEmail(email) != null) { - val resetPasswordToken = ResetPasswordUtil.newToken(email, passwordEncoder) + val resetPasswordToken = ResetPasswordUtil.newToken(email) EmailUtil.sendEmail(email, resetPasswordToken) } } /** - * Get the email address from [resetPasswordToken] and set its password to [newPassword]. + * Get the email address from [resetPasswordUUID] and set its password to [newPassword]. */ - fun changePassword(resetPasswordToken: String, newPassword: String) { - val email = ResetPasswordUtil.getEmailFromToken(resetPasswordToken) - ?: throw InvalidTokenException("ResetPasswordToken is invalid.") + fun changePassword(resetPasswordUUID: UUID, newPassword: String) { + val email = ResetPasswordUtil.getEmailFromUUID(resetPasswordUUID) + ?: throw InvalidTokenException("resetPasswordUUID is invalid.") val user: User = repository.findByEmail(email) ?: throw InvalidTokenException("ResetPasswordToken contains invalid email.") user.password = passwordEncoder.encode(newPassword) From bf8983a11e40599b178238a746e0c9226682277c Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Fri, 6 May 2022 11:27:16 +0200 Subject: [PATCH 022/425] fix: reset password flow now fully works again --- .../be/osoc/team1/backend/controllers/UserController.kt | 2 +- .../be/osoc/team1/backend/security/ResetPasswordUtil.kt | 5 ++++- .../kotlin/be/osoc/team1/backend/services/UserService.kt | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index 86129d42c..b5e234b26 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -83,7 +83,7 @@ class UserController(private val service: UserService) { * Request a resetPasswordToken for [email]. */ @PostMapping("/resetPassword") - fun postEmail(@RequestBody email: String) = service.getResetPasswordTokenByMail(email) + fun postEmail(@RequestBody email: String) = service.sendEmailWithToken(email) /** * Reset password using [resetPasswordUUID]. diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index b9bbe1fd9..a7289305b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -1,10 +1,13 @@ package be.osoc.team1.backend.security import java.security.MessageDigest +import java.util.SortedMap import java.util.UUID object ResetPasswordUtil { - private val resetTokens: MutableMap = mutableMapOf() + private val resetTokens: SortedMap = sortedMapOf( + { a, b -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } + ) private val sha256: MessageDigest = MessageDigest.getInstance("SHA256") diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 6d020f6ac..f36974a55 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -88,10 +88,10 @@ class UserService(private val repository: UserRepository, private val passwordEn /** * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. */ - fun getResetPasswordTokenByMail(email: String) { + fun sendEmailWithToken(email: String) { if (repository.findByEmail(email) != null) { - val resetPasswordToken = ResetPasswordUtil.newToken(email) - EmailUtil.sendEmail(email, resetPasswordToken) + val resetPasswordUUID: UUID = ResetPasswordUtil.newToken(email) + EmailUtil.sendEmail(email, resetPasswordUUID) } } From e79d5f2a8c636237fb141bec148b1909622a0cf8 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Fri, 6 May 2022 11:28:34 +0200 Subject: [PATCH 023/425] test: fixing more errors on tests --- .../osoc/team1/backend/controllers/BaseController.kt | 10 ++++++++-- .../backend/unittests/CommunicationControllerTests.kt | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt index cc9f433b4..cf4efbc9f 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt @@ -20,8 +20,12 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import java.lang.reflect.Field +import java.lang.reflect.Modifier import java.util.* import javax.servlet.http.HttpServletRequest +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible abstract class BaseController(open val service: BaseService) { @Autowired @@ -31,14 +35,16 @@ abstract class BaseController(open val service: BaseService) { lateinit var userDetailService: OsocUserDetailService @Autowired - private lateinit var request: HttpServletRequest + lateinit var request: HttpServletRequest /** * Checks if this [entity] can be accessed by the requesting user. * If the edition of the [entity] isn't active and the requesting user isn't admin [attemptEditionAccess] will throw an [UnauthorizedOperationException] */ private fun attemptAccess(entity: T): T { - val editionName = entity::class.java.getDeclaredField("edition").get(entity) as String + val reflection = entity::class.java.getDeclaredField("edition") + reflection.isAccessible = true + val editionName = reflection.get(entity) as String attemptEditionAccess(editionName, editionService, userDetailService, request) return entity diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt index 89f98f473..8b3aed422 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt @@ -3,6 +3,7 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.CommunicationController import be.osoc.team1.backend.entities.Communication import be.osoc.team1.backend.entities.CommunicationTypeEnum +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.CommunicationService import be.osoc.team1.backend.services.EditionService @@ -16,6 +17,7 @@ import io.mockk.just import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType +import org.springframework.security.authentication.TestingAuthenticationToken import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @@ -48,7 +50,8 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { @Test fun `getCommunicationById returns communication if communication with given id exists`() { every { communicationService.getById(testId) } returns testCommunication - mockMvc.perform(get("$editionUrl/$testId")).andExpect(status().isOk) + every { editionService.getEdition(any()) } returns Edition(testEdition, true) + mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))).andExpect(status().isOk) .andExpect(content().json(jsonRepresentation)) } From bf9cdaa81aff0d35c61eb1f15aa0e5814f276feb Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Fri, 6 May 2022 11:35:20 +0200 Subject: [PATCH 024/425] fix: frontend improvements --- frontend/pages/login.tsx | 2 +- .../resetPassword/[resetPasswordToken].tsx | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 177a068cf..0756e96e4 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -120,7 +120,7 @@ const Login = () => { no account yet?
register here!

- +

password forgotten?

diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index ffe0446fe..c9135c73a 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -1,4 +1,5 @@ import type { NextPage } from 'next'; +import Link from 'next/link'; import { useRouter } from 'next/router'; import { FormEventHandler, useEffect, useRef, useState } from 'react'; import toast from 'react-hot-toast'; @@ -37,7 +38,16 @@ const ResetPassword: NextPage = () => { { duration: 12000 } ); } - console.log(response) + toast.success( + (t) => ( + + Success
+ Password has been reset
+ +
+ ), + { duration: 12000 } + ); } catch (err) { console.log(err); toast.error('An error occurred while trying to reset password.'); @@ -65,6 +75,11 @@ const ResetPassword: NextPage = () => { > Change password + +

+ Got back to login +

+ From cb240eb8826f5869c3ad4eda4e007161b8d150fa Mon Sep 17 00:00:00 2001 From: Michael M Date: Fri, 6 May 2022 11:59:18 +0200 Subject: [PATCH 025/425] chore: fix authentication for test --- .../unittests/CommunicationControllerTests.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt index 8b3aed422..1ad1d1c55 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt @@ -4,6 +4,8 @@ import be.osoc.team1.backend.controllers.CommunicationController import be.osoc.team1.backend.entities.Communication import be.osoc.team1.backend.entities.CommunicationTypeEnum import be.osoc.team1.backend.entities.Edition +import be.osoc.team1.backend.entities.Role +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.CommunicationService import be.osoc.team1.backend.services.EditionService @@ -14,10 +16,15 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.Runs import io.mockk.every import io.mockk.just +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @@ -39,6 +46,10 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { private lateinit var editionService: EditionService @MockkBean private lateinit var osocUserDetailService: OsocUserDetailService + @MockkBean + private lateinit var authentication: Authentication + @MockkBean + private lateinit var securityContext: SecurityContext private val testId = UUID.randomUUID() private val testEdition = "testEdition" @@ -47,10 +58,20 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { private val objectMapper = ObjectMapper() private val jsonRepresentation = objectMapper.writeValueAsString(testCommunication) + private val authenticatedAdmin = User("name", "email", Role.Admin, "password") + @BeforeEach + fun setup() { + SecurityContextHolder.setContext(securityContext) + every { securityContext.authentication } returns authentication + every { osocUserDetailService.getUserFromPrincipal(any()) } returns authenticatedAdmin + } + @Test fun `getCommunicationById returns communication if communication with given id exists`() { every { communicationService.getById(testId) } returns testCommunication every { editionService.getEdition(any()) } returns Edition(testEdition, true) + val resp = mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))) + println(resp) mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))).andExpect(status().isOk) .andExpect(content().json(jsonRepresentation)) } From 5f7b8687d44c8b0a48bcc37fcadab4a20d926540 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Fri, 6 May 2022 12:05:03 +0200 Subject: [PATCH 026/425] docs: document controller --- .../osoc/team1/backend/controllers/UserController.kt | 8 ++++++-- .../osoc/team1/backend/security/ResetPasswordUtil.kt | 8 ++++---- .../be/osoc/team1/backend/services/UserService.kt | 12 ++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt index b5e234b26..408dcb63c 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/UserController.kt @@ -80,10 +80,14 @@ class UserController(private val service: UserService) { service.changeRole(id, role) /** - * Request a resetPasswordToken for [email]. + * Request to reset the password of the user with given [emailAddress]. The link to actually reset the password is + * sent in an email to [emailAddress]. + * This request will always succeed, even when an invalid [emailAddress] is given. Otherwise, people with bad intent + * could track down which email addresses are linked to existing accounts. */ @PostMapping("/resetPassword") - fun postEmail(@RequestBody email: String) = service.sendEmailWithToken(email) + @ResponseStatus(value = HttpStatus.NO_CONTENT) + fun postEmail(@RequestBody emailAddress: String) = service.sendEmailWithToken(emailAddress) /** * Reset password using [resetPasswordUUID]. diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index a7289305b..81be5b11c 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -15,10 +15,10 @@ object ResetPasswordUtil { return sha256.digest(uuid.toString().toByteArray()) } - fun newToken(email: String): UUID { + fun newToken(emailAddress: String): UUID { val uuid: UUID = UUID.randomUUID() val hashedUUID: ByteArray = hash(uuid) - resetTokens[hashedUUID] = ResetToken(email) + resetTokens[hashedUUID] = ResetToken(emailAddress) return uuid } @@ -29,14 +29,14 @@ object ResetPasswordUtil { fun getEmailFromUUID(uuid: UUID): String? { val hashedUUID = hash(uuid) if (isTokenValid(hashedUUID)) { - return resetTokens[hashedUUID]!!.email + return resetTokens[hashedUUID]!!.emailAddress } return null } } data class ResetToken( - val email: String, + val emailAddress: String, val ttl: Long = System.currentTimeMillis() + 30 * 60 * 1000 ) { fun isExpired(): Boolean { diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index f36974a55..7123c17a0 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -88,10 +88,10 @@ class UserService(private val repository: UserRepository, private val passwordEn /** * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. */ - fun sendEmailWithToken(email: String) { - if (repository.findByEmail(email) != null) { - val resetPasswordUUID: UUID = ResetPasswordUtil.newToken(email) - EmailUtil.sendEmail(email, resetPasswordUUID) + fun sendEmailWithToken(emailAddress: String) { + if (repository.findByEmail(emailAddress) != null) { + val resetPasswordUUID: UUID = ResetPasswordUtil.newToken(emailAddress) + EmailUtil.sendEmail(emailAddress, resetPasswordUUID) } } @@ -99,9 +99,9 @@ class UserService(private val repository: UserRepository, private val passwordEn * Get the email address from [resetPasswordUUID] and set its password to [newPassword]. */ fun changePassword(resetPasswordUUID: UUID, newPassword: String) { - val email = ResetPasswordUtil.getEmailFromUUID(resetPasswordUUID) + val emailAddress = ResetPasswordUtil.getEmailFromUUID(resetPasswordUUID) ?: throw InvalidTokenException("resetPasswordUUID is invalid.") - val user: User = repository.findByEmail(email) + val user: User = repository.findByEmail(emailAddress) ?: throw InvalidTokenException("ResetPasswordToken contains invalid email.") user.password = passwordEncoder.encode(newPassword) repository.save(user) From ba21a66d1e60fd2e6640dd1d0758343a9c494e33 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Fri, 6 May 2022 12:28:51 +0200 Subject: [PATCH 027/425] refactor: isolate reset password token --- .../be/osoc/team1/backend/security/EmailUtil.kt | 2 +- .../team1/backend/security/ResetPasswordToken.kt | 10 ++++++++++ .../team1/backend/security/ResetPasswordUtil.kt | 13 ++----------- .../be/osoc/team1/backend/security/TokenUtil.kt | 15 --------------- .../be/osoc/team1/backend/services/UserService.kt | 4 ++-- 5 files changed, 15 insertions(+), 29 deletions(-) create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index b49436b99..aa0160701 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -7,7 +7,7 @@ import java.util.Properties import java.util.UUID /** - * This object contains every function needed to create and send emails. + * This object contains every function needed to make and send emails. */ object EmailUtil { private const val emailAddressSender = "noreply@osoc.com" diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt new file mode 100644 index 000000000..2618090ba --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt @@ -0,0 +1,10 @@ +package be.osoc.team1.backend.security + +data class ResetPasswordToken( + val emailAddress: String, + val ttl: Long = System.currentTimeMillis() + 20 * 60 * 1000 // 20 minutes +) { + fun isExpired(): Boolean { + return ttl < System.currentTimeMillis() + } +} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index 81be5b11c..7a854049d 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -5,7 +5,7 @@ import java.util.SortedMap import java.util.UUID object ResetPasswordUtil { - private val resetTokens: SortedMap = sortedMapOf( + private val resetTokens: SortedMap = sortedMapOf( { a, b -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } ) @@ -18,7 +18,7 @@ object ResetPasswordUtil { fun newToken(emailAddress: String): UUID { val uuid: UUID = UUID.randomUUID() val hashedUUID: ByteArray = hash(uuid) - resetTokens[hashedUUID] = ResetToken(emailAddress) + resetTokens[hashedUUID] = ResetPasswordToken(emailAddress) return uuid } @@ -34,12 +34,3 @@ object ResetPasswordUtil { return null } } - -data class ResetToken( - val emailAddress: String, - val ttl: Long = System.currentTimeMillis() + 30 * 60 * 1000 -) { - fun isExpired(): Boolean { - return ttl < System.currentTimeMillis() - } -} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt index fedda4db6..625925b15 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt @@ -62,21 +62,6 @@ object TokenUtil { .sign(hashingAlgorithm) } - /** - * Create a JSON web token. The token contains email and expiration date of token. resetPasswordTokens are valid for - * 15 minutes. - * The created token gets signed using above hashing algorithm and secret. - */ - fun createResetPasswordToken(email: String): String { - return JWT.create() - .withSubject(email) - .withJWTId(nextInt().toString()) - .withExpiresAt( - Date(System.currentTimeMillis() + 15 * 60 * 1000) - ) - .sign(hashingAlgorithm) - } - /** * Extract access token from request header. return null when there is no access token given. */ diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 7123c17a0..33686b408 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -86,7 +86,7 @@ class UserService(private val repository: UserRepository, private val passwordEn } /** - * Send an email with a resetPasswordToken to [email] if [email] is the email address of an existing user. + * Email [emailAddress] a link to reset their password, if [emailAddress] is linked to an existing user. */ fun sendEmailWithToken(emailAddress: String) { if (repository.findByEmail(emailAddress) != null) { @@ -96,7 +96,7 @@ class UserService(private val repository: UserRepository, private val passwordEn } /** - * Get the email address from [resetPasswordUUID] and set its password to [newPassword]. + * Extract the email address from [resetPasswordUUID] and set the password of that user to [newPassword]. */ fun changePassword(resetPasswordUUID: UUID, newPassword: String) { val emailAddress = ResetPasswordUtil.getEmailFromUUID(resetPasswordUUID) From 54681fc85ffade958384224bb1472dd8cbd3e90b Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 6 May 2022 18:12:32 +0200 Subject: [PATCH 028/425] fix: fix abort exception sending an error on an already committed response "If the response has already been committed, this method throws an IllegalStateException. After using this method, the response should be considered to be committed and should not be written to." --- .../be/osoc/team1/backend/security/AuthorizationFilter.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthorizationFilter.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthorizationFilter.kt index 8c0c4d19c..d1f82c30b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthorizationFilter.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthorizationFilter.kt @@ -4,6 +4,7 @@ import be.osoc.team1.backend.security.TokenUtil.authenticateWithAccessToken import be.osoc.team1.backend.security.TokenUtil.decodeAndVerifyToken import be.osoc.team1.backend.security.TokenUtil.getAccessTokenFromRequest import com.fasterxml.jackson.databind.ObjectMapper +import org.apache.catalina.connector.ClientAbortException import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.web.filter.OncePerRequestFilter @@ -39,7 +40,10 @@ class AuthorizationFilter : OncePerRequestFilter() { } filterChain.doFilter(request, response) } catch (exception: Exception) { - respondException(response, exception) + when (exception) { + is ClientAbortException -> {} + else -> respondException(response, exception) + } } } From 954ddd77b80d0d3f554c152ba0a51b73815ac80d Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Sat, 7 May 2022 16:04:18 +0200 Subject: [PATCH 029/425] docs: add docs all around --- .../osoc/team1/backend/security/EmailUtil.kt | 41 +++++++++++++------ .../backend/security/ResetPasswordToken.kt | 4 ++ .../backend/security/ResetPasswordUtil.kt | 29 +++++++++++-- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index aa0160701..c6b54431c 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -10,19 +10,33 @@ import java.util.UUID * This object contains every function needed to make and send emails. */ object EmailUtil { + /** + * Set email account to send emails with. + */ private const val emailAddressSender = "noreply@osoc.com" private const val passwordSender = "insert.password.here" - private fun getResetPasswordEmailBody(resetPasswordToken: UUID): String { - val url = "http://localhost:3000/resetPassword/$resetPasswordToken" + /** + * Make the body of the email users receive when they request a password change. + */ + private fun getResetPasswordEmailBody(resetPasswordUUID: UUID): String { + val url = "http://localhost:3000/resetPassword/$resetPasswordUUID" return """ - Hello, - Use the link below to set your new password. - $url + Hi, + + Trouble signing in? + Resetting your password is easy. + Use the link below to choose your new password. + $url + + If you did not forget your password, please disregard this email. """.trimIndent() } - private fun getJavaMailSender(): JavaMailSender { + /** + * Get a [JavaMailSender] object which is correctly configured. + */ + private fun getMailSender(): JavaMailSender { val mailSender = JavaMailSenderImpl() mailSender.host = "smtp.gmail.com" mailSender.port = 587 @@ -36,19 +50,22 @@ object EmailUtil { return mailSender } - fun sendEmail(emailaddressReceiver: String, resetPasswordToken: UUID) { - val mailSender = getJavaMailSender() + /** + * Email [emailAddressReceiver] with a [resetPasswordUUID], so [emailAddressReceiver] can reset its email. + */ + fun sendEmail(emailAddressReceiver: String, resetPasswordUUID: UUID) { + // val mailSender = getMailSender() val email = SimpleMailMessage() email.setSubject("Reset Password") - email.setText(getResetPasswordEmailBody(resetPasswordToken)) - email.setTo(emailaddressReceiver) + email.setText(getResetPasswordEmailBody(resetPasswordUUID)) + email.setTo(emailAddressReceiver) email.setFrom(emailAddressSender) println(">>>>>>>") - println("To: $emailaddressReceiver") + println("To: $emailAddressReceiver") println("From: $emailAddressSender") - println(email.subject) + println("> ${email.subject}") println(email.text) println(">>>>>>>") // mailSender.send(email) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt index 2618090ba..ec93d8db4 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt @@ -1,5 +1,9 @@ package be.osoc.team1.backend.security +/** + * This class is used in [ResetPasswordUtil]. An instance of this class gets created when a user requests to change its + * password. This user can change its password as long as the token hasn't expired (20 minutes after creation). + */ data class ResetPasswordToken( val emailAddress: String, val ttl: Long = System.currentTimeMillis() + 20 * 60 * 1000 // 20 minutes diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index 7a854049d..c0fa073a2 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -4,17 +4,32 @@ import java.security.MessageDigest import java.util.SortedMap import java.util.UUID +/** + * This object contains every function needed to manage password reset requests. + */ object ResetPasswordUtil { + /** + * This map holds a [ResetPasswordToken] per ... + */ private val resetTokens: SortedMap = sortedMapOf( { a, b -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } ) + /** + * Init object that can hash using the SHA-256 hash function. + */ private val sha256: MessageDigest = MessageDigest.getInstance("SHA256") - private fun hash(uuid: UUID): ByteArray { - return sha256.digest(uuid.toString().toByteArray()) + /** + * Hash [resetPasswordUUID] with the SHA-256 hash function. + */ + private fun hash(resetPasswordUUID: UUID): ByteArray { + return sha256.digest(resetPasswordUUID.toString().toByteArray()) } + /** + * Create a [ResetPasswordToken] for [emailAddress]. + */ fun newToken(emailAddress: String): UUID { val uuid: UUID = UUID.randomUUID() val hashedUUID: ByteArray = hash(uuid) @@ -22,12 +37,18 @@ object ResetPasswordUtil { return uuid } + /** + * Check whether resetPasswordToken linked to [hashedUUID] is valid and hasn't expired yet. + */ private fun isTokenValid(hashedUUID: ByteArray): Boolean { return (hashedUUID in resetTokens && !resetTokens[hashedUUID]!!.isExpired()) } - fun getEmailFromUUID(uuid: UUID): String? { - val hashedUUID = hash(uuid) + /** + * Get which email address requested given [resetPasswordUUID]. + */ + fun getEmailFromUUID(resetPasswordUUID: UUID): String? { + val hashedUUID = hash(resetPasswordUUID) if (isTokenValid(hashedUUID)) { return resetTokens[hashedUUID]!!.emailAddress } From 9428eed14ea8c7f4b87a62c9b293d95cb1876265 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 8 May 2022 16:25:47 +0200 Subject: [PATCH 030/425] fix: bug in controllerutil --- .../backend/controllers/ControllersUtil.kt | 64 +++---------------- 1 file changed, 9 insertions(+), 55 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index 2b0e02331..29e939f2b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -45,6 +45,10 @@ fun getObjectCreatedResponse( .body(createdObject) } +/** + * This function checks if a request is allowed based on the active edition, + * entries of inactive editions can only be viewed or deleted by admins + */ fun attemptEditionAccess( editionName: String, editionService: EditionService, @@ -57,63 +61,13 @@ fun attemptEditionAccess( if (!edition.accessibleBy(user)) throw UnauthorizedOperationException("Entries of inactive editions can only be accessed by admins!") - - if (httpServletRequest.method != "GET" && httpServletRequest.method != "DELETE") + if (!edition.isActive && httpServletRequest.method != "GET" && httpServletRequest.method != "DELETE") throw ForbiddenOperationException("Entries of inactive editions can only be viewed or delete (Allowed methods: GET, DELETE)") } -//@Component -//class EditionInterceptor(val editionService: EditionService) : -// HandlerInterceptor { -// @Throws(Exception::class) -// @Override -// override fun preHandle( -// request: HttpServletRequest, -// response: HttpServletResponse, -// handler: Any -// ): Boolean { -// return super.preHandle(request, response, handler) -// // URLs that are always allowed -// val regex = -// Regex("^.*/api/(error|editions|login|communications|users|assignments|positions|statusSuggestions|answers|skills|logout|token).*$") -// // this !is check is here so invalid requests (such as GETs to endpoints that don't exist) still get handled regularly -// if (!regex.matches(request.requestURI) && handler !is ResourceHttpRequestHandler) { -// val roles = SecurityContextHolder.getContext().authentication.authorities.map { it.toString() } -// SecurityContextHolder.getContext().authentication as Principal -// val editionName = request.requestURI.split("/")[2] -// if (editionService.getActiveEdition()?.name != editionName) { -// if (!roles.contains("ROLE_ADMIN")) { -// response.sendError(401, "Inactive editions can only be accessed by admins") -// return false -// } -// if (request.method != "GET" && request.method != "DELETE") { -// response.sendError(405, "Entries from inactive editions can only be viewed or deleted") -// return false -// } -// } -// } -// return super.preHandle(request, response, handler) -// } -// -// @Throws(Exception::class) -// override fun postHandle( -// request: HttpServletRequest, -// response: HttpServletResponse, handler: Any, -// modelAndView: ModelAndView? -// ) { -// super.postHandle(request, response, handler, modelAndView) -// } -//} -// -//@Component -//class InterceptorConfig : WebMvcConfigurer { -// @Autowired -// lateinit var editionInterceptor: EditionInterceptor -// override fun addInterceptors(registry: InterceptorRegistry) { -// registry.addInterceptor(editionInterceptor) -// } -//} - +/** + * This class adds the code of the SecuredEdition annotation + */ @Aspect @Component class EditionSecurityAspect(val editionService: EditionService, val userDetailService: OsocUserDetailService) { @@ -139,5 +93,5 @@ class EditionSecurityAspect(val editionService: EditionService, val userDetailSe @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented -@Inherited //Doesn't work in kotlin... +@Inherited annotation class SecuredEdition From 76ec9f7e881ca505507f4f4086e7ed351afd7b98 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 8 May 2022 16:47:14 +0200 Subject: [PATCH 031/425] test: fixing tests --- .../unittests/AssignmentControllerTests.kt | 14 ++++- .../unittests/CommunicationControllerTests.kt | 61 +++++++++++++++++-- .../unittests/PositionControllerTests.kt | 15 +++++ .../StatusSuggestionControllerTests.kt | 15 +++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt index fce2c788c..bb0f2df29 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/AssignmentControllerTests.kt @@ -2,6 +2,7 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.AssignmentController import be.osoc.team1.backend.entities.Assignment +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.entities.Position import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.Skill @@ -22,6 +23,9 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content @@ -41,6 +45,10 @@ class AssignmentControllerTests(@Autowired private val mockMvc: MockMvc) { private lateinit var editionService: EditionService @MockkBean private lateinit var osocUserDetailService: OsocUserDetailService + @MockkBean + private lateinit var authentication: Authentication + @MockkBean + private lateinit var securityContext: SecurityContext private val testId = UUID.randomUUID() private val testStudent = Student("Jitse", "Willaert", "testEdition") @@ -50,10 +58,13 @@ class AssignmentControllerTests(@Autowired private val mockMvc: MockMvc) { private val testAssignment = Assignment(testStudent, testPosition, testSuggester, "reason") private val objectMapper = ObjectMapper() + private val authenticatedAdmin = User("name", "email", Role.Admin, "password") @BeforeEach fun beforeEach() { RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest())) - + SecurityContextHolder.setContext(securityContext) + every { securityContext.authentication } returns authentication + every { osocUserDetailService.getUserFromPrincipal(any()) } returns authenticatedAdmin val simpleModule = SimpleModule() simpleModule.addSerializer(Position::class.java, PositionSerializer()) simpleModule.addSerializer(Student::class.java, StudentSerializer()) @@ -65,6 +76,7 @@ class AssignmentControllerTests(@Autowired private val mockMvc: MockMvc) { fun `getAssignmentById returns assignment if assignment with given id exists`() { val jsonRepresentation = objectMapper.writeValueAsString(testAssignment) every { assignmentService.getById(testId) } returns testAssignment + every { editionService.getEdition(any()) } returns Edition("edition", true) mockMvc.perform(get("/assignments/$testId")).andExpect(status().isOk) .andExpect(content().json(jsonRepresentation)) } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt index 1ad1d1c55..a4ff6a980 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt @@ -16,7 +16,6 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.Runs import io.mockk.every import io.mockk.just -import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -30,7 +29,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import java.util.UUID +import java.util.* @UnsecuredWebMvcTest(CommunicationController::class) class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { @@ -44,10 +43,13 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { // These MockkBean are necessary because the BaseController uses these under the hood @MockkBean private lateinit var editionService: EditionService + @MockkBean private lateinit var osocUserDetailService: OsocUserDetailService + @MockkBean private lateinit var authentication: Authentication + @MockkBean private lateinit var securityContext: SecurityContext @@ -59,6 +61,7 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { private val jsonRepresentation = objectMapper.writeValueAsString(testCommunication) private val authenticatedAdmin = User("name", "email", Role.Admin, "password") + @BeforeEach fun setup() { SecurityContextHolder.setContext(securityContext) @@ -70,9 +73,8 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { fun `getCommunicationById returns communication if communication with given id exists`() { every { communicationService.getById(testId) } returns testCommunication every { editionService.getEdition(any()) } returns Edition(testEdition, true) - val resp = mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))) - println(resp) - mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))).andExpect(status().isOk) + mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))) + .andExpect(status().isOk) .andExpect(content().json(jsonRepresentation)) } @@ -106,4 +108,53 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { .content(jsonRepresentation) ).andExpect(status().isNotFound) } + + @Test + fun `Inactive editions can be accessed by admins`() { + every { communicationService.getById(testId) } returns testCommunication + every { editionService.getEdition(any()) } returns Edition(testEdition, false) + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User( + "name", + "email", + Role.Admin, + "password" + ) + mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))) + .andExpect(status().isOk) + } + + @Test + fun `Inactive editions cannot be accessed by others`() { + every { communicationService.getById(testId) } returns testCommunication + every { editionService.getEdition(any()) } returns Edition(testEdition, false) + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User( + "name", + "email", + Role.Coach, + "password" + ) + mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))) + .andExpect(status().isUnauthorized) + } + + @Test + fun `Gets and deletes are the only allowed HTTP methods on inactive editions`() { + every { communicationService.getById(testId) } returns testCommunication + every { communicationService.createCommunication(any()) } returns testCommunication + every { editionService.getEdition(any()) } returns Edition(testEdition, false) + every { studentService.addCommunicationToStudent(testId, any(), testEdition) } just Runs + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User( + "name", + "email", + Role.Admin, + "password" + ) + mockMvc.perform(get("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null))) + .andExpect(status().isOk) + mockMvc.perform( + post("$editionUrl/$testId").principal(TestingAuthenticationToken(null, null)) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRepresentation) + ).andExpect(status().isForbidden) + } } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt index f7be13e4a..0befe2012 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PositionControllerTests.kt @@ -1,8 +1,11 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.PositionController +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.entities.Position +import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.Skill +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService @@ -14,6 +17,9 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content @@ -33,21 +39,30 @@ class PositionControllerTests(@Autowired private val mockMvc: MockMvc) { private lateinit var editionService: EditionService @MockkBean private lateinit var osocUserDetailService: OsocUserDetailService + @MockkBean + private lateinit var authentication: Authentication + @MockkBean + private lateinit var securityContext: SecurityContext private val testId = UUID.randomUUID() private val testSkill = Skill("Test") private val testPosition = Position(testSkill, 2) private val objectMapper = ObjectMapper() + private val authenticatedAdmin = User("name", "email", Role.Admin, "password") @BeforeEach fun beforeEach() { RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest())) + SecurityContextHolder.setContext(securityContext) + every { securityContext.authentication } returns authentication + every { osocUserDetailService.getUserFromPrincipal(any()) } returns authenticatedAdmin } @Test fun `getPositionById returns position if position with given id exists`() { val jsonRepresentation = objectMapper.writeValueAsString(testPosition) every { positionService.getById(testId) } returns testPosition + every { editionService.getEdition(any()) } returns Edition("edition", true) mockMvc.perform(get("/positions/$testId")).andExpect(status().isOk) .andExpect(content().json(jsonRepresentation)) } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt index fee02beae..40186b12e 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt @@ -1,8 +1,11 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.StatusSuggestionController +import be.osoc.team1.backend.entities.Edition +import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.entities.SuggestionEnum +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService @@ -14,6 +17,9 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content @@ -33,21 +39,30 @@ class StatusSuggestionControllerTests(@Autowired private val mockMvc: MockMvc) { private lateinit var editionService: EditionService @MockkBean private lateinit var osocUserDetailService: OsocUserDetailService + @MockkBean + private lateinit var authentication: Authentication + @MockkBean + private lateinit var securityContext: SecurityContext private val testId = UUID.randomUUID() private val coachId = UUID.randomUUID() private val testStatusSuggestion = StatusSuggestion(coachId, SuggestionEnum.Yes, "motivation") private val objectMapper = ObjectMapper() + private val authenticatedAdmin = User("name", "email", Role.Admin, "password") @BeforeEach fun beforeEach() { RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest())) + SecurityContextHolder.setContext(securityContext) + every { securityContext.authentication } returns authentication + every { osocUserDetailService.getUserFromPrincipal(any()) } returns authenticatedAdmin } @Test fun `getStatusSuggestionById returns statusSuggestion if statusSuggestion with given id exists`() { val jsonRepresentation = objectMapper.writeValueAsString(testStatusSuggestion) every { statusSuggestionService.getById(testId) } returns testStatusSuggestion + every { editionService.getEdition(any()) } returns Edition("edition", true) mockMvc.perform(get("/statusSuggestions/$testId")).andExpect(status().isOk) .andExpect(content().json(jsonRepresentation)) } From af6c7b6ba5b9d29c72cafdfb59e5aa8cc834a8dd Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 8 May 2022 16:47:34 +0200 Subject: [PATCH 032/425] chore: adding sercurededition annotation --- .../be/osoc/team1/backend/controllers/CommunicationController.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/CommunicationController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/CommunicationController.kt index fbfd21769..f111675e4 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/CommunicationController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/CommunicationController.kt @@ -38,6 +38,7 @@ class CommunicationController( @PostMapping("/{studentId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_COACH") + @SecuredEdition fun createCommunication( @PathVariable studentId: UUID, @PathVariable edition: String, From cd2c89e8043918ee6d7f4c93ffbf1f315726c15b Mon Sep 17 00:00:00 2001 From: Michael M Date: Sun, 8 May 2022 18:35:32 +0200 Subject: [PATCH 033/425] chore: add correct annotation loading for EditionSecurityAspect --- .../team1/backend/unittests/CommunicationControllerTests.kt | 2 ++ .../be/osoc/team1/backend/unittests/UnsecuredWebMvcTest.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt index a4ff6a980..ca0e82f65 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt @@ -61,12 +61,14 @@ class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { private val jsonRepresentation = objectMapper.writeValueAsString(testCommunication) private val authenticatedAdmin = User("name", "email", Role.Admin, "password") + private val activeEdition = Edition("activeEdition", true) @BeforeEach fun setup() { SecurityContextHolder.setContext(securityContext) every { securityContext.authentication } returns authentication every { osocUserDetailService.getUserFromPrincipal(any()) } returns authenticatedAdmin + every { editionService.getEdition(any()) } returns activeEdition } @Test diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UnsecuredWebMvcTest.java b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UnsecuredWebMvcTest.java index bf24af97a..01c39ebc3 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UnsecuredWebMvcTest.java +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UnsecuredWebMvcTest.java @@ -1,12 +1,15 @@ package be.osoc.team1.backend.unittests; +import be.osoc.team1.backend.controllers.EditionSecurityAspect; import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; @@ -16,6 +19,8 @@ // Taken from https://stackoverflow.com/a/65504089/15516306 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) +@EnableAspectJAutoProxy +@Import(EditionSecurityAspect.class) @WebMvcTest(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSecurityConfigurer.class)}, excludeAutoConfiguration = {SecurityAutoConfiguration.class, SecurityFilterAutoConfiguration.class, From aa4acf522d2ce0ce5d51540b6f5b3b78f1fa5fb8 Mon Sep 17 00:00:00 2001 From: Michael M Date: Sun, 8 May 2022 19:02:34 +0200 Subject: [PATCH 034/425] chore: fix unittests --- .../unittests/CommunicationControllerTests.kt | 2 +- .../unittests/EditionControllerTests.kt | 13 +++++++++++ .../backend/unittests/PasswordEncoderTests.kt | 20 +++++++++++++++++ .../unittests/ProjectControllerTests.kt | 22 +++++++++++++++++++ .../unittests/StudentControllerTests.kt | 17 ++++++++++++++ .../backend/unittests/TokenControllerTests.kt | 18 +++++++++++++++ .../backend/unittests/UserControllerTests.kt | 16 ++++++++++++++ 7 files changed, 107 insertions(+), 1 deletion(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt index ca0e82f65..684520b4a 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/CommunicationControllerTests.kt @@ -29,7 +29,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import java.util.* +import java.util.UUID @UnsecuredWebMvcTest(CommunicationController::class) class CommunicationControllerTests(@Autowired private val mockMvc: MockMvc) { diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/EditionControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/EditionControllerTests.kt index 2a3541893..4a49c281a 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/EditionControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/EditionControllerTests.kt @@ -2,14 +2,18 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.EditionController import be.osoc.team1.backend.entities.Edition +import be.osoc.team1.backend.entities.Role +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.FailedOperationException import be.osoc.team1.backend.exceptions.ForbiddenOperationException import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import com.ninjasquad.springmockk.MockkBean import io.mockk.Runs import io.mockk.every import io.mockk.just +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -26,8 +30,17 @@ class EditionControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var editionService: EditionService + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + private val editionName = "testEdition" private val editionUrl = "/editions/$editionName" + private val authenticatedAdmin = User("name", "email", Role.Admin, "password") + + @BeforeEach + fun setup() { + every { osocUserDetailService.getUserFromPrincipal(any()) } returns authenticatedAdmin + } @Test fun `createInactiveEdition returns created edition when it succeeds`() { diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt index c42ed2d2f..cfe1d8391 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt @@ -1,6 +1,14 @@ package be.osoc.team1.backend.unittests +import be.osoc.team1.backend.entities.Edition +import be.osoc.team1.backend.entities.Role +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.security.PasswordEncoderConfig +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService +import com.ninjasquad.springmockk.MockkBean +import io.mockk.every +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @@ -11,6 +19,18 @@ class PasswordEncoderTests { @Autowired private lateinit var passwordEncoderConfig: PasswordEncoderConfig + @MockkBean + private lateinit var editionService: EditionService + + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + + @BeforeEach + fun beforeEach() { + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User("", "", Role.Admin, "") + every { editionService.getEdition(any()) } returns Edition("", true) + } + @Test fun `encoding password should take around 1 second`() { val encoder = passwordEncoderConfig.passwordEncoder() diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectControllerTests.kt index af3edfb4e..a72018ea7 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectControllerTests.kt @@ -2,6 +2,7 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.ProjectController import be.osoc.team1.backend.entities.Assignment +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.entities.Position import be.osoc.team1.backend.entities.Project import be.osoc.team1.backend.entities.Role @@ -12,6 +13,8 @@ import be.osoc.team1.backend.exceptions.FailedOperationException import be.osoc.team1.backend.exceptions.ForbiddenOperationException import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.repositories.AssignmentRepository +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.PagedCollection import be.osoc.team1.backend.services.ProjectService import be.osoc.team1.backend.util.Serializer @@ -27,6 +30,9 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.repository.findByIdOrNull import org.springframework.http.MediaType import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -47,6 +53,18 @@ class ProjectControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var assignmentRepository: AssignmentRepository + @MockkBean + private lateinit var editionService: EditionService + + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + + @MockkBean + private lateinit var authentication: Authentication + + @MockkBean + private lateinit var securityContext: SecurityContext + private val testId = UUID.randomUUID() private val testEdition = "testEdition" private val editionUrl = "/$testEdition/projects" @@ -56,6 +74,10 @@ class ProjectControllerTests(@Autowired private val mockMvc: MockMvc) { @BeforeEach fun beforeEach() { RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest())) + SecurityContextHolder.setContext(securityContext) + every { securityContext.authentication } returns authentication + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User("", "", Role.Admin, "") + every { editionService.getEdition(any()) } returns Edition("", true) } @Test diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt index 779e790bc..59b7a6982 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt @@ -2,6 +2,7 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.StudentController import be.osoc.team1.backend.entities.Assignment +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.entities.Position import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.Skill @@ -17,6 +18,7 @@ import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.exceptions.InvalidStudentIdException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.AssignmentRepository +import be.osoc.team1.backend.services.EditionService import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.PagedCollection import be.osoc.team1.backend.services.StudentService @@ -38,6 +40,9 @@ import org.springframework.data.domain.Sort import org.springframework.http.MediaType import org.springframework.mock.web.MockHttpServletRequest import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -63,6 +68,15 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var assignmentRepository: AssignmentRepository + @MockkBean + private lateinit var editionService: EditionService + + @MockkBean + private lateinit var authentication: Authentication + + @MockkBean + private lateinit var securityContext: SecurityContext + private val studentId = UUID.randomUUID() private val testCoach = User("coach", "email", Role.Coach, "password") private val coachId = testCoach.id @@ -77,7 +91,10 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { @BeforeEach fun beforeEach() { RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest())) + SecurityContextHolder.setContext(securityContext) + every { securityContext.authentication } returns authentication every { userDetailService.getUserFromPrincipal(any()) } returns testCoach + every { editionService.getEdition(any()) } returns Edition("", true) } @Test diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenControllerTests.kt index a27f46c1f..5ee865ad5 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenControllerTests.kt @@ -1,9 +1,15 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.TokenController +import be.osoc.team1.backend.entities.Edition +import be.osoc.team1.backend.entities.Role +import be.osoc.team1.backend.entities.User +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.TokenService import com.ninjasquad.springmockk.MockkBean import io.mockk.every +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.web.servlet.MockMvc @@ -15,6 +21,18 @@ class TokenControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var tokenService: TokenService + @MockkBean + private lateinit var editionService: EditionService + + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + + @BeforeEach + fun beforeEach() { + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User("", "", Role.Admin, "") + every { editionService.getEdition(any()) } returns Edition("", true) + } + @Test fun `renewAccessToken should not fail`() { every { tokenService.renewAccessToken(any(), any()) } returns Unit diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UserControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UserControllerTests.kt index 791d7c415..4572f1753 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UserControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/UserControllerTests.kt @@ -1,16 +1,20 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.UserController +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.security.PasswordEncoderConfig +import be.osoc.team1.backend.services.EditionService +import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.UserService import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean import io.mockk.Runs import io.mockk.every import io.mockk.just +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -32,10 +36,22 @@ class UserControllerTests(@Autowired val mockMvc: MockMvc) { @MockkBean private lateinit var userService: UserService + @MockkBean + private lateinit var editionService: EditionService + + @MockkBean + private lateinit var osocUserDetailService: OsocUserDetailService + private val testUser = User("Test", "test@email.com", Role.Admin, "password") private val testId = testUser.id private val testUserJsonRepresentation = ObjectMapper().writeValueAsString(testUser) + @BeforeEach + fun beforeEach() { + every { osocUserDetailService.getUserFromPrincipal(any()) } returns User("", "", Role.Admin, "") + every { editionService.getEdition(any()) } returns Edition("", true) + } + @Test fun `getAllUsers should not fail`() { every { userService.getAllUsers() } returns emptyList() From 3359fc4b9e210f77da0b1e2d6fc89f16e3f84410 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 9 May 2022 09:22:38 +0200 Subject: [PATCH 035/425] docs: fixed communication endpoint --- docs/osoc.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/osoc.yaml b/docs/osoc.yaml index 54c1758b6..0c5c65e3c 100644 --- a/docs/osoc.yaml +++ b/docs/osoc.yaml @@ -849,7 +849,7 @@ paths: student: "https://example.com/api/students/abb97568-ac54-11ec-b909-0242ac120002" projects: [ "https://example.com/api/projects/afc1e1cc-ac54-11ec-b909-0242ac120002", "https://example.com/api/projects/b6a81d12-ac54-11ec-b909-0242ac120002" ] - /communications/{communicationId}: + /{edition}/communications/{communicationId}: get: summary: Get Communications by id description: | @@ -863,6 +863,11 @@ paths: schema: type: string format: uuid + - in: path + required: true + name: edition + schema: + type: string responses: '200': description: | @@ -875,7 +880,7 @@ paths: '404': description: | The response if there is no communication in the database with the given id. - /communications/{studentId}: + /{edition}/communications/{studentId}: post: summary: Add a new communication with a student description: | @@ -888,6 +893,11 @@ paths: schema: type: string format: uuid + - in: path + required: true + name: edition + schema: + type: string requestBody: content: application/json: From a40f013eb6ef9b542db8ad0f33f49d03eadea8cb Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 9 May 2022 09:23:05 +0200 Subject: [PATCH 036/425] chore: added SecuredEdition annotation --- .../osoc/team1/backend/controllers/ProjectController.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt index 96149973f..c4270ad46 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ProjectController.kt @@ -108,6 +108,7 @@ class ProjectController(private val service: ProjectService) { */ @PatchMapping("/{projectId}") @Secured("ROLE_ADMIN") + @SecuredEdition fun patchProject( @PathVariable projectId: UUID, @PathVariable edition: String, @@ -126,6 +127,7 @@ class ProjectController(private val service: ProjectService) { */ @GetMapping("/{projectId}/students") @Secured("ROLE_COACH") + @SecuredEdition fun getStudentsOfProject(@PathVariable projectId: UUID, @PathVariable edition: String): Collection = service.getStudents(projectId, edition) @@ -135,6 +137,7 @@ class ProjectController(private val service: ProjectService) { */ @GetMapping("/{projectId}/coaches") @Secured("ROLE_COACH") + @SecuredEdition fun getCoachesOfProject(@PathVariable projectId: UUID, @PathVariable edition: String): Collection = service.getProjectById(projectId, edition).coaches @@ -145,6 +148,7 @@ class ProjectController(private val service: ProjectService) { @PostMapping("/{projectId}/coaches") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") + @SecuredEdition fun postCoachToProject(@PathVariable projectId: UUID, @RequestBody coachId: UUID, @PathVariable edition: String) = service.addCoachToProject(projectId, coachId, edition) @@ -156,6 +160,7 @@ class ProjectController(private val service: ProjectService) { @DeleteMapping("/{projectId}/coaches/{coachId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") + @SecuredEdition fun deleteCoachFromProject(@PathVariable projectId: UUID, @PathVariable coachId: UUID, @PathVariable edition: String) = service.removeCoachFromProject(projectId, coachId, edition) @@ -176,6 +181,7 @@ class ProjectController(private val service: ProjectService) { */ @GetMapping("/conflicts") @Secured("ROLE_COACH") + @SecuredEdition fun getProjectConflicts(@PathVariable edition: String): MutableList = service.getConflicts(edition) @@ -194,6 +200,7 @@ class ProjectController(private val service: ProjectService) { */ @PostMapping("/{projectId}/assignments") @Secured("ROLE_COACH") + @SecuredEdition fun postAssignment( @PathVariable projectId: UUID, @RequestBody assignment: ProjectService.AssignmentPost, @@ -209,6 +216,7 @@ class ProjectController(private val service: ProjectService) { @DeleteMapping("/{projectId}/assignments/{assignmentId}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_COACH") + @SecuredEdition fun deleteAssignment(@PathVariable projectId: UUID, @PathVariable assignmentId: UUID, @PathVariable edition: String) = service.deleteAssignment(projectId, assignmentId, edition) } From aca150dcd9bc689f7f85a396db0f0fb07821a92f Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 9 May 2022 09:35:37 +0200 Subject: [PATCH 037/425] docs: small doc update --- .../be/osoc/team1/backend/controllers/ControllersUtil.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index 29e939f2b..6b079bceb 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -46,8 +46,9 @@ fun getObjectCreatedResponse( } /** - * This function checks if a request is allowed based on the active edition, - * entries of inactive editions can only be viewed or deleted by admins + * This function checks if a request is allowed based on the edition of the accessed resource ([editionName]), + * (entries of inactive editions can only be viewed or deleted by admins). + * [editionService], [userDetailService] and [httpServletRequest] should be injected in the caller of this function. */ fun attemptEditionAccess( editionName: String, From 5f4a22d1e4fd612528ab648b0c60e68108978042 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 9 May 2022 09:35:48 +0200 Subject: [PATCH 038/425] test: fixed authtest --- .../team1/backend/integrationtests/AuthorizationTests.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt index ac851c723..4b7ecda11 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt @@ -1,8 +1,10 @@ package be.osoc.team1.backend.integrationtests +import be.osoc.team1.backend.entities.Edition import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.Student import be.osoc.team1.backend.entities.User +import be.osoc.team1.backend.repositories.EditionRepository import be.osoc.team1.backend.repositories.StudentRepository import be.osoc.team1.backend.repositories.UserRepository import be.osoc.team1.backend.security.ConfigUtil @@ -40,6 +42,7 @@ class AuthorizationTests { userRepository.save(coachUser) userRepository.save(disabledUser) studentRepository.save(testStudent) + editionRepository.save(testEdition) } @Autowired @@ -51,7 +54,11 @@ class AuthorizationTests { @Autowired private lateinit var userRepository: UserRepository + @Autowired + private lateinit var editionRepository: EditionRepository + private val testEditionName = "testEditionName" + private val testEdition = Edition(testEditionName, true) private val studentBaseUrl = "/$testEditionName/students" private val adminPassword = "adminPassword" From 80206db77f88737861fde38d34cc649cfc901a93 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 9 May 2022 09:40:27 +0200 Subject: [PATCH 039/425] chore: ktlint --- .../be/osoc/team1/backend/controllers/BaseController.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt index cf4efbc9f..b27fdd5fa 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/BaseController.kt @@ -20,12 +20,8 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import java.lang.reflect.Field -import java.lang.reflect.Modifier -import java.util.* +import java.util.UUID import javax.servlet.http.HttpServletRequest -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.isAccessible abstract class BaseController(open val service: BaseService) { @Autowired From b648df30339d876aad1f00128235a8b5727167fb Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Mon, 9 May 2022 09:40:02 +0200 Subject: [PATCH 040/425] feat: the StatusSuggestion class now uses a OneToOne User object instead of a UUID The population script was also updated to account for this change. --- .../backend/controllers/StudentController.kt | 6 +-- .../be/osoc/team1/backend/entities/Student.kt | 17 ++++--- .../team1/backend/services/StudentService.kt | 10 ++-- .../team1/backend/util/UrlDeserializer.kt | 5 ++ .../backend/unittests/SerializationTests.kt | 4 +- .../StatusSuggestionControllerTests.kt | 13 +++-- .../unittests/StatusSuggestionServiceTests.kt | 6 ++- .../unittests/StudentControllerTests.kt | 40 +++++++++++++++- .../backend/unittests/StudentServiceTests.kt | 48 ++++--------------- docker/subpopulate.py | 3 +- 10 files changed, 86 insertions(+), 66 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index c43abcb5f..b7b15c2b8 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -192,10 +192,8 @@ class StudentController( principal: Principal, ) { val user = userDetailService.getUserFromPrincipal(principal) - if (statusSuggestion.coachId != user.id) - throw UnauthorizedOperationException( - "The 'coachId' did not equal authenticated user id!" - ) + if (statusSuggestion.suggester != user) + throw UnauthorizedOperationException("The 'coachId' did not equal authenticated user id!") service.addStudentStatusSuggestion(studentId, statusSuggestion, edition) } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt index 02b53a286..e27e08809 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt @@ -6,6 +6,8 @@ import be.osoc.team1.backend.util.AnswerListSerializer import be.osoc.team1.backend.util.CommunicationListSerializer import be.osoc.team1.backend.util.StatusSuggestionListSerializer import be.osoc.team1.backend.util.TallyDeserializer +import be.osoc.team1.backend.util.UserDeserializer +import be.osoc.team1.backend.util.UserSerializer import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize @@ -16,6 +18,7 @@ import javax.persistence.Entity import javax.persistence.Id import javax.persistence.ManyToMany import javax.persistence.OneToMany +import javax.persistence.OneToOne import javax.persistence.OrderBy import javax.validation.constraints.NotBlank @@ -53,10 +56,9 @@ enum class SuggestionEnum { /** * Represents the entry of a [status] suggestion in the database. - * Every [StatusSuggestion] is made by a coach of type [User]. The [coachId] of the [User] who made the suggestion - * is included in the object. A coach can make multiple suggestions about different [Student]s, but - * it wouldn't make any sense for a coach to make multiple suggestions about the same [Student]. - * Therefore the combination of [coachId] and [Student] must be unique. + * Every [StatusSuggestion] is made by a [suggester] of type [User]. A coach can make multiple suggestions about + * different [Student]s, but it wouldn't make any sense for a coach to make multiple suggestions about the same [Student]. + * Therefore, the combination of [suggester] and [Student] must be unique. * This constraint is checked when adding a new [StatusSuggestion] to a [Student]. * A [StatusSuggestion] always belongs to one particular [Student] in the database. * This [Student] is included in the [StatusSuggestion] to verify the unique constraint, @@ -67,7 +69,10 @@ enum class SuggestionEnum { */ @Entity class StatusSuggestion( - val coachId: UUID, + @OneToOne + @JsonSerialize(using = UserSerializer::class) + @JsonDeserialize(using = UserDeserializer::class) + val suggester: User, val status: SuggestionEnum, val motivation: String, @JsonIgnore @@ -166,7 +171,7 @@ fun List.filterByName(nameQuery: String) = * be returned. */ fun List.filterBySuggested(callee: User) = - filter { student: Student -> student.statusSuggestions.none { it.coachId == callee.id } } + filter { student: Student -> student.statusSuggestions.none { it.suggester == callee } } /** * This function will filter [Student]s based on a set of [skillNames]. diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/StudentService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/StudentService.kt index 503277671..a20ce6b6d 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/StudentService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/StudentService.kt @@ -1,7 +1,6 @@ package be.osoc.team1.backend.services import be.osoc.team1.backend.entities.Communication -import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.StatusEnum import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.entities.Student @@ -70,12 +69,9 @@ class StudentService(private val repository: StudentRepository, private val user * the coach role. */ fun addStudentStatusSuggestion(studentId: UUID, statusSuggestion: StatusSuggestion, edition: String) { - val coach = userService.getUserById(statusSuggestion.coachId) - if (!coach.role.hasPermissionLevel(Role.Coach)) { - throw ForbiddenOperationException("Only coaches and admins can make status suggestions.") - } + val coach = statusSuggestion.suggester val student = getStudentById(studentId, edition) - val sameCoachSuggestion = student.statusSuggestions.find { it.coachId == coach.id } + val sameCoachSuggestion = student.statusSuggestions.find { it.suggester == coach } if (sameCoachSuggestion !== null) { throw ForbiddenOperationException("This coach has already made a suggestion for this student.") } @@ -94,7 +90,7 @@ class StudentService(private val repository: StudentRepository, private val user fun deleteStudentStatusSuggestion(studentId: UUID, coachId: UUID, edition: String) { val coach = userService.getUserById(coachId) val student = getStudentById(studentId, edition) - val suggestion = student.statusSuggestions.find { it.coachId == coach.id } + val suggestion = student.statusSuggestions.find { it.suggester == coach } if (suggestion === null) { throw FailedOperationException("This coach hasn't made a suggestion for the given student.") } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/util/UrlDeserializer.kt b/backend/src/main/kotlin/be/osoc/team1/backend/util/UrlDeserializer.kt index 2ff43fafe..6d3581dda 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/util/UrlDeserializer.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/util/UrlDeserializer.kt @@ -1,8 +1,10 @@ package be.osoc.team1.backend.util import be.osoc.team1.backend.entities.Assignment +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.FailedOperationException import be.osoc.team1.backend.repositories.AssignmentRepository +import be.osoc.team1.backend.repositories.UserRepository import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer @@ -28,3 +30,6 @@ open class UrlDeserializer(private val repository: CrudRepository) : class AssignmentDeserializer(assignmentRepository: AssignmentRepository) : UrlDeserializer(assignmentRepository) + +class UserDeserializer(userRepository: UserRepository) : + UrlDeserializer(userRepository) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SerializationTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SerializationTests.kt index b0e4b104b..1578f6ed7 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SerializationTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/SerializationTests.kt @@ -21,7 +21,6 @@ import org.springframework.boot.test.json.JsonContent import org.springframework.mock.web.MockHttpServletRequest import org.springframework.web.context.request.RequestContextHolder import org.springframework.web.context.request.ServletRequestAttributes -import java.util.UUID @JsonTest class SerializationTests { @@ -81,7 +80,8 @@ class SerializationTests { @Test fun `Serialization of Student returns the correct result`() { - val testStatusSuggestion = StatusSuggestion(UUID.randomUUID(), SuggestionEnum.Yes, "motivation", testEdition) + val suggester = User("username", "email", Role.Coach, "password") + val testStatusSuggestion = StatusSuggestion(suggester, SuggestionEnum.Yes, "motivation", testEdition) val testCommunication = Communication("test", CommunicationTypeEnum.Email, testEdition) val testStudent = Student("Jitse", "Willaert", testEdition) testStudent.communications.add(testCommunication) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt index e9079096b..c5845a7f8 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionControllerTests.kt @@ -1,11 +1,15 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.StatusSuggestionController +import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.entities.SuggestionEnum +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.services.StatusSuggestionService +import be.osoc.team1.backend.util.UserSerializer import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule import com.ninjasquad.springmockk.MockkBean import io.mockk.every import org.junit.jupiter.api.BeforeEach @@ -27,9 +31,8 @@ class StatusSuggestionControllerTests(@Autowired private val mockMvc: MockMvc) { private lateinit var statusSuggestionService: StatusSuggestionService private val testId = UUID.randomUUID() - private val coachId = UUID.randomUUID() - private val testStatusSuggestion = StatusSuggestion(coachId, SuggestionEnum.Yes, "motivation") - private val objectMapper = ObjectMapper() + private val coach = User("username", "email", Role.Coach, "password") + private val testStatusSuggestion = StatusSuggestion(coach, SuggestionEnum.Yes, "motivation") @BeforeEach fun beforeEach() { @@ -38,6 +41,10 @@ class StatusSuggestionControllerTests(@Autowired private val mockMvc: MockMvc) { @Test fun `getStatusSuggestionById returns statusSuggestion if statusSuggestion with given id exists`() { + val objectMapper = ObjectMapper() + val simpleModule = SimpleModule() + simpleModule.addSerializer(User::class.java, UserSerializer()) + objectMapper.registerModule(simpleModule) val jsonRepresentation = objectMapper.writeValueAsString(testStatusSuggestion) every { statusSuggestionService.getById(testId) } returns testStatusSuggestion mockMvc.perform(get("/statusSuggestions/$testId")).andExpect(status().isOk) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionServiceTests.kt index 8dc44867d..062873937 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StatusSuggestionServiceTests.kt @@ -1,7 +1,9 @@ package be.osoc.team1.backend.unittests +import be.osoc.team1.backend.entities.Role import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.entities.SuggestionEnum +import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.repositories.StatusSuggestionRepository import be.osoc.team1.backend.services.StatusSuggestionService @@ -17,8 +19,8 @@ import java.util.UUID class StatusSuggestionServiceTests { private val testId = UUID.randomUUID() - private val coachId = UUID.randomUUID() - private val testStatusSuggestion = StatusSuggestion(coachId, SuggestionEnum.Yes, "motivation") + private val coach = User("username", "email", Role.Coach, "password") + private val testStatusSuggestion = StatusSuggestion(coach, SuggestionEnum.Yes, "motivation") private fun getRepository(statusSuggestionAlreadyExists: Boolean): StatusSuggestionRepository { val repository: StatusSuggestionRepository = mockk() diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt index f65002217..aa6f002cb 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt @@ -17,13 +17,17 @@ import be.osoc.team1.backend.exceptions.InvalidIdException import be.osoc.team1.backend.exceptions.InvalidStudentIdException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.AssignmentRepository +import be.osoc.team1.backend.repositories.UserRepository import be.osoc.team1.backend.services.OsocUserDetailService import be.osoc.team1.backend.services.PagedCollection import be.osoc.team1.backend.services.StudentService import be.osoc.team1.backend.services.applyIf import be.osoc.team1.backend.util.TallyDeserializer +import be.osoc.team1.backend.util.UserDeserializer +import be.osoc.team1.backend.util.UserSerializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ObjectNode import com.ninjasquad.springmockk.MockkBean import io.mockk.Runs @@ -35,6 +39,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.domain.Sort +import org.springframework.data.repository.findByIdOrNull import org.springframework.http.MediaType import org.springframework.mock.web.MockHttpServletRequest import org.springframework.security.authentication.TestingAuthenticationToken @@ -63,6 +68,9 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { @MockkBean private lateinit var assignmentRepository: AssignmentRepository + @MockkBean + private lateinit var userRepository: UserRepository + private val studentId = UUID.randomUUID() private val testCoach = User("coach", "email", Role.Coach, "password") private val coachId = testCoach.id @@ -72,7 +80,7 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { private val editionUrl = "/$testEdition/students" private val defaultPrincipal = TestingAuthenticationToken(null, null) private val defaultSort = Sort.by("id") - private val testSuggestion = StatusSuggestion(coachId, SuggestionEnum.Yes, "test motivation") + private val testSuggestion = StatusSuggestion(testCoach, SuggestionEnum.Yes, "test motivation") @BeforeEach fun beforeEach() { @@ -182,7 +190,7 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { @Test fun `getAllStudents include filtering works`() { val testStudent = Student("Lars", "Van", testEdition) - testStudent.statusSuggestions.add(StatusSuggestion(testCoach.id, SuggestionEnum.Yes, "Nice!")) + testStudent.statusSuggestions.add(StatusSuggestion(testCoach, SuggestionEnum.Yes, "Nice!")) every { studentService.getAllStudents(defaultSort, testEdition) } returns listOf(testStudent) mockMvc.perform(get("$editionUrl?includeSuggested=false").principal(defaultPrincipal)) .andExpect(status().isOk) @@ -426,9 +434,23 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { .andExpect(status().isNotFound) } + private fun userSerializedObjectMapper(): ObjectMapper { + val objectMapper = ObjectMapper() + val simpleModule = SimpleModule() + simpleModule.addSerializer(User::class.java, UserSerializer()) + simpleModule.addDeserializer(User::class.java, UserDeserializer(userRepository)) + objectMapper.registerModule(simpleModule) + + return objectMapper + } + @Test fun `addStudentStatusSuggestion succeeds when student with given id exists`() { every { studentService.addStudentStatusSuggestion(studentId, any(), testEdition) } just Runs + + val objectMapper = userSerializedObjectMapper() + every { userRepository.findByIdOrNull(testSuggestion.suggester.id) } returns testSuggestion.suggester + mockMvc.perform( post("$editionUrl/$studentId/suggestions") .contentType(MediaType.APPLICATION_JSON) @@ -442,6 +464,10 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { fun `addStudentStatusSuggestion returns 404 Not Found if student with given id does not exist`() { every { studentService.addStudentStatusSuggestion(studentId, any(), testEdition) } .throws(InvalidStudentIdException()) + + val objectMapper = userSerializedObjectMapper() + every { userRepository.findByIdOrNull(testSuggestion.suggester.id) } returns testSuggestion.suggester + mockMvc.perform( post("$editionUrl/$studentId/suggestions") .contentType(MediaType.APPLICATION_JSON) @@ -455,6 +481,10 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { fun `addStudentStatusSuggestion returns 403 Forbidden if coach already made suggestion for student`() { every { studentService.addStudentStatusSuggestion(studentId, any(), testEdition) } .throws(ForbiddenOperationException()) + + val objectMapper = userSerializedObjectMapper() + every { userRepository.findByIdOrNull(testSuggestion.suggester.id) } returns testSuggestion.suggester + mockMvc.perform( post("$editionUrl/$studentId/suggestions") .contentType(MediaType.APPLICATION_JSON) @@ -468,6 +498,9 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { every { studentService.addStudentStatusSuggestion(studentId, any(), testEdition) } .throws(InvalidUserIdException()) + val objectMapper = userSerializedObjectMapper() + every { userRepository.findByIdOrNull(testSuggestion.suggester.id) } returns testSuggestion.suggester + mockMvc.perform( post("$editionUrl/$studentId/suggestions") .contentType(MediaType.APPLICATION_JSON) @@ -481,6 +514,9 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { every { userDetailService.getUserFromPrincipal(any()) } returns User("other user", "email", Role.Coach, "password") + val objectMapper = userSerializedObjectMapper() + every { userRepository.findByIdOrNull(testSuggestion.suggester.id) } returns testSuggestion.suggester + mockMvc.perform( post("$editionUrl/$studentId/suggestions") .contentType(MediaType.APPLICATION_JSON) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentServiceTests.kt index ba660735a..2a7d06f6d 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentServiceTests.kt @@ -11,7 +11,6 @@ import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.FailedOperationException import be.osoc.team1.backend.exceptions.ForbiddenOperationException import be.osoc.team1.backend.exceptions.InvalidStudentIdException -import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.StudentRepository import be.osoc.team1.backend.services.PagedCollection import be.osoc.team1.backend.services.Pager @@ -27,7 +26,6 @@ import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.springframework.data.domain.Sort -import java.util.UUID class StudentServiceTests { @@ -35,7 +33,7 @@ class StudentServiceTests { private val testStudent = Student("Tom", "Alard", testEdition) private val studentId = testStudent.id private val testCoach = User("", "", Role.Coach, "") - private val testSuggestion = StatusSuggestion(testCoach.id, SuggestionEnum.Yes, "test motivation") + private val testSuggestion = StatusSuggestion(testCoach, SuggestionEnum.Yes, "test motivation") private val userService = mockk() private val defaultSort = Sort.by("id") @@ -121,7 +119,6 @@ class StudentServiceTests { every { repository.findByIdAndEdition(studentId, testEdition) } returns student every { repository.save(student) } returns student val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns testCoach val service = StudentService(repository, customUserService) service.addStudentStatusSuggestion(studentId, testSuggestion, testEdition) val suggestionId = testSuggestion.id @@ -132,7 +129,6 @@ class StudentServiceTests { @Test fun `addStudentStatusSuggestion fails when no student with that id exists`() { val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns testCoach val service = StudentService(getRepository(false), customUserService) assertThrows { service.addStudentStatusSuggestion(studentId, testSuggestion, testEdition) } } @@ -141,45 +137,27 @@ class StudentServiceTests { fun `addStudentStatusSuggestion fails when coach already made suggestion for student`() { val repository: StudentRepository = mockk() val student: Student = mockk() - val coachId2 = UUID.randomUUID() - val testSuggestion2 = StatusSuggestion(coachId2, SuggestionEnum.No, "test motivation2") + val coach2 = User("coach 2", "coach2@email.com", Role.Admin, "password") + val testSuggestion2 = StatusSuggestion(coach2, SuggestionEnum.No, "test motivation2") every { student.statusSuggestions.iterator() } returns mutableListOf(testSuggestion2, testSuggestion).iterator() every { repository.findByIdAndEdition(studentId, testEdition) } returns student val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns testCoach val service = StudentService(repository, customUserService) assertThrows { service.addStudentStatusSuggestion(studentId, testSuggestion, testEdition) } } @Test - fun `addStudentStatusSuggestion fails when coach does not exist`() { - val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) }.throws(InvalidUserIdException()) - val service = StudentService(getRepository(true), customUserService) - assertThrows { service.addStudentStatusSuggestion(studentId, testSuggestion, testEdition) } - } - - @Test - fun `addStudentStatusSuggestion fails when user does not have coach role`() { - val disabledUser = User("", "", Role.Disabled, "") - val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns disabledUser - val service = StudentService(getRepository(true), customUserService) - assertThrows { service.addStudentStatusSuggestion(studentId, testSuggestion, testEdition) } - } - - @Test - fun `deleteStudentStatusSuggestion removes suggestion when student, suggestion and coach exist`() { + fun `deleteStudentStatusSuggestion removes suggestion when student, suggestion and suggester exist`() { val repository: StudentRepository = mockk() val student: Student = mockk() - val coachId2 = UUID.randomUUID() - val testSuggestion2 = StatusSuggestion(coachId2, SuggestionEnum.No, "test motivation2") + val coach2 = User("coach 2", "coach2@email.com", Role.Admin, "password") + val testSuggestion2 = StatusSuggestion(coach2, SuggestionEnum.No, "test motivation2") every { student.statusSuggestions.remove(testSuggestion) } returns true every { student.statusSuggestions.iterator() } returns mutableListOf(testSuggestion2, testSuggestion).iterator() every { repository.findByIdAndEdition(studentId, testEdition) } returns student every { repository.save(student) } returns student val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns testCoach + every { customUserService.getUserById(testSuggestion.suggester.id) } returns testCoach val service = StudentService(repository, customUserService) service.deleteStudentStatusSuggestion(studentId, testCoach.id, testEdition) verify { student.statusSuggestions.remove(testSuggestion) } @@ -188,7 +166,7 @@ class StudentServiceTests { @Test fun `deleteStudentStatusSuggestion fails when no student with that id exists`() { val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns testCoach + every { customUserService.getUserById(testCoach.id) } returns testCoach val service = StudentService(getRepository(false), customUserService) assertThrows { service.deleteStudentStatusSuggestion(studentId, testCoach.id, testEdition) } } @@ -196,19 +174,11 @@ class StudentServiceTests { @Test fun `deleteStudentStatusSuggestion fails when given coach hasn't made a suggestion for this student`() { val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) } returns testCoach + every { customUserService.getUserById(testCoach.id) } returns testCoach val service = StudentService(getRepository(true), customUserService) assertThrows { service.deleteStudentStatusSuggestion(studentId, testCoach.id, testEdition) } } - @Test - fun `deleteStudentStatusSuggestion fails when coach does not exist`() { - val customUserService: UserService = mockk() - every { customUserService.getUserById(testSuggestion.coachId) }.throws(InvalidUserIdException()) - val service = StudentService(getRepository(true), customUserService) - assertThrows { service.deleteStudentStatusSuggestion(studentId, testCoach.id, testEdition) } - } - @Test fun `addCommunicationToStudent adds communication to list of student`() { val repository = getRepository(true) diff --git a/docker/subpopulate.py b/docker/subpopulate.py index 9be969695..3592fa169 100644 --- a/docker/subpopulate.py +++ b/docker/subpopulate.py @@ -805,7 +805,8 @@ def make_student(): coach_token = requests.post('http://localhost:8080/api/login', data={"email": coach["email"], "password": "suuuuuperseeeeecret"}).json()["accessToken"] for studid in studentsids[:total//4]: - requests.post(f'http://localhost:8080/api/ed/students/{studid}/suggestions', json={"coachId": coach["id"], "status": random.choice( + print("Making suggestion") + requests.post(f'http://localhost:8080/api/ed/students/{studid}/suggestions', json={"suggester": f"http://localhost:8080/api/users/{coach['id']}", "status": random.choice( ["Yes", "No", "Maybe"]), "motivation": fake.paragraph(nb_sentences=4)}, headers={'Authorization': f'Basic {coach_token}', 'Content-Type': 'application/json'}) # students to projects # coaches to projects From a4f2fed2b21af601ba6ffbbbb3344d31d7117c27 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Mon, 9 May 2022 09:53:48 +0200 Subject: [PATCH 041/425] docs: updated addStudentStatusSuggestion documentation --- .../team1/backend/controllers/StudentController.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index b7b15c2b8..8db5d8226 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -162,17 +162,15 @@ class StudentController( service.setStudentStatus(studentId, status, edition) /** - * Add a [statusSuggestion] to the student with the given [studentId]. The coachId field should - * be equal to the id of the coach who is making this suggestion, so equal to the id of the - * currently authenticated user. If either of these id's do not have a matching record in the - * database, a "404: Not Found" message is returned to the caller instead. If the coachId does - * not match the id of the currently authenticated user a '401: Unauthorized" is returned. The + * Add a [statusSuggestion] to the student with the given [studentId]. The suggester field should + * be equal to the coach who is making this suggestion, so equal to the currently authenticated user. + * If the suggester does not match the currently authenticated user a '401: Unauthorized" is returned. The * [statusSuggestion] should be passed in the request body as a JSON object and should have the * following format: * * ``` * { - * "coachId": "(INSERT ID)" + * "suggester": "(INSERT url to User)" * "status": "Yes" OR "Maybe" OR "No", * "motivation": "(INSERT MOTIVATION)" * } From c7e974bb9c01441569c4cf58a783e648fecd7453 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 9 May 2022 09:54:08 +0200 Subject: [PATCH 042/425] test: added 500ms to passwordencoder time window --- .../be/osoc/team1/backend/unittests/PasswordEncoderTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt index cfe1d8391..e1f15da84 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/PasswordEncoderTests.kt @@ -38,7 +38,7 @@ class PasswordEncoderTests { encoder.encode("Password.test1") val passedTimeMillis = System.currentTimeMillis() - startTimeMillis assert(passedTimeMillis > 700) - assert(passedTimeMillis < 2000) + assert(passedTimeMillis < 2500) } @Test From 2d51451f49855862e56bdfc973593fb3c9e0c726 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Mon, 9 May 2022 11:56:56 +0200 Subject: [PATCH 043/425] docs: explain reset tokens map --- .../be/osoc/team1/backend/security/ResetPasswordUtil.kt | 4 +++- .../main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index c0fa073a2..6890c778a 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -9,7 +9,9 @@ import java.util.UUID */ object ResetPasswordUtil { /** - * This map holds a [ResetPasswordToken] per ... + * When a user requests to change its password, a random [UUID] gets generated and a new entry gets added to + * [resetTokens]. The key of the entry is the hashed uuid, the value consists of a [ResetPasswordToken] containing + * TTL and email of the user. The uuid is used to generate a unique url for the user to reset its password. */ private val resetTokens: SortedMap = sortedMapOf( { a, b -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt index 625925b15..ca3198ec7 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/TokenUtil.kt @@ -16,7 +16,7 @@ import kotlin.random.Random.Default.nextBytes import kotlin.random.Random.Default.nextInt /** - * This object contains every function needed to create and process a token. + * This object contains every function needed to create and process a token (works with both access and refresh tokens). */ object TokenUtil { /** From bfc5a218a5d203168b1da9cbb786e7e279c238c7 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Mon, 9 May 2022 13:51:29 +0200 Subject: [PATCH 044/425] docs: update osoc.yaml --- docs/osoc.yaml | 134 ++++++++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 53 deletions(-) diff --git a/docs/osoc.yaml b/docs/osoc.yaml index 54c1758b6..38af13181 100644 --- a/docs/osoc.yaml +++ b/docs/osoc.yaml @@ -474,21 +474,21 @@ paths: schema: $ref: '#/components/schemas/Project' example: - { - "name": "Project name", - "clientName": "Client name", - "description": "Description", - "positions": [ - { - "skill": { "skillName": "Backend" }, - "amount": "2" - }, - { - "skill": { "skillName": "Frontend" }, - "amount": "1" - } - ] - } + { + "name": "Project name", + "clientName": "Client name", + "description": "Description", + "positions": [ + { + "skill": { "skillName": "Backend" }, + "amount": "2" + }, + { + "skill": { "skillName": "Frontend" }, + "amount": "1" + } + ] + } responses: '201': description: | @@ -549,28 +549,28 @@ paths: schema: $ref: '#/components/schemas/Project' example: - { - "name": "Project name", - "clientName": "Client name", - "description": "Description", - "edition": "testEdition", - "positions": [ - { - "skill": { "skillName": "Backend" }, - "amount": "2", - "id": "6adf6cb3-6638-4771-a281-403c0bf3427a" - }, - { - "skill": { "skillName": "Frontend" }, - "amount": "1", - "id": "41e25173-f7cb-4c4b-b4e7-d12f30e5fbe3" - } - ], - "assignments": [ - "http://localhost:8080/api/assignments/f9fefa13-80a3-439a-bf59-3cdacd60e2fc" - ], - "id": "68afaeef-48f4-4bf1-9ac7-a6a2c1bcf6d7" - } + { + "name": "Project name", + "clientName": "Client name", + "description": "Description", + "edition": "testEdition", + "positions": [ + { + "skill": { "skillName": "Backend" }, + "amount": "2", + "id": "6adf6cb3-6638-4771-a281-403c0bf3427a" + }, + { + "skill": { "skillName": "Frontend" }, + "amount": "1", + "id": "41e25173-f7cb-4c4b-b4e7-d12f30e5fbe3" + } + ], + "assignments": [ + "http://localhost:8080/api/assignments/f9fefa13-80a3-439a-bf59-3cdacd60e2fc" + ], + "id": "68afaeef-48f4-4bf1-9ac7-a6a2c1bcf6d7" + } responses: '200': description: successful operation @@ -796,7 +796,7 @@ paths: $ref: '#/components/schemas/Assignment' responses: '200': - description: succesful operation + description: successful operation '404': description: Not Found. No such projectId. '400': @@ -824,7 +824,7 @@ paths: format: uuid responses: '204': - description: succesful operation + description: successful operation '404': description: Not Found. No such projectId or assignmentId. /{edition}/projects/conflicts: @@ -914,7 +914,7 @@ paths: summary: Get list containing all users responses: '200': - description: succesful operation + description: successful operation content: application/json: schema: @@ -930,7 +930,7 @@ paths: $ref: '#/components/schemas/User' responses: '201': - description: succesful operation + description: successful operation content: application/json: schema: @@ -953,7 +953,7 @@ paths: format: uuid responses: '200': - description: succesful operation. + description: successful operation. content: application/json: schema: @@ -976,7 +976,7 @@ paths: $ref: '#/components/schemas/User' responses: '200': - description: succesful operation + description: successful operation content: application/json: schema: @@ -1003,7 +1003,7 @@ paths: format: uuid responses: '204': - description: succesful operation + description: successful operation '404': description: Not Found. No such userId. /users/{userId}/role: @@ -1024,7 +1024,7 @@ paths: enum: [ Disabled, Coach, Admin ] responses: '200': - description: succesful operation + description: successful operation content: application/json: schema: @@ -1033,6 +1033,34 @@ paths: description: NOT FOUND '400': description: Bad Request. + /users/resetPassword: + post: + summary: request to change password of given email + requestBody: + content: + text/plain: + schema: + type: string + responses: + '200': + description: successful operation + '400': + description: Bad Request. + /users/{resetPasswordUUID}/: + patch: + summary: reset password of user linked to resetPasswordUUID + parameters: + - name: resetPasswordUUID + required: true + in: path + schema: + type: string + format: uuid + responses: + '200': + description: successful operation + '400': + description: Bad Request. /assignments/{assignmentId}: get: summary: Get Assignment by id @@ -1178,14 +1206,14 @@ paths: description: | The response if authorization has failed. /logout: - post: - summary: log user out. invalidate http session, refresh token and redirect to /login?logout - responses: - '302': - description: logout successful, redirect to /login?logout - '400': - description: - Bad Request. + post: + summary: log user out. invalidate http session, refresh token and redirect to /login?logout + responses: + '302': + description: logout successful, redirect to /login?logout + '400': + description: + Bad Request. /token/refresh: post: summary: renew access token using refresh token From cf997c495c4a10c86ceca2a02ee128ffd6b57092 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Mon, 9 May 2022 14:05:33 +0200 Subject: [PATCH 045/425] feat: remove expired reset password tokens --- .../backend/security/ResetPasswordUtil.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt index 6890c778a..a1a7f5e49 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt @@ -10,10 +10,10 @@ import java.util.UUID object ResetPasswordUtil { /** * When a user requests to change its password, a random [UUID] gets generated and a new entry gets added to - * [resetTokens]. The key of the entry is the hashed uuid, the value consists of a [ResetPasswordToken] containing + * [resetPasswordTokens]. The key of the entry is the hashed uuid, the value consists of a [ResetPasswordToken] containing * TTL and email of the user. The uuid is used to generate a unique url for the user to reset its password. */ - private val resetTokens: SortedMap = sortedMapOf( + private val resetPasswordTokens: SortedMap = sortedMapOf( { a, b -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } ) @@ -29,13 +29,21 @@ object ResetPasswordUtil { return sha256.digest(resetPasswordUUID.toString().toByteArray()) } + /** + * Remove expired tokens from [resetPasswordTokens]. + */ + private fun removeExpiredTokens() { + resetPasswordTokens.values.removeIf { it.isExpired() } + } + /** * Create a [ResetPasswordToken] for [emailAddress]. */ fun newToken(emailAddress: String): UUID { + removeExpiredTokens() val uuid: UUID = UUID.randomUUID() val hashedUUID: ByteArray = hash(uuid) - resetTokens[hashedUUID] = ResetPasswordToken(emailAddress) + resetPasswordTokens[hashedUUID] = ResetPasswordToken(emailAddress) return uuid } @@ -43,7 +51,7 @@ object ResetPasswordUtil { * Check whether resetPasswordToken linked to [hashedUUID] is valid and hasn't expired yet. */ private fun isTokenValid(hashedUUID: ByteArray): Boolean { - return (hashedUUID in resetTokens && !resetTokens[hashedUUID]!!.isExpired()) + return (hashedUUID in resetPasswordTokens && !resetPasswordTokens[hashedUUID]!!.isExpired()) } /** @@ -52,7 +60,7 @@ object ResetPasswordUtil { fun getEmailFromUUID(resetPasswordUUID: UUID): String? { val hashedUUID = hash(resetPasswordUUID) if (isTokenValid(hashedUUID)) { - return resetTokens[hashedUUID]!!.emailAddress + return resetPasswordTokens[hashedUUID]!!.emailAddress } return null } From ede7697ddfea65362d2ea6dfe5218a1a4dd9f1d4 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Mon, 9 May 2022 14:14:17 +0200 Subject: [PATCH 046/425] style: fix eslint and prettier complaints --- frontend/lib/endpoints.ts | 2 +- frontend/pages/forgotPassword.tsx | 45 +++++++++---------- .../resetPassword/[resetPasswordToken].tsx | 29 +++++++----- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/frontend/lib/endpoints.ts b/frontend/lib/endpoints.ts index dc55aaa9a..012d45d2b 100644 --- a/frontend/lib/endpoints.ts +++ b/frontend/lib/endpoints.ts @@ -11,7 +11,7 @@ enum Endpoints { PROJECTS = '/projects', STUDENTS = '/students', SKILLS = '/skills', - RESETPASSWORD = '/users/resetPassword' + RESETPASSWORD = '/users/resetPassword', } export default Endpoints; diff --git a/frontend/pages/forgotPassword.tsx b/frontend/pages/forgotPassword.tsx index 08a0d987a..312b5e409 100644 --- a/frontend/pages/forgotPassword.tsx +++ b/frontend/pages/forgotPassword.tsx @@ -24,21 +24,19 @@ const ForgotPassword: NextPage = () => { if (email) { try { - const response = await axios.post( - Endpoints.RESETPASSWORD, - email, - { - headers: { 'Content-Type' : 'text/plain' } - } - ); - console.log(response) + const response = await axios.post(Endpoints.RESETPASSWORD, email, { + headers: { 'Content-Type': 'text/plain' }, + }); + console.log(response); // router.push('/'); toast.success( (t) => ( Email sent
An email has been sent to {email}
- +
), { duration: 12000 } @@ -50,18 +48,18 @@ const ForgotPassword: NextPage = () => { } }; - return ( - <> - -
+ return ( + <> + + - -
- - ) -} - + +
+ + ); +}; export default ForgotPassword; diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index c9135c73a..e4a4d518b 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -1,7 +1,7 @@ import type { NextPage } from 'next'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { FormEventHandler, useEffect, useRef, useState } from 'react'; +import { FormEventHandler, useState } from 'react'; import toast from 'react-hot-toast'; import FormContainer from '../../components/FormContainer'; import axios from '../../lib/axios'; @@ -19,10 +19,10 @@ const ResetPassword: NextPage = () => { if (password) { try { const response = await axios.patch( - Endpoints.RESETPASSWORD + "/" + token, + Endpoints.RESETPASSWORD + '/' + token, password, { - headers: { 'Content-Type' : 'text/plain' } + headers: { 'Content-Type': 'text/plain' }, } ); // router.push('/'); @@ -32,7 +32,12 @@ const ResetPassword: NextPage = () => { Password reset
Password has been reset to {password}
- +
), { duration: 12000 } @@ -43,7 +48,9 @@ const ResetPassword: NextPage = () => { Success
Password has been reset
- +
), { duration: 12000 } @@ -57,9 +64,9 @@ const ResetPassword: NextPage = () => { return ( <> - -
-
+ ); }; From b994f43405c45917dae27ad8afc609ca970bfc3f Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Mon, 9 May 2022 14:55:55 +0200 Subject: [PATCH 047/425] feat: send actual emails --- .../be/osoc/team1/backend/security/EmailUtil.kt | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index c6b54431c..0eca54f82 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -13,8 +13,8 @@ object EmailUtil { /** * Set email account to send emails with. */ - private const val emailAddressSender = "noreply@osoc.com" - private const val passwordSender = "insert.password.here" + private const val emailAddressSender = "opensummerofcode.info@gmail.com" + private const val passwordSender = "nharepxthiwygcpj" /** * Make the body of the email users receive when they request a password change. @@ -26,8 +26,9 @@ object EmailUtil { Trouble signing in? Resetting your password is easy. - Use the link below to choose your new password. + Use the link below to choose a new password. $url + (if this link isn't clickable, you can copy and paste it into search bar) If you did not forget your password, please disregard this email. """.trimIndent() @@ -54,20 +55,12 @@ object EmailUtil { * Email [emailAddressReceiver] with a [resetPasswordUUID], so [emailAddressReceiver] can reset its email. */ fun sendEmail(emailAddressReceiver: String, resetPasswordUUID: UUID) { - // val mailSender = getMailSender() - val email = SimpleMailMessage() email.setSubject("Reset Password") email.setText(getResetPasswordEmailBody(resetPasswordUUID)) email.setTo(emailAddressReceiver) email.setFrom(emailAddressSender) - println(">>>>>>>") - println("To: $emailAddressReceiver") - println("From: $emailAddressSender") - println("> ${email.subject}") - println(email.text) - println(">>>>>>>") - // mailSender.send(email) + getMailSender().send(email) } } From 2427208174ebcfeaf69bff4039f08c620d95b42a Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Mon, 9 May 2022 15:28:30 +0200 Subject: [PATCH 048/425] fix: new password should also be regexed --- frontend/pages/forgotPassword.tsx | 1 + .../resetPassword/[resetPasswordToken].tsx | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/frontend/pages/forgotPassword.tsx b/frontend/pages/forgotPassword.tsx index 312b5e409..efe2b9e60 100644 --- a/frontend/pages/forgotPassword.tsx +++ b/frontend/pages/forgotPassword.tsx @@ -34,6 +34,7 @@ const ForgotPassword: NextPage = () => { Email sent
An email has been sent to {email}
+ You might want to look in spam.
diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index e4a4d518b..bd81ddcdf 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -1,26 +1,36 @@ import type { NextPage } from 'next'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { FormEventHandler, useState } from 'react'; +import { FormEventHandler, useState, useEffect } from 'react'; import toast from 'react-hot-toast'; import FormContainer from '../../components/FormContainer'; +import useInput from '../../hooks/useInput'; import axios from '../../lib/axios'; import Endpoints from '../../lib/endpoints'; +import { customPasswordRegex } from '../../lib/regex'; const ResetPassword: NextPage = () => { const router = useRouter(); const token = router.query.resetPasswordToken as string; - const [password, setPassword] = useState(''); + /* eslint-disable */ + const [password, resetPassword, passwordProps] = useInput(''); + const [validPassword, setValidPassword] = useState(true); + + useEffect(() => { + setValidPassword(customPasswordRegex.test(password)); + }, [validPassword]); const doSubmit: FormEventHandler = async (e) => { e.preventDefault(); + console.log("password:"+password) + console.log("validPassword:"+validPassword) - if (password) { + if (validPassword) { try { const response = await axios.patch( Endpoints.RESETPASSWORD + '/' + token, - password, + validPassword, { headers: { 'Content-Type': 'text/plain' }, } @@ -31,7 +41,7 @@ const ResetPassword: NextPage = () => { (t) => ( Password reset
- Password has been reset to {password}
+ Password has been reset to {validPassword}
@@ -386,10 +397,8 @@ const StudentView: React.FC = ({ ); }; -// TODO This should get the coach name somehow const StudentStatusSuggestion: React.FC = ({ - statusSuggestion, - coachMap, + statusSuggestion }: StatusSuggestionProp) => { let myLabel = question_mark; let myColor = 'text-check-orange'; @@ -404,7 +413,7 @@ const StudentStatusSuggestion: React.FC = ({ return (
{myLabel} -

{coachMap.get(statusSuggestion.coachId)}

+

{statusSuggestion.suggester.username}

); }; diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 2e08df2c6..0e6780677 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -120,7 +120,16 @@ export type StudentBase = { * This is the StatusSuggestion type as defined in osoc.yaml */ export type StatusSuggestion = { - coachId: UUID; + suggester: User; + status: StatusSuggestionStatus; + motivation: string; +}; + +/** + * This is the exact collection type returned by a get to the StatusSuggestion endpoint + */ +export type StatusSuggestionBase = { + suggester: string; status: StatusSuggestionStatus; motivation: string; }; From 493f39d7e52555c932945f61b6bc395ad3c994d0 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Mon, 9 May 2022 23:16:21 +0200 Subject: [PATCH 060/425] chore: removed some console.log calls --- frontend/components/student/StudentView.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/components/student/StudentView.tsx b/frontend/components/student/StudentView.tsx index 2e09b6cbb..419e93a25 100644 --- a/frontend/components/student/StudentView.tsx +++ b/frontend/components/student/StudentView.tsx @@ -145,11 +145,8 @@ async function getEntireStudent( router ); - statusSuggestionBaseList.forEach((suggestion, i) => { + statusSuggestionBaseList.forEach((suggestion) => { axiosAuthenticated.get(suggestion.suggester).then((response) => { - console.log("Response user"); - console.log(response.data); - const statusSuggestion = {} as StatusSuggestion; statusSuggestion.suggester = response.data as User; statusSuggestion.status = suggestion.status; From e267edfdb89267bf7878755d2305e31d946b7910 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Tue, 10 May 2022 08:40:00 +0200 Subject: [PATCH 061/425] chore: requested changes --- backend/pom.xml | 1 + .../be/osoc/team1/backend/controllers/ControllersUtil.kt | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index d210f24d9..a7072dda4 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -116,6 +116,7 @@ org.springframework.boot spring-boot-starter-aop + 2.6.7 diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt index 6b079bceb..47bb379ff 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ControllersUtil.kt @@ -63,11 +63,11 @@ fun attemptEditionAccess( if (!edition.accessibleBy(user)) throw UnauthorizedOperationException("Entries of inactive editions can only be accessed by admins!") if (!edition.isActive && httpServletRequest.method != "GET" && httpServletRequest.method != "DELETE") - throw ForbiddenOperationException("Entries of inactive editions can only be viewed or delete (Allowed methods: GET, DELETE)") + throw ForbiddenOperationException("Entries of inactive editions can only be viewed or deleted (Allowed methods: GET, DELETE)") } /** - * This class adds the code of the SecuredEdition annotation + * This class adds the code for the SecuredEdition annotation */ @Aspect @Component @@ -84,9 +84,7 @@ class EditionSecurityAspect(val editionService: EditionService, val userDetailSe if (editionFieldIndex < 0) throw IllegalStateException("The @SecuredEdition edition argument was not found! (With this annotation the function needs an edition argument)") val editionName = joinPoint.args[editionFieldIndex] as String - attemptEditionAccess(editionName, editionService, userDetailService, request) - return joinPoint } } From 16840bc0e3a69d24151848eab3873a062c6257b7 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 10 May 2022 13:43:42 +0200 Subject: [PATCH 062/425] refactor: rename reset to forgot --- .../osoc/team1/backend/security/EmailUtil.kt | 10 +-- ...asswordToken.kt => ForgotPasswordToken.kt} | 4 +- .../backend/security/ForgotPasswordUtil.kt | 67 +++++++++++++++++++ .../backend/security/ResetPasswordUtil.kt | 67 ------------------- .../backend/services/ForgotPasswordService.kt | 16 ++--- .../unittests/ForgotPasswordServiceTests.kt | 4 ++ 6 files changed, 86 insertions(+), 82 deletions(-) rename backend/src/main/kotlin/be/osoc/team1/backend/security/{ResetPasswordToken.kt => ForgotPasswordToken.kt} (70%) create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt delete mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt create mode 100644 backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 0eca54f82..785d2bba2 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -19,8 +19,8 @@ object EmailUtil { /** * Make the body of the email users receive when they request a password change. */ - private fun getResetPasswordEmailBody(resetPasswordUUID: UUID): String { - val url = "http://localhost:3000/resetPassword/$resetPasswordUUID" + private fun getForgotPasswordEmailBody(forgotPasswordUUID: UUID): String { + val url = "http://localhost:3000/forgotPassword/$forgotPasswordUUID" return """ Hi, @@ -52,12 +52,12 @@ object EmailUtil { } /** - * Email [emailAddressReceiver] with a [resetPasswordUUID], so [emailAddressReceiver] can reset its email. + * Email [emailAddressReceiver] with a [forgotPasswordUUID], so [emailAddressReceiver] can reset its email. */ - fun sendEmail(emailAddressReceiver: String, resetPasswordUUID: UUID) { + fun sendEmail(emailAddressReceiver: String, forgotPasswordUUID: UUID) { val email = SimpleMailMessage() email.setSubject("Reset Password") - email.setText(getResetPasswordEmailBody(resetPasswordUUID)) + email.setText(getForgotPasswordEmailBody(forgotPasswordUUID)) email.setTo(emailAddressReceiver) email.setFrom(emailAddressSender) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt similarity index 70% rename from backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt rename to backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt index ec93d8db4..b97e4c21f 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordToken.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt @@ -1,10 +1,10 @@ package be.osoc.team1.backend.security /** - * This class is used in [ResetPasswordUtil]. An instance of this class gets created when a user requests to change its + * This class is used in [ForgotPasswordUtil]. An instance of this class gets created when a user requests to change its * password. This user can change its password as long as the token hasn't expired (20 minutes after creation). */ -data class ResetPasswordToken( +data class ForgotPasswordToken( val emailAddress: String, val ttl: Long = System.currentTimeMillis() + 20 * 60 * 1000 // 20 minutes ) { diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt new file mode 100644 index 000000000..2b600f0d3 --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt @@ -0,0 +1,67 @@ +package be.osoc.team1.backend.security + +import java.security.MessageDigest +import java.util.SortedMap +import java.util.UUID + +/** + * This object contains every function to manage password forgotten requests. + */ +object ForgotPasswordUtil { + /** + * When a user requests to change its password, a random [UUID] gets generated and a new entry gets added to + * [forgotPasswordTokens]. The key of the entry is the hashed uuid, the value consists of a [ForgotPasswordToken] containing + * TTL and email of the user. The uuid is used to generate a unique url for the user to change its password. + */ + private val forgotPasswordTokens: SortedMap = sortedMapOf( + { a: ByteArray, b: ByteArray -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } + ) + + /** + * Init object that can hash using the SHA-256 hash function. + */ + private val sha256: MessageDigest = MessageDigest.getInstance("SHA256") + + /** + * Hash [forgotPasswordUUID] with the SHA-256 hash function. + */ + private fun hash(forgotPasswordUUID: UUID): ByteArray { + return sha256.digest(forgotPasswordUUID.toString().toByteArray()) + } + + /** + * Remove expired tokens from [forgotPasswordTokens]. + */ + private fun removeExpiredTokens() { + forgotPasswordTokens.values.removeIf { it.isExpired() } + } + + /** + * Create a [ForgotPasswordToken] for [emailAddress]. + */ + fun newToken(emailAddress: String): UUID { + removeExpiredTokens() + val uuid: UUID = UUID.randomUUID() + val hashedUUID: ByteArray = hash(uuid) + forgotPasswordTokens[hashedUUID] = ForgotPasswordToken(emailAddress) + return uuid + } + + /** + * Check whether forgotPasswordToken linked to [hashedUUID] is valid and hasn't expired yet. + */ + private fun isTokenValid(hashedUUID: ByteArray): Boolean { + return (hashedUUID in forgotPasswordTokens && !forgotPasswordTokens[hashedUUID]!!.isExpired()) + } + + /** + * Get which email address requested given [forgotPasswordUUID]. + */ + fun getEmailFromUUID(forgotPasswordUUID: UUID): String? { + val hashedUUID = hash(forgotPasswordUUID) + if (isTokenValid(hashedUUID)) { + return forgotPasswordTokens[hashedUUID]!!.emailAddress + } + return null + } +} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt deleted file mode 100644 index a1a7f5e49..000000000 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ResetPasswordUtil.kt +++ /dev/null @@ -1,67 +0,0 @@ -package be.osoc.team1.backend.security - -import java.security.MessageDigest -import java.util.SortedMap -import java.util.UUID - -/** - * This object contains every function needed to manage password reset requests. - */ -object ResetPasswordUtil { - /** - * When a user requests to change its password, a random [UUID] gets generated and a new entry gets added to - * [resetPasswordTokens]. The key of the entry is the hashed uuid, the value consists of a [ResetPasswordToken] containing - * TTL and email of the user. The uuid is used to generate a unique url for the user to reset its password. - */ - private val resetPasswordTokens: SortedMap = sortedMapOf( - { a, b -> return@sortedMapOf if (a.contentEquals(b)) 0 else 1 } - ) - - /** - * Init object that can hash using the SHA-256 hash function. - */ - private val sha256: MessageDigest = MessageDigest.getInstance("SHA256") - - /** - * Hash [resetPasswordUUID] with the SHA-256 hash function. - */ - private fun hash(resetPasswordUUID: UUID): ByteArray { - return sha256.digest(resetPasswordUUID.toString().toByteArray()) - } - - /** - * Remove expired tokens from [resetPasswordTokens]. - */ - private fun removeExpiredTokens() { - resetPasswordTokens.values.removeIf { it.isExpired() } - } - - /** - * Create a [ResetPasswordToken] for [emailAddress]. - */ - fun newToken(emailAddress: String): UUID { - removeExpiredTokens() - val uuid: UUID = UUID.randomUUID() - val hashedUUID: ByteArray = hash(uuid) - resetPasswordTokens[hashedUUID] = ResetPasswordToken(emailAddress) - return uuid - } - - /** - * Check whether resetPasswordToken linked to [hashedUUID] is valid and hasn't expired yet. - */ - private fun isTokenValid(hashedUUID: ByteArray): Boolean { - return (hashedUUID in resetPasswordTokens && !resetPasswordTokens[hashedUUID]!!.isExpired()) - } - - /** - * Get which email address requested given [resetPasswordUUID]. - */ - fun getEmailFromUUID(resetPasswordUUID: UUID): String? { - val hashedUUID = hash(resetPasswordUUID) - if (isTokenValid(hashedUUID)) { - return resetPasswordTokens[hashedUUID]!!.emailAddress - } - return null - } -} diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt index 4a1ed4e90..c9ef2fdf5 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt @@ -4,7 +4,7 @@ import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.exceptions.InvalidForgotPasswordUUIDException import be.osoc.team1.backend.repositories.UserRepository import be.osoc.team1.backend.security.EmailUtil -import be.osoc.team1.backend.security.ResetPasswordUtil +import be.osoc.team1.backend.security.ForgotPasswordUtil import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import java.util.UUID @@ -16,19 +16,19 @@ class ForgotPasswordService(private val repository: UserRepository, private val */ fun sendEmailWithToken(emailAddress: String) { if (repository.findByEmail(emailAddress) != null) { - val resetPasswordUUID: UUID = ResetPasswordUtil.newToken(emailAddress) - EmailUtil.sendEmail(emailAddress, resetPasswordUUID) + val forgotPasswordUUID: UUID = ForgotPasswordUtil.newToken(emailAddress) + EmailUtil.sendEmail(emailAddress, forgotPasswordUUID) } } /** - * Extract the email address from [resetPasswordUUID] and set the password of that user to [newPassword]. + * Extract the email address from [forgotPasswordUUID] and set the password of that user to [newPassword]. */ - fun changePassword(resetPasswordUUID: UUID, newPassword: String) { - val emailAddress = ResetPasswordUtil.getEmailFromUUID(resetPasswordUUID) - ?: throw InvalidForgotPasswordUUIDException("resetPasswordUUID is invalid.") + fun changePassword(forgotPasswordUUID: UUID, newPassword: String) { + val emailAddress = ForgotPasswordUtil.getEmailFromUUID(forgotPasswordUUID) + ?: throw InvalidForgotPasswordUUIDException("forgotPasswordUUID is invalid.") val user: User = repository.findByEmail(emailAddress) - ?: throw InvalidForgotPasswordUUIDException("ResetPasswordToken contains invalid email.") + ?: throw InvalidForgotPasswordUUIDException("ForgotPasswordToken contains invalid email.") user.password = passwordEncoder.encode(newPassword) repository.save(user) } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt new file mode 100644 index 000000000..7298bfc30 --- /dev/null +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt @@ -0,0 +1,4 @@ +package be.osoc.team1.backend.unittests + +class ForgotPasswordServiceTests { +} \ No newline at end of file From aeb1acc983b7493d402cc6be4a56600dc1f40e2b Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 10 May 2022 14:50:30 +0200 Subject: [PATCH 063/425] test: add service tests --- .../controllers/ForgotPasswordController.kt | 8 +-- .../ForgotPasswordControllerTests.kt | 14 ++-- .../unittests/ForgotPasswordServiceTests.kt | 68 ++++++++++++++++++- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ForgotPasswordController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ForgotPasswordController.kt index ff310e4b8..44d8fa9e4 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ForgotPasswordController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/ForgotPasswordController.kt @@ -25,9 +25,9 @@ class ForgotPasswordController(private val service: ForgotPasswordService) { fun postEmail(@RequestBody emailAddress: String) = service.sendEmailWithToken(emailAddress) /** - * Reset password using [resetPasswordUUID]. + * Reset password using [forgotPasswordUUID]. */ - @PatchMapping("/{resetPasswordUUID}") - fun patchPassword(@PathVariable resetPasswordUUID: UUID, @RequestBody newPassword: String) = - service.changePassword(resetPasswordUUID, newPassword) + @PatchMapping("/{forgotPasswordUUID}") + fun patchPassword(@PathVariable forgotPasswordUUID: UUID, @RequestBody newPassword: String) = + service.changePassword(forgotPasswordUUID, newPassword) } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordControllerTests.kt index 0cd588d8c..41ba22f03 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordControllerTests.kt @@ -1,7 +1,7 @@ package be.osoc.team1.backend.unittests import be.osoc.team1.backend.controllers.ForgotPasswordController -import be.osoc.team1.backend.exceptions.InvalidTokenException +import be.osoc.team1.backend.exceptions.InvalidForgotPasswordUUIDException import be.osoc.team1.backend.security.PasswordEncoderConfig import be.osoc.team1.backend.services.ForgotPasswordService import com.fasterxml.jackson.databind.ObjectMapper @@ -41,18 +41,18 @@ class ForgotPasswordControllerTests(@Autowired val mockMvc: MockMvc) { mockMvc.perform( MockMvcRequestBuilders.patch("/forgotPassword/$validUUID") .contentType(MediaType.TEXT_PLAIN) - .content(ObjectMapper().writeValueAsString(validNewPassword)) - ).andExpect(MockMvcResultMatchers.status().isNoContent) + .content(validNewPassword) + ).andExpect(MockMvcResultMatchers.status().isOk) } @Test fun `changePassword should fail when invalid uuid given`() { val invalidUUID = UUID.randomUUID() - every { service.changePassword(invalidUUID, validNewPassword) } throws InvalidTokenException() + every { service.changePassword(invalidUUID, validNewPassword) } throws InvalidForgotPasswordUUIDException() mockMvc.perform( - MockMvcRequestBuilders.patch("/forgotPassword/$validUUID") + MockMvcRequestBuilders.patch("/forgotPassword/$invalidUUID") .contentType(MediaType.TEXT_PLAIN) - .content(ObjectMapper().writeValueAsString(validNewPassword)) - ).andExpect(MockMvcResultMatchers.status().isNoContent) + .content(validNewPassword) + ).andExpect(MockMvcResultMatchers.status().isBadRequest) } } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt index 7298bfc30..88a2d5535 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt @@ -1,4 +1,70 @@ package be.osoc.team1.backend.unittests +import be.osoc.team1.backend.entities.Role +import be.osoc.team1.backend.entities.User +import be.osoc.team1.backend.exceptions.InvalidForgotPasswordUUIDException +import be.osoc.team1.backend.repositories.UserRepository +import be.osoc.team1.backend.security.ForgotPasswordUtil +import be.osoc.team1.backend.services.ForgotPasswordService +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.security.crypto.password.PasswordEncoder +import java.util.UUID + class ForgotPasswordServiceTests { -} \ No newline at end of file + private val testEmail = "test@email.com" + private val testUser = User("Test", testEmail, Role.Admin, "password") + private val newPassword = "new_password" + + private fun getRepository(userAlreadyExists: Boolean = true): UserRepository { + val repository: UserRepository = mockk() + every { repository.findByEmail(testEmail) } returns if (userAlreadyExists) testUser else null + every { repository.save(testUser) } returns testUser + return repository + } + + @Test + fun `sendEmailWithToken does not fail`() { + val service = ForgotPasswordService(getRepository(), mockk()) + service.sendEmailWithToken(testEmail) + } + + @Test + fun `changePassword fails when forgotPasswordUUID is invalid`() { + val invalidUUID = UUID.randomUUID() + mockkObject(ForgotPasswordUtil) + every { ForgotPasswordUtil.getEmailFromUUID(invalidUUID) } throws InvalidForgotPasswordUUIDException() + + val service = ForgotPasswordService(getRepository(), mockk()) + Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { + service.changePassword(invalidUUID, newPassword) + } + } + + @Test + fun `changePassword fails when email is invalid`() { + val validUUID = UUID.randomUUID() + mockkObject(ForgotPasswordUtil) + every { ForgotPasswordUtil.getEmailFromUUID(validUUID) } returns testEmail + + val service = ForgotPasswordService(getRepository(false), mockk()) + Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { + service.changePassword(validUUID, newPassword) + } + } + + @Test + fun `changePassword does not fail when valid arguments given`() { + val validUUID = UUID.randomUUID() + mockkObject(ForgotPasswordUtil) + every { ForgotPasswordUtil.getEmailFromUUID(validUUID) } returns testEmail + + val passwordEncoder: PasswordEncoder = mockk() + every { passwordEncoder.encode(any()) } returns "Encoded password" + val service = ForgotPasswordService(getRepository(), passwordEncoder) + service.changePassword(validUUID, newPassword) + } +} From bcf8e6a06069a60e24401258273ce90766802f36 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 10 May 2022 15:13:18 +0200 Subject: [PATCH 064/425] feat: invalidate token after use --- .../be/osoc/team1/backend/security/ForgotPasswordUtil.kt | 7 +++++++ .../osoc/team1/backend/services/ForgotPasswordService.kt | 1 + frontend/pages/resetPassword/[resetPasswordToken].tsx | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt index 2b600f0d3..6af2fe36b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordUtil.kt @@ -47,6 +47,13 @@ object ForgotPasswordUtil { return uuid } + /** + * Remove token linked to [forgotPasswordUUID] from [forgotPasswordTokens]. + */ + fun removeToken(forgotPasswordUUID: UUID) { + forgotPasswordTokens.remove(hash(forgotPasswordUUID)) + } + /** * Check whether forgotPasswordToken linked to [hashedUUID] is valid and hasn't expired yet. */ diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt index c9ef2fdf5..4af0a7434 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/ForgotPasswordService.kt @@ -31,5 +31,6 @@ class ForgotPasswordService(private val repository: UserRepository, private val ?: throw InvalidForgotPasswordUUIDException("ForgotPasswordToken contains invalid email.") user.password = passwordEncoder.encode(newPassword) repository.save(user) + ForgotPasswordUtil.removeToken(forgotPasswordUUID) } } diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/resetPassword/[resetPasswordToken].tsx index bd81ddcdf..ca202bcc0 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/resetPassword/[resetPasswordToken].tsx @@ -23,8 +23,8 @@ const ResetPassword: NextPage = () => { const doSubmit: FormEventHandler = async (e) => { e.preventDefault(); - console.log("password:"+password) - console.log("validPassword:"+validPassword) + console.log('password:' + password); + console.log('validPassword:' + validPassword); if (validPassword) { try { From 690f6646c4409ccd7c0a020db7954040bd4502a4 Mon Sep 17 00:00:00 2001 From: Michael M Date: Tue, 10 May 2022 15:16:28 +0200 Subject: [PATCH 065/425] chore: update dependencies --- frontend/package.json | 12 +-- frontend/yarn.lock | 175 ++++++++++++++++++++++-------------------- 2 files changed, 98 insertions(+), 89 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 60492e6e9..7c42056a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,12 +20,12 @@ "font-awesome": "^4.7.0", "next": "latest", "react": "^18.1.0", - "react-dnd": "16.0.0", + "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "^18.1.0", "react-hot-toast": "^2.2.0", "react-minimal-pie-chart": "^8.3.0", - "react-select": "^5.3.0", + "react-select": "^5.3.2", "react-use-poll": "^1.1.5", "reactjs-popup": "^2.0.5", "recoil": "^0.7.2", @@ -34,12 +34,12 @@ }, "devDependencies": { "@types/node": "^17.0.31", - "@types/react": "^18.0.8", + "@types/react": "^18.0.9", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.22.0", - "@typescript-eslint/parser": "^5.22.0", + "@typescript-eslint/eslint-plugin": "^5.23.0", + "@typescript-eslint/parser": "^5.23.0", "autoprefixer": "^10.4.7", - "eslint": "^8.14.0", + "eslint": "^8.15.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.5.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index df904f393..c3d3f3354 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -140,19 +140,19 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@eslint/eslintrc@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae" - integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg== +"@eslint/eslintrc@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" + integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.3.1" + espree "^9.3.2" globals "^13.9.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" "@fortawesome/fontawesome-common-types@6.1.1": @@ -316,12 +316,12 @@ resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A== -"@react-dnd/invariant@^4.0.0", "@react-dnd/invariant@^4.0.1": +"@react-dnd/invariant@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df" integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw== -"@react-dnd/shallowequal@^4.0.0": +"@react-dnd/shallowequal@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4" integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA== @@ -353,7 +353,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.0.8": +"@types/react@*": version "18.0.8" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.8.tgz#a051eb380a9fbcaa404550543c58e1cf5ce4ab87" integrity sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw== @@ -362,6 +362,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.0.9": + version "18.0.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" + integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -372,14 +381,14 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== -"@typescript-eslint/eslint-plugin@^5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz#7b52a0de2e664044f28b36419210aea4ab619e2a" - integrity sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg== +"@typescript-eslint/eslint-plugin@^5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8" + integrity sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA== dependencies: - "@typescript-eslint/scope-manager" "5.22.0" - "@typescript-eslint/type-utils" "5.22.0" - "@typescript-eslint/utils" "5.22.0" + "@typescript-eslint/scope-manager" "5.23.0" + "@typescript-eslint/type-utils" "5.23.0" + "@typescript-eslint/utils" "5.23.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -387,72 +396,72 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.22.0.tgz#7bedf8784ef0d5d60567c5ba4ce162460e70c178" - integrity sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ== +"@typescript-eslint/parser@^5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.23.0.tgz#443778e1afc9a8ff180f91b5e260ac3bec5e2de1" + integrity sha512-V06cYUkqcGqpFjb8ttVgzNF53tgbB/KoQT/iB++DOIExKmzI9vBJKjZKt/6FuV9c+zrDsvJKbJ2DOCYwX91cbw== dependencies: - "@typescript-eslint/scope-manager" "5.22.0" - "@typescript-eslint/types" "5.22.0" - "@typescript-eslint/typescript-estree" "5.22.0" + "@typescript-eslint/scope-manager" "5.23.0" + "@typescript-eslint/types" "5.23.0" + "@typescript-eslint/typescript-estree" "5.23.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz#590865f244ebe6e46dc3e9cab7976fc2afa8af24" - integrity sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA== +"@typescript-eslint/scope-manager@5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz#4305e61c2c8e3cfa3787d30f54e79430cc17ce1b" + integrity sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw== dependencies: - "@typescript-eslint/types" "5.22.0" - "@typescript-eslint/visitor-keys" "5.22.0" + "@typescript-eslint/types" "5.23.0" + "@typescript-eslint/visitor-keys" "5.23.0" -"@typescript-eslint/type-utils@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz#0c0e93b34210e334fbe1bcb7250c470f4a537c19" - integrity sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA== +"@typescript-eslint/type-utils@5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz#f852252f2fc27620d5bb279d8fed2a13d2e3685e" + integrity sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw== dependencies: - "@typescript-eslint/utils" "5.22.0" + "@typescript-eslint/utils" "5.23.0" debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.22.0.tgz#50a4266e457a5d4c4b87ac31903b28b06b2c3ed0" - integrity sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw== +"@typescript-eslint/types@5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09" + integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw== -"@typescript-eslint/typescript-estree@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz#e2116fd644c3e2fda7f4395158cddd38c0c6df97" - integrity sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw== +"@typescript-eslint/typescript-estree@5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065" + integrity sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg== dependencies: - "@typescript-eslint/types" "5.22.0" - "@typescript-eslint/visitor-keys" "5.22.0" + "@typescript-eslint/types" "5.23.0" + "@typescript-eslint/visitor-keys" "5.23.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.22.0.tgz#1f2c4897e2cf7e44443c848a13c60407861babd8" - integrity sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ== +"@typescript-eslint/utils@5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.23.0.tgz#4691c3d1b414da2c53d8943310df36ab1c50648a" + integrity sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.22.0" - "@typescript-eslint/types" "5.22.0" - "@typescript-eslint/typescript-estree" "5.22.0" + "@typescript-eslint/scope-manager" "5.23.0" + "@typescript-eslint/types" "5.23.0" + "@typescript-eslint/typescript-estree" "5.23.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz#f49c0ce406944ffa331a1cfabeed451ea4d0909c" - integrity sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg== +"@typescript-eslint/visitor-keys@5.23.0": + version "5.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz#057c60a7ca64667a39f991473059377a8067c87b" + integrity sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg== dependencies: - "@typescript-eslint/types" "5.22.0" + "@typescript-eslint/types" "5.23.0" eslint-visitor-keys "^3.0.0" -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== @@ -476,7 +485,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.7.0: +acorn@^8.7.1: version "8.7.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== @@ -808,7 +817,7 @@ dlv@^1.1.3: resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== -dnd-core@^16.0.0, dnd-core@^16.0.1: +dnd-core@^16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng== @@ -982,12 +991,12 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.14.0: - version "8.14.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.14.0.tgz#62741f159d9eb4a79695b28ec4989fcdec623239" - integrity sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw== +eslint@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.15.0.tgz#fea1d55a7062da48d82600d2e0974c55612a11e9" + integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA== dependencies: - "@eslint/eslintrc" "^1.2.2" + "@eslint/eslintrc" "^1.2.3" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -998,7 +1007,7 @@ eslint@^8.14.0: eslint-scope "^7.1.1" eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" - espree "^9.3.1" + espree "^9.3.2" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -1014,7 +1023,7 @@ eslint@^8.14.0: json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" regexpp "^3.2.0" @@ -1023,13 +1032,13 @@ eslint@^8.14.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== +espree@^9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" + integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" + acorn "^8.7.1" + acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" esquery@^1.4.0: @@ -1871,14 +1880,14 @@ react-dnd-html5-backend@16.0.1: dependencies: dnd-core "^16.0.1" -react-dnd@16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.0.tgz#6a391bc397462f7aca4455688db80137f44addd2" - integrity sha512-RCoeWRWhuwSoqdLaJV8N/weARLyXqsf43OC3QiBWPORIIGGovF/EqI8ckf14ca3bl6oZNI/igtxX49+IDmNDeQ== +react-dnd@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37" + integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q== dependencies: - "@react-dnd/invariant" "^4.0.0" - "@react-dnd/shallowequal" "^4.0.0" - dnd-core "^16.0.0" + "@react-dnd/invariant" "^4.0.1" + "@react-dnd/shallowequal" "^4.0.1" + dnd-core "^16.0.1" fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" @@ -1907,10 +1916,10 @@ react-minimal-pie-chart@^8.3.0: resolved "https://registry.yarnpkg.com/react-minimal-pie-chart/-/react-minimal-pie-chart-8.3.0.tgz#cf6de3d712ea07e87e404276cbb811143ca00682" integrity sha512-ICot74PGPrmGkaie8O+5tRZ5ivukYQ3fRN5ppvw1J01sIa5tHO9cekakyv9nXUkxKifiKn/FQu5uWHfSD39mRQ== -react-select@^5.3.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.3.1.tgz#2cb651b71493e494c56f6b4ce40011669b34bd95" - integrity sha512-Y195MmhDoDAj/8gTDyYZU1Raf7tmZd81wxM6RkFko4pqJ4Xv0/ilqUMtSn+GYkwmSlTWeMlzh+e+t7PJgtuXPw== +react-select@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.3.2.tgz#ecee0d5c59ed4acb7f567f7de3c75a488d93dacb" + integrity sha512-W6Irh7U6Ha7p5uQQ2ZnemoCQ8mcfgOtHfw3wuMzG6FAu0P+CYicgofSLOq97BhjMx8jS+h+wwWdCBeVVZ9VqlQ== dependencies: "@babel/runtime" "^7.12.0" "@emotion/cache" "^11.4.0" From 85170f5e69919429cbc978a2b417883b15fbb885 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Tue, 10 May 2022 15:28:23 +0200 Subject: [PATCH 066/425] refactor: final refactor steps --- .../exceptions/InvalidForgotPasswordUUIDException.kt | 2 +- .../kotlin/be/osoc/team1/backend/security/ConfigUtil.kt | 4 ++-- .../kotlin/be/osoc/team1/backend/security/EmailUtil.kt | 2 +- docs/osoc.yaml | 8 ++++---- frontend/lib/endpoints.ts | 2 +- .../[forgotPasswordToken].tsx} | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) rename frontend/pages/{resetPassword/[resetPasswordToken].tsx => forgotPassword/[forgotPasswordToken].tsx} (96%) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidForgotPasswordUUIDException.kt b/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidForgotPasswordUUIDException.kt index 905f8ba5c..728614aac 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidForgotPasswordUUIDException.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidForgotPasswordUUIDException.kt @@ -8,6 +8,6 @@ import org.springframework.web.bind.annotation.ResponseStatus */ @ResponseStatus(value = HttpStatus.BAD_REQUEST) class InvalidForgotPasswordUUIDException( - message: String = "resetPasswordUUID is invalid.", + message: String = "forgotPasswordUUID is invalid.", cause: Throwable? = null ) : Exception(message, cause) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt index b6a920dcd..6ca30f15b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt @@ -6,10 +6,10 @@ package be.osoc.team1.backend.security */ object ConfigUtil { val urlsOpenToAll: Array = arrayOf( - "/", "/login", "/logout", "/error", "/users/resetPassword/*" + "/", "/login", "/logout", "/error", "/users/forgotPassword/*" ) val urlsOpenToAllToPostTo: Array = arrayOf( - "/users", "/users/resetPassword", "/token/refresh", "/*/students" + "/users", "/users/forgotPassword", "/token/refresh", "/*/students" ) val allowedCorsOrigins: List = listOf("http://localhost:3000", "https://sel2-1.ugent.be") } diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index 785d2bba2..e8f740828 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -20,7 +20,7 @@ object EmailUtil { * Make the body of the email users receive when they request a password change. */ private fun getForgotPasswordEmailBody(forgotPasswordUUID: UUID): String { - val url = "http://localhost:3000/forgotPassword/$forgotPasswordUUID" + val url = "https://sel2-1.ugent.be/forgotPassword/$forgotPasswordUUID" return """ Hi, diff --git a/docs/osoc.yaml b/docs/osoc.yaml index 38af13181..3e4a4aeff 100644 --- a/docs/osoc.yaml +++ b/docs/osoc.yaml @@ -1033,7 +1033,7 @@ paths: description: NOT FOUND '400': description: Bad Request. - /users/resetPassword: + /forgotPassword: post: summary: request to change password of given email requestBody: @@ -1046,11 +1046,11 @@ paths: description: successful operation '400': description: Bad Request. - /users/{resetPasswordUUID}/: + /forgotPassword/{forgotPasswordUUID}/: patch: - summary: reset password of user linked to resetPasswordUUID + summary: reset password of user linked to forgotPasswordUUID parameters: - - name: resetPasswordUUID + - name: forgotPasswordUUID required: true in: path schema: diff --git a/frontend/lib/endpoints.ts b/frontend/lib/endpoints.ts index 012d45d2b..7b8887f2e 100644 --- a/frontend/lib/endpoints.ts +++ b/frontend/lib/endpoints.ts @@ -11,7 +11,7 @@ enum Endpoints { PROJECTS = '/projects', STUDENTS = '/students', SKILLS = '/skills', - RESETPASSWORD = '/users/resetPassword', + RESETPASSWORD = '/forgotPassword', } export default Endpoints; diff --git a/frontend/pages/resetPassword/[resetPasswordToken].tsx b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx similarity index 96% rename from frontend/pages/resetPassword/[resetPasswordToken].tsx rename to frontend/pages/forgotPassword/[forgotPasswordToken].tsx index ca202bcc0..ed547b9a9 100644 --- a/frontend/pages/resetPassword/[resetPasswordToken].tsx +++ b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx @@ -9,9 +9,9 @@ import axios from '../../lib/axios'; import Endpoints from '../../lib/endpoints'; import { customPasswordRegex } from '../../lib/regex'; -const ResetPassword: NextPage = () => { +const ForgotPassword: NextPage = () => { const router = useRouter(); - const token = router.query.resetPasswordToken as string; + const token = router.query.forgotPasswordToken as string; /* eslint-disable */ const [password, resetPassword, passwordProps] = useInput(''); @@ -108,4 +108,4 @@ const ResetPassword: NextPage = () => { ); }; -export default ResetPassword; +export default ForgotPassword; From 54ab6ae9f69ca6ef718bcf8421d24e843fc13a0c Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Tue, 10 May 2022 20:51:50 +0200 Subject: [PATCH 067/425] chore: removed test print message in population script --- docker/subpopulate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/subpopulate.py b/docker/subpopulate.py index 3592fa169..7a8479ffd 100644 --- a/docker/subpopulate.py +++ b/docker/subpopulate.py @@ -805,7 +805,6 @@ def make_student(): coach_token = requests.post('http://localhost:8080/api/login', data={"email": coach["email"], "password": "suuuuuperseeeeecret"}).json()["accessToken"] for studid in studentsids[:total//4]: - print("Making suggestion") requests.post(f'http://localhost:8080/api/ed/students/{studid}/suggestions', json={"suggester": f"http://localhost:8080/api/users/{coach['id']}", "status": random.choice( ["Yes", "No", "Maybe"]), "motivation": fake.paragraph(nb_sentences=4)}, headers={'Authorization': f'Basic {coach_token}', 'Content-Type': 'application/json'}) # students to projects From fe7ef91dc283c5a776d6ebcfc30ea108b985f79c Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Wed, 11 May 2022 09:24:18 +0200 Subject: [PATCH 068/425] chore: ran prettier --- frontend/components/student/StudentView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/components/student/StudentView.tsx b/frontend/components/student/StudentView.tsx index 419e93a25..44f7f84cb 100644 --- a/frontend/components/student/StudentView.tsx +++ b/frontend/components/student/StudentView.tsx @@ -152,8 +152,8 @@ async function getEntireStudent( statusSuggestion.status = suggestion.status; statusSuggestion.motivation = suggestion.motivation; newStudent.statusSuggestions.push(statusSuggestion); - }) - }) + }); + }); // TODO temp solution until this gets fixed const coaches = new Map(); @@ -395,7 +395,7 @@ const StudentView: React.FC = ({ }; const StudentStatusSuggestion: React.FC = ({ - statusSuggestion + statusSuggestion, }: StatusSuggestionProp) => { let myLabel = question_mark; let myColor = 'text-check-orange'; From f33371d7b7a38cd3aa87b28f8f269545c9f2b8f0 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Wed, 11 May 2022 12:08:43 +0200 Subject: [PATCH 069/425] chore: added/updated exceptions --- .../exceptions/InvalidRefreshTokenException.kt | 13 +++++++++++++ .../backend/exceptions/InvalidTokenException.kt | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidRefreshTokenException.kt diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidRefreshTokenException.kt b/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidRefreshTokenException.kt new file mode 100644 index 000000000..5256c47b6 --- /dev/null +++ b/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidRefreshTokenException.kt @@ -0,0 +1,13 @@ +package be.osoc.team1.backend.exceptions + +import be.osoc.team1.backend.services.TokenService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus +/** + * This exception is thrown by [TokenService] when authorization failed due to an invalid token. + */ +@ResponseStatus(value = HttpStatus.I_AM_A_TEAPOT) +class InvalidRefreshTokenException( + message: String = "Invalid refresh token given", + cause: Throwable? = null +) : Exception(message, cause) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidTokenException.kt b/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidTokenException.kt index e0116c7b5..0f949df24 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidTokenException.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/exceptions/InvalidTokenException.kt @@ -1,10 +1,11 @@ package be.osoc.team1.backend.exceptions +import be.osoc.team1.backend.security.TokenUtil import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus /** - * This exception is thrown by TokenUtil when authorization failed due to an invalid token. + * This exception is thrown by [TokenUtil] when authorization failed due to an invalid token. */ @ResponseStatus(value = HttpStatus.BAD_REQUEST) class InvalidTokenException( From 812f4a7e66f6df585b9fcc4648e1e765ffc7314d Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Wed, 11 May 2022 12:09:49 +0200 Subject: [PATCH 070/425] chore: added exceptions --- .../kotlin/be/osoc/team1/backend/services/TokenService.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt index 2f01bc3c2..3e01ea2c8 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt @@ -1,5 +1,6 @@ package be.osoc.team1.backend.services +import be.osoc.team1.backend.exceptions.InvalidRefreshTokenException import be.osoc.team1.backend.exceptions.InvalidTokenException import be.osoc.team1.backend.security.TokenUtil.decodeAndVerifyToken import be.osoc.team1.backend.security.TokenUtil.refreshTokenRotation @@ -14,11 +15,11 @@ class TokenService { */ fun renewAccessToken(request: HttpServletRequest, response: HttpServletResponse) { val refreshToken: String = request.getParameter("refreshToken") - ?: throw InvalidTokenException("No refresh token found in request body.") + ?: throw InvalidRefreshTokenException("No refresh token found in request body.") val decodedToken = decodeAndVerifyToken(refreshToken) if (decodedToken.getClaim("isAccessToken").asBoolean()) { - throw InvalidTokenException("Expected a refresh token, got an access token.") + throw InvalidRefreshTokenException("Expected a refresh token, got an access token.") } val email: String = decodedToken.subject From 232f4a3008239c2eef7dc8850dad8d7851ef2ae4 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Wed, 11 May 2022 12:13:53 +0200 Subject: [PATCH 071/425] test: fixed test --- .../be/osoc/team1/backend/unittests/TokenServiceTests.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenServiceTests.kt index ba4cf717b..a4d6e7d5e 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/TokenServiceTests.kt @@ -1,6 +1,6 @@ package be.osoc.team1.backend.unittests -import be.osoc.team1.backend.exceptions.InvalidTokenException +import be.osoc.team1.backend.exceptions.InvalidRefreshTokenException import be.osoc.team1.backend.security.TokenUtil import be.osoc.team1.backend.security.TokenUtil.decodeAndVerifyToken import be.osoc.team1.backend.security.TokenUtil.refreshTokenRotation @@ -24,7 +24,7 @@ class TokenServiceTests { fun `renewAccessToken fails if no token given`() { val request = MockHttpServletRequest() val response = MockHttpServletResponse() - val exception = assertThrows { tokenService.renewAccessToken(request, response) } + val exception = assertThrows { tokenService.renewAccessToken(request, response) } assertEquals("No refresh token found in request body.", exception.message) } @@ -35,7 +35,7 @@ class TokenServiceTests { val accessToken = TokenUtil.createAccessAndRefreshToken(testEmail, testAuthorities).accessToken request.addParameter("refreshToken", accessToken) - val exception = assertThrows { tokenService.renewAccessToken(request, response) } + val exception = assertThrows { tokenService.renewAccessToken(request, response) } assertEquals("Expected a refresh token, got an access token.", exception.message) } From 6d998828fb4cc79869393ad435cda0627bcd45b1 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Wed, 11 May 2022 12:13:58 +0200 Subject: [PATCH 072/425] chore: ktlint --- .../main/kotlin/be/osoc/team1/backend/services/TokenService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt index 3e01ea2c8..9f85a391e 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/TokenService.kt @@ -1,7 +1,6 @@ package be.osoc.team1.backend.services import be.osoc.team1.backend.exceptions.InvalidRefreshTokenException -import be.osoc.team1.backend.exceptions.InvalidTokenException import be.osoc.team1.backend.security.TokenUtil.decodeAndVerifyToken import be.osoc.team1.backend.security.TokenUtil.refreshTokenRotation import org.springframework.stereotype.Service From e9fd155d7ec2555ce00a682355a138cbe8a12f5f Mon Sep 17 00:00:00 2001 From: Michael M Date: Wed, 11 May 2022 15:25:00 +0200 Subject: [PATCH 073/425] chore: fix polling projects bug --- frontend/components/projects/ProjectTile.tsx | 20 +++++++++++--------- frontend/hooks/useOnScreen.ts | 8 +++----- frontend/pages/[editionName]/projects.tsx | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/components/projects/ProjectTile.tsx b/frontend/components/projects/ProjectTile.tsx index e433b4d6b..793446f32 100644 --- a/frontend/components/projects/ProjectTile.tsx +++ b/frontend/components/projects/ProjectTile.tsx @@ -455,15 +455,17 @@ const ProjectTile: React.FC = ({ {/* assigned students list */}
- {myProject.assignments.map((assignment) => ( - - ))} + {myProject.assignments + .sort((one, two) => (one > two ? -1 : 1)) + .map((assignment) => ( + + ))}
{loading && ( diff --git a/frontend/hooks/useOnScreen.ts b/frontend/hooks/useOnScreen.ts index af535c530..b495450de 100644 --- a/frontend/hooks/useOnScreen.ts +++ b/frontend/hooks/useOnScreen.ts @@ -17,15 +17,13 @@ export default function useOnScreen(ref: RefObject) { useEffect(() => { if (observerRef.current && ref.current) { - observerRef.current.observe(ref.current); + observerRef.current?.observe(ref.current); return () => { - if (observerRef.current) { - observerRef.current.disconnect(); - } + observerRef.current?.disconnect(); }; } - }, [ref]); + }, [ref.current]); return isOnScreen; } diff --git a/frontend/pages/[editionName]/projects.tsx b/frontend/pages/[editionName]/projects.tsx index 5137318ed..6ed9f55e7 100644 --- a/frontend/pages/[editionName]/projects.tsx +++ b/frontend/pages/[editionName]/projects.tsx @@ -115,8 +115,8 @@ const Projects: NextPage = () => { const [projectForm, setProjectForm] = useState( JSON.parse(JSON.stringify({ ...defaultprojectForm })) ); - const elementRef = useRef(null); - const isOnScreen = useOnScreen(elementRef); + const elementRef1 = useRef(null); + const isOnScreen = useOnScreen(elementRef1); let controller = new AbortController(); useAxiosAuth(); @@ -291,7 +291,6 @@ const Projects: NextPage = () => { {/* Holds the projects searchbar + project tiles */}
{
{/* TODO add an easy reset/undo search button */} {/* TODO either move search icon left and add xmark to the right or vice versa */} From 95e2b6305f622c5a97b951565ddee303daaf1e3c Mon Sep 17 00:00:00 2001 From: Michael M Date: Wed, 11 May 2022 15:27:51 +0200 Subject: [PATCH 074/425] chore: remove unneeded existence check --- frontend/hooks/useOnScreen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/hooks/useOnScreen.ts b/frontend/hooks/useOnScreen.ts index b495450de..4217f9019 100644 --- a/frontend/hooks/useOnScreen.ts +++ b/frontend/hooks/useOnScreen.ts @@ -17,7 +17,7 @@ export default function useOnScreen(ref: RefObject) { useEffect(() => { if (observerRef.current && ref.current) { - observerRef.current?.observe(ref.current); + observerRef.current.observe(ref.current); return () => { observerRef.current?.disconnect(); From 0f7d728aeda9aed6c329178a084fc5059a5723c6 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 12 May 2022 11:31:40 +0200 Subject: [PATCH 075/425] test: increase code coverage --- .../osoc/team1/backend/security/ConfigUtil.kt | 4 ++-- .../unittests/ForgotPasswordServiceTests.kt | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt index 6ca30f15b..0c6a8a1d5 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ConfigUtil.kt @@ -6,10 +6,10 @@ package be.osoc.team1.backend.security */ object ConfigUtil { val urlsOpenToAll: Array = arrayOf( - "/", "/login", "/logout", "/error", "/users/forgotPassword/*" + "/", "/login", "/logout", "/error", "/forgotPassword/*" ) val urlsOpenToAllToPostTo: Array = arrayOf( - "/users", "/users/forgotPassword", "/token/refresh", "/*/students" + "/users", "/forgotPassword", "/token/refresh", "/*/students" ) val allowedCorsOrigins: List = listOf("http://localhost:3000", "https://sel2-1.ugent.be") } diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt index 88a2d5535..db53e9073 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt @@ -27,16 +27,22 @@ class ForgotPasswordServiceTests { } @Test - fun `sendEmailWithToken does not fail`() { + fun `sendEmailWithToken does not fail when email is valid`() { val service = ForgotPasswordService(getRepository(), mockk()) service.sendEmailWithToken(testEmail) } + @Test + fun `sendEmailWithToken does not fail when email is invalid`() { + val service = ForgotPasswordService(getRepository(false), mockk()) + service.sendEmailWithToken(testEmail) + } + @Test fun `changePassword fails when forgotPasswordUUID is invalid`() { val invalidUUID = UUID.randomUUID() mockkObject(ForgotPasswordUtil) - every { ForgotPasswordUtil.getEmailFromUUID(invalidUUID) } throws InvalidForgotPasswordUUIDException() + every { ForgotPasswordUtil.getEmailFromUUID(invalidUUID) } returns null val service = ForgotPasswordService(getRepository(), mockk()) Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { @@ -46,9 +52,7 @@ class ForgotPasswordServiceTests { @Test fun `changePassword fails when email is invalid`() { - val validUUID = UUID.randomUUID() - mockkObject(ForgotPasswordUtil) - every { ForgotPasswordUtil.getEmailFromUUID(validUUID) } returns testEmail + val validUUID = ForgotPasswordUtil.newToken(testEmail) val service = ForgotPasswordService(getRepository(false), mockk()) Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { @@ -58,9 +62,7 @@ class ForgotPasswordServiceTests { @Test fun `changePassword does not fail when valid arguments given`() { - val validUUID = UUID.randomUUID() - mockkObject(ForgotPasswordUtil) - every { ForgotPasswordUtil.getEmailFromUUID(validUUID) } returns testEmail + val validUUID = ForgotPasswordUtil.newToken(testEmail) val passwordEncoder: PasswordEncoder = mockk() every { passwordEncoder.encode(any()) } returns "Encoded password" From 275fc0835fff2a82402b3f9ed86af2ee6a14fd8d Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 12 May 2022 11:51:26 +0200 Subject: [PATCH 076/425] test: increase code coverage --- .../backend/unittests/ForgotPasswordServiceTests.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt index db53e9073..42ee859bb 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ForgotPasswordServiceTests.kt @@ -8,7 +8,6 @@ import be.osoc.team1.backend.security.ForgotPasswordUtil import be.osoc.team1.backend.services.ForgotPasswordService import io.mockk.every import io.mockk.mockk -import io.mockk.mockkObject import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.springframework.security.crypto.password.PasswordEncoder @@ -41,29 +40,26 @@ class ForgotPasswordServiceTests { @Test fun `changePassword fails when forgotPasswordUUID is invalid`() { val invalidUUID = UUID.randomUUID() - mockkObject(ForgotPasswordUtil) - every { ForgotPasswordUtil.getEmailFromUUID(invalidUUID) } returns null - val service = ForgotPasswordService(getRepository(), mockk()) - Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { + val exception = Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { service.changePassword(invalidUUID, newPassword) } + Assertions.assertEquals("forgotPasswordUUID is invalid.", exception.message) } @Test fun `changePassword fails when email is invalid`() { val validUUID = ForgotPasswordUtil.newToken(testEmail) - val service = ForgotPasswordService(getRepository(false), mockk()) - Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { + val exception = Assertions.assertThrows(InvalidForgotPasswordUUIDException().javaClass) { service.changePassword(validUUID, newPassword) } + Assertions.assertEquals("ForgotPasswordToken contains invalid email.", exception.message) } @Test fun `changePassword does not fail when valid arguments given`() { val validUUID = ForgotPasswordUtil.newToken(testEmail) - val passwordEncoder: PasswordEncoder = mockk() every { passwordEncoder.encode(any()) } returns "Encoded password" val service = ForgotPasswordService(getRepository(), passwordEncoder) From 5670425cc163eaee8a9feeb986fb3d70405fd62d Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 12 May 2022 12:07:09 +0200 Subject: [PATCH 077/425] style: cleanup --- .../osoc/team1/backend/security/EmailUtil.kt | 1 - frontend/lib/endpoints.ts | 2 +- frontend/pages/forgotPassword.tsx | 5 ++--- .../forgotPassword/[forgotPasswordToken].tsx | 18 ++---------------- 4 files changed, 5 insertions(+), 21 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index e8f740828..cb51bb629 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -60,7 +60,6 @@ object EmailUtil { email.setText(getForgotPasswordEmailBody(forgotPasswordUUID)) email.setTo(emailAddressReceiver) email.setFrom(emailAddressSender) - getMailSender().send(email) } } diff --git a/frontend/lib/endpoints.ts b/frontend/lib/endpoints.ts index 7b8887f2e..a65edfaf2 100644 --- a/frontend/lib/endpoints.ts +++ b/frontend/lib/endpoints.ts @@ -11,7 +11,7 @@ enum Endpoints { PROJECTS = '/projects', STUDENTS = '/students', SKILLS = '/skills', - RESETPASSWORD = '/forgotPassword', + FORGOTPASSWORD = '/forgotPassword', } export default Endpoints; diff --git a/frontend/pages/forgotPassword.tsx b/frontend/pages/forgotPassword.tsx index efe2b9e60..3e753d977 100644 --- a/frontend/pages/forgotPassword.tsx +++ b/frontend/pages/forgotPassword.tsx @@ -24,11 +24,10 @@ const ForgotPassword: NextPage = () => { if (email) { try { - const response = await axios.post(Endpoints.RESETPASSWORD, email, { + const response = await axios.post(Endpoints.FORGOTPASSWORD, email, { headers: { 'Content-Type': 'text/plain' }, }); - console.log(response); - // router.push('/'); + toast.success( (t) => ( diff --git a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx index ed547b9a9..c691c8119 100644 --- a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx +++ b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx @@ -23,19 +23,17 @@ const ForgotPassword: NextPage = () => { const doSubmit: FormEventHandler = async (e) => { e.preventDefault(); - console.log('password:' + password); - console.log('validPassword:' + validPassword); if (validPassword) { try { const response = await axios.patch( - Endpoints.RESETPASSWORD + '/' + token, + Endpoints.FORGOTPASSWORD + '/' + token, validPassword, { headers: { 'Content-Type': 'text/plain' }, } ); - // router.push('/'); + if (response?.data) { toast.success( (t) => ( @@ -53,18 +51,6 @@ const ForgotPassword: NextPage = () => { { duration: 12000 } ); } - toast.success( - (t) => ( - - Success
- Password has been reset
- -
- ), - { duration: 12000 } - ); } catch (err) { console.log(err); toast.error('An error occurred while trying to reset password.'); From f1972b72df84a6e65ae757a29e6720c51ef95dd2 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 12 May 2022 12:37:09 +0200 Subject: [PATCH 078/425] fix: valid password check bug fix --- .../osoc/team1/backend/security/EmailUtil.kt | 4 +- .../backend/security/ForgotPasswordToken.kt | 2 +- .../forgotPassword/[forgotPasswordToken].tsx | 59 +++++++++---------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index cb51bb629..c86f3e494 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -24,9 +24,9 @@ object EmailUtil { return """ Hi, - Trouble signing in? - Resetting your password is easy. + Trouble signing in? Resetting your password is easy. Use the link below to choose a new password. + You can only use this link once to reset your password and it is only valid for 20 minutes. $url (if this link isn't clickable, you can copy and paste it into search bar) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt index b97e4c21f..494383a10 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/ForgotPasswordToken.kt @@ -6,7 +6,7 @@ package be.osoc.team1.backend.security */ data class ForgotPasswordToken( val emailAddress: String, - val ttl: Long = System.currentTimeMillis() + 20 * 60 * 1000 // 20 minutes + val ttl: Long = System.currentTimeMillis() + 20 * 60 * 1000 ) { fun isExpired(): Boolean { return ttl < System.currentTimeMillis() diff --git a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx index c691c8119..c361707f8 100644 --- a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx +++ b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx @@ -19,42 +19,39 @@ const ForgotPassword: NextPage = () => { useEffect(() => { setValidPassword(customPasswordRegex.test(password)); - }, [validPassword]); + }, [password]); const doSubmit: FormEventHandler = async (e) => { e.preventDefault(); + if (!validPassword) { return; } - if (validPassword) { - try { - const response = await axios.patch( - Endpoints.FORGOTPASSWORD + '/' + token, - validPassword, - { - headers: { 'Content-Type': 'text/plain' }, - } - ); - - if (response?.data) { - toast.success( - (t) => ( - - Password reset
- Password has been reset to {validPassword}
- -
- ), - { duration: 12000 } - ); + try { + const response = await axios.patch( + Endpoints.FORGOTPASSWORD + '/' + token, + password, + { + headers: { 'Content-Type': 'text/plain' }, } - } catch (err) { - console.log(err); - toast.error('An error occurred while trying to reset password.'); - } + ); + + toast.success( + (t) => ( + + Password reset
+ Password has been reset.
+ +
+ ), + { duration: 12000 } + ); + } catch (err) { + console.log(err); + toast.error('An error occurred while trying to reset password.'); } }; From 44b85b9c05dee16fbc6bcc13b44059b936e67635 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Thu, 12 May 2022 12:42:07 +0200 Subject: [PATCH 079/425] style: fix eslint and prettier --- frontend/pages/forgotPassword/[forgotPasswordToken].tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx index c361707f8..9b06f108d 100644 --- a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx +++ b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx @@ -23,7 +23,7 @@ const ForgotPassword: NextPage = () => { const doSubmit: FormEventHandler = async (e) => { e.preventDefault(); - if (!validPassword) { return; } + if (!validPassword) return; try { const response = await axios.patch( @@ -39,10 +39,7 @@ const ForgotPassword: NextPage = () => { Password reset
Password has been reset.
-
From 22e2f07e191d2563c4ecb519c445d68e40271508 Mon Sep 17 00:00:00 2001 From: NielsPraet Date: Thu, 12 May 2022 13:09:20 +0200 Subject: [PATCH 080/425] chore: delete console logging --- frontend/components/PersistLogin.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/components/PersistLogin.tsx b/frontend/components/PersistLogin.tsx index 9eabc7930..79f304997 100644 --- a/frontend/components/PersistLogin.tsx +++ b/frontend/components/PersistLogin.tsx @@ -55,7 +55,8 @@ const PersistLogin: FC> = ({ await refresh(); } catch (err) { - console.log(err); + // currently nothing happens here, but we can choose to + // push users to the login page from here if that pleases us } finally { setLoading(false); } From 0ac3ffd471a8d320d3768595326e44d0d194fb0f Mon Sep 17 00:00:00 2001 From: NielsPraet Date: Thu, 12 May 2022 13:19:04 +0200 Subject: [PATCH 081/425] chore: run prettier --- frontend/components/Header.tsx | 28 +++++++------- frontend/pages/[editionName]/projects.tsx | 47 +++++++++++++---------- frontend/pages/[editionName]/students.tsx | 24 +++++------- 3 files changed, 49 insertions(+), 50 deletions(-) diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 1d19a099f..5232ce199 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -29,20 +29,20 @@ const Header: React.FC = () => {
    {edition && ( <> -
  • - Select Students -
  • -
  • - Projects -
  • +
  • + Select Students +
  • +
  • + Projects +
  • )}
  • { } absolute left-[24px] top-[16px] z-50 flex flex-col justify-center text-[30px] opacity-20 md:hidden`} > setShowSidebar(!showSidebar)}>{arrow_in} -
- null} /> -
- - {/* Holds the projects searchbar + project tiles */} -
-
- {/* button to open sidebar on mobile */} -
- {/* button to close sidebar on mobile */} - setShowSidebar(!showSidebar)}>{arrow_in} -
null} />
+ {/* Holds the projects searchbar + project tiles */} +
+
+ {/* button to open sidebar on mobile */} +
+ {/* button to close sidebar on mobile */} + setShowSidebar(!showSidebar)}> + {arrow_in} + +
+ null} + /> +
+
+ {/* Holds the projects searchbar + project tiles */}
{ useAxiosAuth(); return ( - - + -
-
- -
- {/* Holds the sidebar with search, filter and student results */} -
- {/* button to close sidebar on mobile */} -
+
+ +
+ {/* Holds the sidebar with search, filter and student results */} +
+ {/* button to close sidebar on mobile */}
Date: Thu, 12 May 2022 14:52:43 +0200 Subject: [PATCH 082/425] refactor: clean up header code --- frontend/components/Header.tsx | 91 ++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 6effc18ed..2655e92f6 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -1,17 +1,48 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; -import { PropsWithChildren } from 'react'; +import { FC, PropsWithChildren } from 'react'; import useUser from '../hooks/useUser'; import { UserRole } from '../lib/types'; import useEdition from '../hooks/useEdition'; type HeaderProps = PropsWithChildren; -const Header: React.FC = () => { +type EditionHeaderLinkProps = HeaderLinkProps; + +const EditionHeaderLink: FC = ({ href, children }) => { + + const [edition] = useEdition(); + + return ( + { children } + + ) +} + +type HeaderLinkProps = PropsWithChildren<{ + href: string; +}> + +const HeaderLink: FC = ({ href, children }: HeaderLinkProps) => { const router = useRouter(); + const current_path = router.pathname; + + return ( +
  • + { children } +
  • + ) +} + +const Header: React.FC = () => { const [user] = useUser(); const [edition] = useEdition(); - const current_path = router.pathname; return (
    @@ -27,37 +58,33 @@ const Header: React.FC = () => {
    From cc2b815a5f78cbf839c2064b268085b398f29397 Mon Sep 17 00:00:00 2001 From: Michael M Date: Sun, 15 May 2022 13:00:55 +0200 Subject: [PATCH 099/425] chore: remove dialect from properties --- backend/src/main/resources/application.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5245626bc..f05f4611d 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -4,7 +4,6 @@ spring.datasource.username=postgres spring.datasource.password=postgres spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL81Dialect spring.jpa.generate-ddl=true server.servlet.context-path=/api From cdd4f4e66dacfd06845e46ee3ce87b0b045beaca Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 15 May 2022 13:58:22 +0200 Subject: [PATCH 100/425] test: added test for view --- .../unittests/StudentControllerTests.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt index 2b1af11d8..178fc7e3e 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt @@ -9,6 +9,7 @@ import be.osoc.team1.backend.entities.Skill import be.osoc.team1.backend.entities.StatusEnum import be.osoc.team1.backend.entities.StatusSuggestion import be.osoc.team1.backend.entities.Student +import be.osoc.team1.backend.entities.StudentView import be.osoc.team1.backend.entities.SuggestionEnum import be.osoc.team1.backend.entities.User import be.osoc.team1.backend.entities.filterByName @@ -32,6 +33,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ObjectNode import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.all import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -111,6 +113,30 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { mockMvc.perform(get(editionUrl).principal(defaultPrincipal)).andExpect(status().isOk) } + @Test + fun `getAllStudents Basic view returns a basic form of students`() { + val testStudent1 = Student("L1", "VC", testEdition) + val testStudent2 = Student("L2", "VC", testEdition) + val testStudent3 = Student("L3", "VC", testEdition) + val testStudent4 = Student("L4", "VC", testEdition) + val allStudents = listOf(testStudent1, testStudent2, testStudent3, testStudent4) + every { studentService.getAllStudents(defaultSort, testEdition) } returns allStudents + mockMvc.perform(get("$editionUrl?view=Basic").principal(defaultPrincipal)).andExpect(status().isOk) + .andExpect(content().json(objectMapper.writerWithView(StudentView.Basic::class.java).writeValueAsString(PagedCollection(allStudents,4)))) + } + + @Test + fun `getAllStudents Full view returns the full form of students`() { + val testStudent1 = Student("L1", "VC", testEdition) + val testStudent2 = Student("L2", "VC", testEdition) + val testStudent3 = Student("L3", "VC", testEdition) + val testStudent4 = Student("L4", "VC", testEdition) + val allStudents = listOf(testStudent1, testStudent2, testStudent3, testStudent4) + every { studentService.getAllStudents(defaultSort, testEdition) } returns allStudents + mockMvc.perform(get("$editionUrl?view=Full").principal(defaultPrincipal)).andExpect(status().isOk) + .andExpect(content().json(objectMapper.writeValueAsString(PagedCollection(allStudents,4)))) + } + @Test fun `getAllStudents paging returns the correct amount`() { val allStudents = listOf( From 25d9d180e774c11ef341979c4ee71708068ade6e Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 15 May 2022 13:58:30 +0200 Subject: [PATCH 101/425] docs: updated osoc.yaml --- docs/osoc.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/osoc.yaml b/docs/osoc.yaml index b9a558bda..a4e64c856 100644 --- a/docs/osoc.yaml +++ b/docs/osoc.yaml @@ -170,7 +170,6 @@ paths: description: Part of a name of a student that you want to filter on (this string will be made lowercase and spaces will be removed, this is done to more easily get matches). - in: query name: status - required: true schema: type: array items: @@ -181,6 +180,11 @@ paths: schema: type: boolean description: Whether or not to include students you have already added a suggestion for (default value is true) + - in: query + name: view + schema: + $ref: '#/components/schemas/StudentView' + description: The view of the student, with different views the students will contain less or more information (default is Full). summary: Get a list containing all Student objects description: Get a list of all students in the database. This request cannot fail. responses: @@ -1579,6 +1583,9 @@ components: example: OSOC 2022 isActive: type: boolean + StudentView: + type: string + enum: [ Full, Basic ] securitySchemes: BasicAuth: type: http From df0ef01ecc6a2611881bf0f2d779b49b2c2c7303 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 15 May 2022 13:59:43 +0200 Subject: [PATCH 102/425] chore: ktlint --- .../osoc/team1/backend/unittests/StudentControllerTests.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt index 178fc7e3e..2e9193939 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/StudentControllerTests.kt @@ -33,7 +33,6 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ObjectNode import com.ninjasquad.springmockk.MockkBean -import io.kotest.assertions.all import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -122,7 +121,7 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { val allStudents = listOf(testStudent1, testStudent2, testStudent3, testStudent4) every { studentService.getAllStudents(defaultSort, testEdition) } returns allStudents mockMvc.perform(get("$editionUrl?view=Basic").principal(defaultPrincipal)).andExpect(status().isOk) - .andExpect(content().json(objectMapper.writerWithView(StudentView.Basic::class.java).writeValueAsString(PagedCollection(allStudents,4)))) + .andExpect(content().json(objectMapper.writerWithView(StudentView.Basic::class.java).writeValueAsString(PagedCollection(allStudents, 4)))) } @Test @@ -134,7 +133,7 @@ class StudentControllerTests(@Autowired private val mockMvc: MockMvc) { val allStudents = listOf(testStudent1, testStudent2, testStudent3, testStudent4) every { studentService.getAllStudents(defaultSort, testEdition) } returns allStudents mockMvc.perform(get("$editionUrl?view=Full").principal(defaultPrincipal)).andExpect(status().isOk) - .andExpect(content().json(objectMapper.writeValueAsString(PagedCollection(allStudents,4)))) + .andExpect(content().json(objectMapper.writeValueAsString(PagedCollection(allStudents, 4)))) } @Test From b35a8aa9d8e6a8538b11c8bb95305cc916b066c6 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 15 May 2022 14:07:46 +0200 Subject: [PATCH 103/425] docs: added documentation --- .../team1/backend/controllers/StudentController.kt | 11 +++++++++-- .../kotlin/be/osoc/team1/backend/entities/Student.kt | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index ca80b7e49..374f1ae97 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -37,7 +37,7 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import java.net.URLDecoder import java.security.Principal -import java.util.UUID +import java.util.* import javax.servlet.http.HttpServletResponse @RestController @@ -57,6 +57,9 @@ class StudentController( * by [status] (default value allows all statuses), by [includeSuggested] (default value is true, so * you will also see students you already suggested for), by [skills], by only alumni students([alumnOnly]), by only student coach * volunteers([studentCoachOnly]) and by only unassigned students ([unassignedOnly]) students. + * + * The returned students can also be altered using the [view] query parameter: [Basic] will limit the data the student object contains, + * [Full] will return the full object. */ @GetMapping @Secured("ROLE_COACH") @@ -175,7 +178,11 @@ class StudentController( @ResponseStatus(value = HttpStatus.NO_CONTENT) @Secured("ROLE_ADMIN") @SecuredEdition - fun setStudentStatus(@PathVariable studentId: UUID, @RequestBody status: StatusEnum, @PathVariable edition: String) = + fun setStudentStatus( + @PathVariable studentId: UUID, + @RequestBody status: StatusEnum, + @PathVariable edition: String + ) = service.setStudentStatus(studentId, status, edition) /** diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt index eec953770..542c020cf 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt @@ -1,5 +1,6 @@ package be.osoc.team1.backend.entities +import be.osoc.team1.backend.controllers.StudentController import be.osoc.team1.backend.repositories.AssignmentRepository import be.osoc.team1.backend.services.nameMatchesSearchQuery import be.osoc.team1.backend.util.AnswerListSerializer @@ -171,11 +172,19 @@ class Student( fun calculateStatusSuggestionPercentage(): Map = statusSuggestions.groupingBy { it.status }.eachCount() } +/** + * This class represents a few views which can be used by entities. A field marked as [Full] will not be displayed + * when writerWithView [Basic] is used, If writerWithView [Full] is used both the [Basic] and [Full] fields will be + * displayed, this because [Full] inherits [Basic]. + */ class StudentView { open class Basic open class Full : Basic() } +/** + * Enum to represent the [StudentView]s in the [StudentController] + */ enum class StudentViewEnum { Basic, Full } From ce3a7f973ea29d9630b1f8d141a70eb66c643169 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Sun, 15 May 2022 14:10:39 +0200 Subject: [PATCH 104/425] chore: ktlint --- .../be/osoc/team1/backend/controllers/StudentController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index 374f1ae97..cee176368 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -37,7 +37,7 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import java.net.URLDecoder import java.security.Principal -import java.util.* +import java.util.UUID import javax.servlet.http.HttpServletResponse @RestController From db04635da0d2ceca0eb9c7ffc6fb1b01e5dee9ed Mon Sep 17 00:00:00 2001 From: Michael M Date: Sun, 15 May 2022 15:21:09 +0200 Subject: [PATCH 105/425] feat: show conflicts and do conflicts polling --- frontend/components/projects/ProjectTile.tsx | 13 ++- frontend/lib/endpoints.ts | 1 + frontend/lib/types.ts | 8 ++ frontend/pages/[editionName]/projects.tsx | 103 +++++++++++++++++-- 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/frontend/components/projects/ProjectTile.tsx b/frontend/components/projects/ProjectTile.tsx index 793446f32..603596e98 100644 --- a/frontend/components/projects/ProjectTile.tsx +++ b/frontend/components/projects/ProjectTile.tsx @@ -36,6 +36,7 @@ const edit_icon = ; type ProjectProp = { projectInput: ProjectBase; refreshProjects: () => void; + conflictStudents: UUID[]; }; type UserProp = { @@ -51,6 +52,7 @@ type AssignmentProp = { setOpenUnassignment: (openUnAssignment: boolean) => void; setAssignmentId: (assignmentId: UUID) => void; setRemoveStudentName: (removeStudentName: string) => void; + conflictStudents: UUID[]; }; /** @@ -280,6 +282,7 @@ async function getEntireProject( const ProjectTile: React.FC = ({ projectInput, refreshProjects, + conflictStudents, }: ProjectProp) => { const router = useRouter(); const [user] = useUser(); @@ -464,6 +467,7 @@ const ProjectTile: React.FC = ({ setOpenUnassignment={setOpenUnassignment} setAssignmentId={setAssignmentId} setRemoveStudentName={setRemoveStudentName} + conflictStudents={conflictStudents} /> ))} @@ -749,12 +753,19 @@ const ProjectAssignmentsList: React.FC = ({ setAssignmentId, setOpenUnassignment, setRemoveStudentName, + conflictStudents, }: AssignmentProp) => { return (
    -

    +

    {assignment.student.firstName + ' ' + assignment.student.lastName}

    diff --git a/frontend/lib/endpoints.ts b/frontend/lib/endpoints.ts index 1d85e7a84..46562a834 100644 --- a/frontend/lib/endpoints.ts +++ b/frontend/lib/endpoints.ts @@ -11,6 +11,7 @@ enum Endpoints { PROJECTS = '/projects', STUDENTS = '/students', SKILLS = '/skills', + CONFLICTS = '/projects/conflicts', } export default Endpoints; diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 7a0c1c453..21446e3ae 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -205,3 +205,11 @@ export type Answer = { export const ItemTypes = { STUDENTTILE: 'studentTile', }; + +/** + * This is one element from the exact collection type returned by a get to the Conflicts endpoint + */ +export type Conflict = { + student: Url; + projects: Url[]; +}; diff --git a/frontend/pages/[editionName]/projects.tsx b/frontend/pages/[editionName]/projects.tsx index 28b4300de..b790e9519 100644 --- a/frontend/pages/[editionName]/projects.tsx +++ b/frontend/pages/[editionName]/projects.tsx @@ -5,7 +5,14 @@ import { Icon } from '@iconify/react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { useEffect, useRef, useState } from 'react'; -import { ProjectBase, ProjectData, UserRole } from '../../lib/types'; +import { + Conflict, + ProjectBase, + ProjectData, + Url, + UserRole, + UUID, +} from '../../lib/types'; import { axiosAuthenticated } from '../../lib/axios'; import Endpoints from '../../lib/endpoints'; import useAxiosAuth from '../../hooks/useAxiosAuth'; @@ -20,7 +27,7 @@ import FlatList from 'flatlist-react'; import useUser from '../../hooks/useUser'; import { SpinnerCircular } from 'spinners-react'; import Error from '../../components/Error'; -import { parseError } from '../../lib/requestUtils'; +import { getUrlList, parseError } from '../../lib/requestUtils'; import RouteProtection from '../../components/RouteProtection'; import { useRouter } from 'next/router'; import { NextRouter } from 'next/dist/client/router'; @@ -95,6 +102,53 @@ function searchProject( }); } +async function searchConflicts( + setProjects: (projects: ProjectBase[]) => void, + setConflictStudents: (conflictStudents: UUID[]) => void, + setLoading: (loading: boolean) => void, + signal: AbortSignal, + setError: (error: string) => void, + router: NextRouter +) { + setLoading(true); + const edition = router.query.editionName as string; + + try { + const conflictsResponse = await axiosAuthenticated.get( + '/' + edition + Endpoints.CONFLICTS, + { + signal: signal, + } + ); + const conflicts = conflictsResponse.data; + const conflictProjects = [] as ProjectBase[]; + const projectUrls = conflicts + .map((conflict) => { + return conflict.projects; + }) + .flat() as Url[]; + await getUrlList( + projectUrls, + conflictProjects, + signal, + setError, + router + ); + + setProjects(conflictProjects); + setConflictStudents( + conflicts.map((conflict) => { + return conflict.student.split('/').pop() as UUID; + }) + ); + } catch (err) { + parseError(err, setError, signal, router); + if (!signal.aborted) { + setLoading(false); + } + } +} + /** * Projects page for OSOC application * @returns Projects page @@ -108,6 +162,8 @@ const Projects: NextPage = () => { const [projectSearch, setProjectSearch] = useState('' as string); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [showConflicts, setShowConflicts] = useState(false); + const [conflictStudents, setConflictStudents] = useState([] as UUID[]); const [projects, setProjects]: [ ProjectBase[], (projects: ProjectBase[]) => void @@ -126,6 +182,12 @@ const Projects: NextPage = () => { return search(); }, []); + useEffect(() => { + if (!showConflicts) { + setConflictStudents([] as UUID[]); + } + }, [showConflicts]); + /** * function to add new project results instead of overwriting old results * @param projectsList - list of projects to add to all projects @@ -190,7 +252,7 @@ const Projects: NextPage = () => { */ usePoll( () => { - if (!state.loading && { isOnScreen }.isOnScreen) { + if (!state.loading && { isOnScreen }.isOnScreen && !showConflicts) { controller.abort(); controller = new AbortController(); const signal = controller.signal; @@ -212,9 +274,24 @@ const Projects: NextPage = () => { return () => { controller.abort(); }; + } else if (!state.loading && { isOnScreen }.isOnScreen && showConflicts) { + controller.abort(); + controller = new AbortController(); + const signal = controller.signal; + searchConflicts( + setProjects, + setConflictStudents, + setLoading, + signal, + setError, + router + ); + return () => { + controller.abort(); + }; } }, - [state, projectSearch, { isOnScreen }.isOnScreen], + [state, projectSearch, { isOnScreen }.isOnScreen, showConflicts], { interval: 3000, } @@ -324,11 +401,19 @@ const Projects: NextPage = () => {
    setProjectSearch(e.target.value)} onKeyPress={(e) => { + if (showConflicts) { + return; + } if (e.key == 'Enter') { return search(); } @@ -337,6 +422,9 @@ const Projects: NextPage = () => { { + if (showConflicts) { + return; + } return search(); }} > @@ -354,9 +442,9 @@ const Projects: NextPage = () => { {/* Button to create new project */} @@ -384,6 +472,7 @@ const Projects: NextPage = () => { )} From 4b910385226f34bba405337d88d02672b6aee452 Mon Sep 17 00:00:00 2001 From: Michael M Date: Sun, 15 May 2022 16:46:26 +0200 Subject: [PATCH 106/425] chore: start changing conflicts for new layout --- frontend/pages/[editionName]/projects.tsx | 89 +++++++++++++++++++---- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/frontend/pages/[editionName]/projects.tsx b/frontend/pages/[editionName]/projects.tsx index b790e9519..213ad2919 100644 --- a/frontend/pages/[editionName]/projects.tsx +++ b/frontend/pages/[editionName]/projects.tsx @@ -9,6 +9,7 @@ import { Conflict, ProjectBase, ProjectData, + StudentBase, Url, UserRole, UUID, @@ -27,7 +28,7 @@ import FlatList from 'flatlist-react'; import useUser from '../../hooks/useUser'; import { SpinnerCircular } from 'spinners-react'; import Error from '../../components/Error'; -import { getUrlList, parseError } from '../../lib/requestUtils'; +import { getUrlMap, parseError } from '../../lib/requestUtils'; import RouteProtection from '../../components/RouteProtection'; import { useRouter } from 'next/router'; import { NextRouter } from 'next/dist/client/router'; @@ -37,6 +38,14 @@ const magnifying_glass = ; const arrow_out = ; const arrow_in = ; +/** + * Helper type to avoid having to change this everywhere + */ +type conflictMapType = Map< + UUID, + { student: StudentBase; projectUrls: Set; amount: number } +>; + /** * function that allows searching projects by name * @@ -105,6 +114,8 @@ function searchProject( async function searchConflicts( setProjects: (projects: ProjectBase[]) => void, setConflictStudents: (conflictStudents: UUID[]) => void, + conflictMap: conflictMapType, + setConflictMap: (map: conflictMapType) => void, setLoading: (loading: boolean) => void, signal: AbortSignal, setError: (error: string) => void, @@ -121,26 +132,67 @@ async function searchConflicts( } ); const conflicts = conflictsResponse.data; - const conflictProjects = [] as ProjectBase[]; - const projectUrls = conflicts - .map((conflict) => { - return conflict.projects; - }) - .flat() as Url[]; - await getUrlList( - projectUrls, - conflictProjects, + + const newConflictMap = {} as conflictMapType; + conflictMap.forEach((value, key) => { + value.amount = 1; + newConflictMap.set(key, JSON.parse(JSON.stringify(value))); + }); + + const newStudents = conflicts + .map((conflict) => conflict.student as Url) + .filter((student) => + newConflictMap.has(student.split('/').pop() as UUID) + ); + + const newConflictStudents = {} as Map; + await getUrlMap( + newStudents, + newConflictStudents, signal, setError, router ); - setProjects(conflictProjects); - setConflictStudents( - conflicts.map((conflict) => { - return conflict.student.split('/').pop() as UUID; - }) - ); + for (const conflict of conflicts) { + const studId = conflict.student.split('/').pop() as UUID; + const value = newConflictMap.get(studId); + if (value) { + conflict.projects.forEach((item) => value.projectUrls.add(item)); + value.amount = conflict.projects.length; + newConflictMap.set(studId, value); + } else { + const newValue = { + projectUrls: new Set(conflict.projects), + amount: conflict.projects.length, + student: newConflictStudents.get(conflict.student) as StudentBase, + }; + newConflictMap.set(studId, newValue); + } + } + + setConflictMap(newConflictMap); + + // const conflictProjects = [] as ProjectBase[]; + // const projectUrls = conflicts + // .map((conflict) => { + // return conflict.projects; + // }) + // .flat() as Url[]; + // await getUrlList( + // projectUrls, + // conflictProjects, + // signal, + // setError, + // router + // ); + // + // setProjects(conflictProjects); + // setConflictStudents( + // conflicts.map((conflict) => { + // return conflict.student.split('/').pop() as UUID; + // }) + // ); } catch (err) { parseError(err, setError, signal, router); if (!signal.aborted) { @@ -164,6 +216,8 @@ const Projects: NextPage = () => { const [error, setError] = useState(''); const [showConflicts, setShowConflicts] = useState(false); const [conflictStudents, setConflictStudents] = useState([] as UUID[]); + const [conflictMap, setConflictMap] = useState({} as conflictMapType); + // const [conflictsLoaded, setConflictsLoaded] = useState(false); const [projects, setProjects]: [ ProjectBase[], (projects: ProjectBase[]) => void @@ -184,6 +238,7 @@ const Projects: NextPage = () => { useEffect(() => { if (!showConflicts) { + setConflictMap({} as conflictMapType); setConflictStudents([] as UUID[]); } }, [showConflicts]); @@ -281,6 +336,8 @@ const Projects: NextPage = () => { searchConflicts( setProjects, setConflictStudents, + conflictMap, + setConflictMap, setLoading, signal, setError, From 511d0160036f0a460b445d16d91af7a4fbce7b2d Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Sun, 15 May 2022 17:28:41 +0200 Subject: [PATCH 107/425] style: improve backend according to pr review --- .../osoc/team1/backend/security/EmailUtil.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt index c86f3e494..782936fe2 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/EmailUtil.kt @@ -12,6 +12,7 @@ import java.util.UUID object EmailUtil { /** * Set email account to send emails with. + * The password below is a Gmail app password, so it can't be used to log into the Google account. */ private const val emailAddressSender = "opensummerofcode.info@gmail.com" private const val passwordSender = "nharepxthiwygcpj" @@ -28,7 +29,7 @@ object EmailUtil { Use the link below to choose a new password. You can only use this link once to reset your password and it is only valid for 20 minutes. $url - (if this link isn't clickable, you can copy and paste it into search bar) + (if this link isn't clickable, you can copy and paste it into the search bar) If you did not forget your password, please disregard this email. """.trimIndent() @@ -38,11 +39,12 @@ object EmailUtil { * Get a [JavaMailSender] object which is correctly configured. */ private fun getMailSender(): JavaMailSender { - val mailSender = JavaMailSenderImpl() - mailSender.host = "smtp.gmail.com" - mailSender.port = 587 - mailSender.username = emailAddressSender - mailSender.password = passwordSender + val mailSender = JavaMailSenderImpl().apply { + host = "smtp.gmail.com" + port = 587 + username = emailAddressSender + password = passwordSender + } val props: Properties = mailSender.javaMailProperties props["mail.transport.protocol"] = "smtp" props["mail.smtp.auth"] = "true" @@ -55,11 +57,12 @@ object EmailUtil { * Email [emailAddressReceiver] with a [forgotPasswordUUID], so [emailAddressReceiver] can reset its email. */ fun sendEmail(emailAddressReceiver: String, forgotPasswordUUID: UUID) { - val email = SimpleMailMessage() - email.setSubject("Reset Password") - email.setText(getForgotPasswordEmailBody(forgotPasswordUUID)) - email.setTo(emailAddressReceiver) - email.setFrom(emailAddressSender) + val email = SimpleMailMessage().apply { + setSubject("Reset Password") + setText(getForgotPasswordEmailBody(forgotPasswordUUID)) + setTo(emailAddressReceiver) + setFrom(emailAddressSender) + } getMailSender().send(email) } } From 861d795fc908c1e11d50597648cdf017407078d0 Mon Sep 17 00:00:00 2001 From: Tymen Van Himme Date: Sun, 15 May 2022 17:44:53 +0200 Subject: [PATCH 108/425] style: small fixes --- backend/pom.xml | 1 + frontend/pages/forgotPassword.tsx | 1 - .../pages/forgotPassword/[forgotPasswordToken].tsx | 10 +++------- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 7005ba572..4779f6d34 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -123,6 +123,7 @@ spring-boot-starter-mail 2.6.7 + org.springframework.boot spring-boot-starter-aop 2.6.7 diff --git a/frontend/pages/forgotPassword.tsx b/frontend/pages/forgotPassword.tsx index 9750aefb3..9b94eaf0d 100644 --- a/frontend/pages/forgotPassword.tsx +++ b/frontend/pages/forgotPassword.tsx @@ -1,5 +1,4 @@ import { NextPage } from 'next'; -import { useRouter } from 'next/router'; import { FormEventHandler, useEffect, useRef } from 'react'; import toast from 'react-hot-toast'; import FormContainer from '../components/FormContainer'; diff --git a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx index e1661e2ea..d7c824cc6 100644 --- a/frontend/pages/forgotPassword/[forgotPasswordToken].tsx +++ b/frontend/pages/forgotPassword/[forgotPasswordToken].tsx @@ -25,13 +25,9 @@ const ForgotPassword: NextPage = () => { if (!validPassword) return; try { - await axios.patch( - Endpoints.FORGOTPASSWORD + '/' + token, - password, - { - headers: { 'Content-Type': 'text/plain' }, - } - ); + await axios.patch(Endpoints.FORGOTPASSWORD + '/' + token, password, { + headers: { 'Content-Type': 'text/plain' }, + }); toast.success( (t) => ( From d1598fc07b06c69605e9b4ac32a87668af740478 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 16 May 2022 09:35:49 +0200 Subject: [PATCH 109/425] docs: updated osoc.yaml --- docs/osoc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/osoc.yaml b/docs/osoc.yaml index a4e64c856..4c20f4828 100644 --- a/docs/osoc.yaml +++ b/docs/osoc.yaml @@ -184,7 +184,7 @@ paths: name: view schema: $ref: '#/components/schemas/StudentView' - description: The view of the student, with different views the students will contain less or more information (default is Full). + description: The view of the student, with different views the students will contain less or more information (default is Full). The Basic view can be used to get basic information of the student to display it in a listing, this way unnecessary data does not need to be sent. summary: Get a list containing all Student objects description: Get a list of all students in the database. This request cannot fail. responses: From 04fb7b43e01ac8e27071871cb82537cc56d4414b Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 16 May 2022 09:36:01 +0200 Subject: [PATCH 110/425] chore: requested changes --- .../osoc/team1/backend/controllers/StudentController.kt | 9 ++++----- .../be/osoc/team1/backend/util/TallyDeserializer.kt | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt index cee176368..9ed69fa6b 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/controllers/StudentController.kt @@ -100,11 +100,11 @@ class StudentController( .applyIf(unassignedOnly) { filterByNotYetAssigned(assignmentRepository) } .page(pager) - return if (view == StudentViewEnum.Full) { - ObjectMapper().writerWithView(StudentView.Full::class.java).writeValueAsString(filteredStudents) - } else { - ObjectMapper().writerWithView(StudentView.Basic::class.java).writeValueAsString(filteredStudents) + val viewType = when (view) { + StudentViewEnum.Full -> StudentView.Full::class.java + StudentViewEnum.Basic -> StudentView.Basic::class.java } + return ObjectMapper().writerWithView(viewType).writeValueAsString(filteredStudents) } /** @@ -151,7 +151,6 @@ class StudentController( studentRegistration.skills, studentRegistration.alumn, studentRegistration.possibleStudentCoach, - // studentRegistration.answers ) student.answers = studentRegistration.answers val createdStudent = service.addStudent(student) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/util/TallyDeserializer.kt b/backend/src/main/kotlin/be/osoc/team1/backend/util/TallyDeserializer.kt index be1ec98c5..00370cae9 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/util/TallyDeserializer.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/util/TallyDeserializer.kt @@ -50,7 +50,6 @@ class TallyDeserializer : StdDeserializer(Student::class.java) { getAnswerForKey( answerMap, TallyKeys.studentCoachQuestion, "studentCoach" ).optionId == TallyKeys.studentCoachYesId, - // answerMap.values.toList(), ) s.answers = answerMap.values.toList() return s From 10d89c8cbd51207f9930ab3a9587e487f14957a4 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 16 May 2022 09:43:09 +0200 Subject: [PATCH 111/425] test: fixed tests --- .../osoc/team1/backend/integrationtests/AuthorizationTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt index 4b7ecda11..48dc5c2ba 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt @@ -399,7 +399,7 @@ class AuthorizationTests { val firstRefreshResponse: ResponseEntity = requestNewAccessToken(refreshToken) assert(firstRefreshResponse.statusCodeValue == 200) val secondRefreshResponse: ResponseEntity = requestNewAccessToken(refreshToken) - assert(secondRefreshResponse.statusCodeValue == 400) + assert(secondRefreshResponse.statusCodeValue == 418) } @Test From 372efe29a62a03263a5540da2310cf3717776cd4 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 16 May 2022 09:54:50 +0200 Subject: [PATCH 112/425] test: more fixed --- .../backend/integrationtests/AuthorizationTests.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt index 48dc5c2ba..b778eeadd 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/integrationtests/AuthorizationTests.kt @@ -367,12 +367,12 @@ class AuthorizationTests { } @Test - fun `use access token to renew access token returns 400`() { + fun `use access token to renew access token returns 418`() { val logInResponse: ResponseEntity = loginUser(adminEmail, adminPassword) val accessToken: String = JSONObject(logInResponse.body).get("accessToken") as String val refreshResponse: ResponseEntity = requestNewAccessToken(accessToken) - assert(refreshResponse.statusCodeValue == 400) + assert(refreshResponse.statusCodeValue == 418) } @Test @@ -392,7 +392,7 @@ class AuthorizationTests { } @Test - fun `using same refresh token twice returns 400`() { + fun `using same refresh token twice returns 418`() { val logInResponse: ResponseEntity = loginUser(adminEmail, adminPassword) val refreshToken: String = JSONObject(logInResponse.body).get("refreshToken") as String @@ -436,7 +436,7 @@ class AuthorizationTests { } @Test - fun `using refresh token after logout returns 400`() { + fun `using refresh token after logout returns 418`() { val logInResponse: ResponseEntity = loginUser(adminEmail, adminPassword) val accessToken = JSONObject(logInResponse.body).get("accessToken") as String val refreshToken = JSONObject(logInResponse.body).get("refreshToken") as String @@ -446,7 +446,7 @@ class AuthorizationTests { assert(logoutResponse.statusCodeValue == 302) val refreshResponse: ResponseEntity = requestNewAccessToken(refreshToken) - assert(refreshResponse.statusCodeValue == 400) + assert(refreshResponse.statusCodeValue == 418) } // Login first to test GET with protected endpoint From e7a1cc9ed97fcd55128ee1128bdb9cf83161a255 Mon Sep 17 00:00:00 2001 From: Tom Alard Date: Mon, 16 May 2022 10:29:10 +0200 Subject: [PATCH 113/425] fix: conflict URL's now include /{edition} --- .../team1/backend/services/ProjectService.kt | 16 +++++++++------- .../be/osoc/team1/backend/util/Serializers.kt | 7 ++++++- .../backend/unittests/ProjectServiceTests.kt | 18 +++++++++--------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt index 7cd8b311a..c75e23df9 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt @@ -11,8 +11,9 @@ import be.osoc.team1.backend.exceptions.InvalidPositionIdException import be.osoc.team1.backend.exceptions.InvalidProjectIdException import be.osoc.team1.backend.exceptions.InvalidUserIdException import be.osoc.team1.backend.repositories.ProjectRepository +import be.osoc.team1.backend.util.ProjectSerializer +import be.osoc.team1.backend.util.StudentSerializer import org.springframework.stereotype.Service -import org.springframework.web.servlet.support.ServletUriComponentsBuilder import java.util.UUID @Service @@ -98,20 +99,21 @@ class ProjectService( * Gets conflicts (a conflict involves a student being assigned to 2 projects at the same time) */ fun getConflicts(edition: String): MutableList { - val baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString() - val studentsMap = mutableMapOf>() + val studentsMap = mutableMapOf>() + val projectSerializer = ProjectSerializer() for (project in getAllProjects(edition)) { for (student in getStudents(project)) { // add project id to map with student as key - studentsMap.putIfAbsent(student.id, mutableListOf()) - studentsMap[student.id]!!.add("$baseUrl/projects/" + project.id) + studentsMap.putIfAbsent(student, mutableListOf()) + studentsMap[student]!!.add(projectSerializer.toUrl(project)) } } val conflicts = mutableListOf() - for ((studentId, projectIds) in studentsMap.entries) { + val studentSerializer = StudentSerializer() + for ((student, projectIds) in studentsMap.entries) { if (projectIds.size > 1) { // this student has a conflict - conflicts.add(Conflict("$baseUrl/students/$studentId", projectIds)) + conflicts.add(Conflict(studentSerializer.toUrl(student), projectIds)) } } return conflicts diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/util/Serializers.kt b/backend/src/main/kotlin/be/osoc/team1/backend/util/Serializers.kt index c5c704845..42bc05270 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/util/Serializers.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/util/Serializers.kt @@ -1,6 +1,7 @@ package be.osoc.team1.backend.util import be.osoc.team1.backend.entities.Position +import be.osoc.team1.backend.entities.Project import be.osoc.team1.backend.entities.Student import be.osoc.team1.backend.entities.User import com.fasterxml.jackson.core.JsonGenerator @@ -12,12 +13,16 @@ import com.fasterxml.jackson.databind.SerializerProvider */ open class Serializer(private val genFunc: (T) -> String) : BaseSerializer() { + fun toUrl(item: T): String = baseUrl + genFunc(item) + override fun serialize(item: T?, gen: JsonGenerator?, provider: SerializerProvider?) = - gen!!.writeObject(baseUrl + genFunc(item!!)) + gen!!.writeObject(toUrl(item!!)) } class PositionSerializer : Serializer({ "/positions/${it.id}" }) class StudentSerializer : Serializer({ "/${it.edition}/students/${it.id}" }) +class ProjectSerializer : Serializer({ "/${it.edition}/projects/${it.id}" }) + class UserSerializer : Serializer({ "/users/${it.id}" }) diff --git a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectServiceTests.kt b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectServiceTests.kt index c5a28f9f9..b77d2c954 100644 --- a/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectServiceTests.kt +++ b/backend/src/test/kotlin/be/osoc/team1/backend/unittests/ProjectServiceTests.kt @@ -189,9 +189,9 @@ class ProjectServiceTests { @Test fun `getConflicts returns the correct result`() { - val testStudent = Student("Lars", "Van Cauter", "") - val testStudent2 = Student("Lars2", "Van Cauter2", "") - val testStudent3 = Student("Lars3", "Van Cauter3", "") + val testStudent = Student("Lars", "Van Cauter", testEdition) + val testStudent2 = Student("Lars2", "Van Cauter2", testEdition) + val testStudent3 = Student("Lars3", "Van Cauter3", testEdition) val position = Position(Skill("backend"), 2) val suggester = User("suggester", "email", Role.Coach, "password") val testProjectConflict = Project( @@ -227,19 +227,19 @@ class ProjectServiceTests { val conflictList = service.getConflicts(testEdition) assert( conflictList[0] == ProjectService.Conflict( - "https://example.com/api/students/" + testStudent.id, + "https://example.com/api/$testEdition/students/" + testStudent.id, mutableListOf( - "https://example.com/api/projects/" + testProjectConflict.id, - "https://example.com/api/projects/" + testProjectConflict2.id + "https://example.com/api/$testEdition/projects/" + testProjectConflict.id, + "https://example.com/api/$testEdition/projects/" + testProjectConflict2.id ) ) ) assert( conflictList[1] == ProjectService.Conflict( - "https://example.com/api/students/" + testStudent2.id, + "https://example.com/api/$testEdition/students/" + testStudent2.id, mutableListOf( - "https://example.com/api/projects/" + testProjectConflict2.id, - "https://example.com/api/projects/" + testProjectConflict3.id + "https://example.com/api/$testEdition/projects/" + testProjectConflict2.id, + "https://example.com/api/$testEdition/projects/" + testProjectConflict3.id ) ) ) From 1e7109338ccc6a7572a396108a932dd0c989205a Mon Sep 17 00:00:00 2001 From: Tom Alard Date: Mon, 16 May 2022 10:32:38 +0200 Subject: [PATCH 114/425] docs: update osoc.yaml --- docs/osoc.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/osoc.yaml b/docs/osoc.yaml index c32414cf3..76d9025ae 100644 --- a/docs/osoc.yaml +++ b/docs/osoc.yaml @@ -846,8 +846,9 @@ paths: items: $ref: '#/components/schemas/Conflict' example: - student: "https://example.com/api/students/abb97568-ac54-11ec-b909-0242ac120002" - projects: [ "https://example.com/api/projects/afc1e1cc-ac54-11ec-b909-0242ac120002", "https://example.com/api/projects/b6a81d12-ac54-11ec-b909-0242ac120002" ] + student: "https://example.com/api/OSOC2022/students/abb97568-ac54-11ec-b909-0242ac120002" + projects: [ "https://example.com/api/OSOC2022/projects/afc1e1cc-ac54-11ec-b909-0242ac120002", + "https://example.com/api/OSOC2022/projects/b6a81d12-ac54-11ec-b909-0242ac120002" ] /{edition}/communications/{communicationId}: get: From ce99f478e9cbd566e5ff05c7d639156b57231270 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 16 May 2022 11:10:45 +0200 Subject: [PATCH 115/425] fix: lowercase emails --- .../be/osoc/team1/backend/security/AuthenticationFilter.kt | 2 +- .../main/kotlin/be/osoc/team1/backend/services/UserService.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthenticationFilter.kt b/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthenticationFilter.kt index dd962166c..2a8ebc10c 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthenticationFilter.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/security/AuthenticationFilter.kt @@ -34,7 +34,7 @@ class AuthenticationFilter(authenticationManager: AuthenticationManager?, privat * [AuthenticationManager.authenticate]. */ override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication { - val email: String? = request.getParameter("email") + val email: String? = request.getParameter("email")?.lowercase() val password: String? = request.getParameter("password") if (email == null || password == null) { throw AuthenticationCredentialsNotFoundException("The \"email\" and \"password\" parameters are required!") diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt index 4091a1a4b..8355895d0 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/UserService.kt @@ -41,14 +41,14 @@ class UserService(private val repository: UserRepository, private val passwordEn val encodedPassword = passwordEncoder.encode(plaintextPasswordUser.password) val encodedPasswordUser = User( plaintextPasswordUser.username, - plaintextPasswordUser.email, + plaintextPasswordUser.email.lowercase(), Role.Disabled, encodedPassword ) try { return repository.save(encodedPasswordUser) } catch (_: DataIntegrityViolationException) { - throw ForbiddenOperationException("User with email = '${encodedPasswordUser.email}' already exists!") + throw ForbiddenOperationException("User with email = '${encodedPasswordUser.email.lowercase()}' already exists!") } } From d557b7331c7493efae7d2732f448f4dade6fd0dc Mon Sep 17 00:00:00 2001 From: Tom Alard Date: Mon, 16 May 2022 12:44:54 +0200 Subject: [PATCH 116/425] feat: set landing page --- frontend/pages/login.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index ecc66e525..238518e23 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -66,14 +66,16 @@ const Login = () => { if (user.role === UserRole.Disabled) { router.push('/wait'); } else { - // TODO this is a temporary fix const response = await axios.get(Endpoints.EDITIONACTIVE, { headers: { Authorization: `Basic ${accessToken}` }, }); - if (response) { - setEdition(response.data.name); + if (response.data) { + const edition = response.data.name; + setEdition(edition); + router.push(`/${edition}/projects`); + } else { + router.push('/editions'); } - router.push('/'); } } else { toast.error('Something went wrong trying to process the request.'); From f3076760fa3e94536d6c2aa72e26e3160a6ba6cd Mon Sep 17 00:00:00 2001 From: Tom Alard Date: Mon, 16 May 2022 12:57:55 +0200 Subject: [PATCH 117/425] chore: remove GitHub login button --- frontend/pages/login.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 15bde90ab..2dc1f1c58 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -130,17 +130,6 @@ const Login = () => { no account yet?
    register here!

    -

    - Or log in using -

    - {/* Github provider. Right now, this doesn't work*/} - From 9624ece8f5e683acf138cc053ea88e1c48082abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 May 2022 11:49:47 +0000 Subject: [PATCH 118/425] chore: bump @types/node from 17.0.31 to 17.0.33 in /frontend Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 17.0.31 to 17.0.33. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7c42056a2..fac7e2d45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@types/node": "^17.0.31", + "@types/node": "^17.0.33", "@types/react": "^18.0.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.23.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c3d3f3354..2648273a8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -331,10 +331,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/node@^17.0.31": - version "17.0.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d" - integrity sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q== +"@types/node@^17.0.33": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.33.tgz#3c1879b276dc63e73030bb91165e62a4509cd506" + integrity sha512-miWq2m2FiQZmaHfdZNcbpp9PuXg34W5JZ5CrJ/BaS70VuhoJENBEQybeiYSaPBRNq6KQGnjfEnc/F3PN++D+XQ== "@types/parse-json@^4.0.0": version "4.0.0" From 5a146cefebbee0ae628665dee066fa972f3340e0 Mon Sep 17 00:00:00 2001 From: Tom Alard Date: Mon, 16 May 2022 13:51:28 +0200 Subject: [PATCH 119/425] chore: projectIds to projectUrls --- .../kotlin/be/osoc/team1/backend/services/ProjectService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt b/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt index c75e23df9..766ed07eb 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/services/ProjectService.kt @@ -110,10 +110,10 @@ class ProjectService( } val conflicts = mutableListOf() val studentSerializer = StudentSerializer() - for ((student, projectIds) in studentsMap.entries) { - if (projectIds.size > 1) { + for ((student, projectUrls) in studentsMap.entries) { + if (projectUrls.size > 1) { // this student has a conflict - conflicts.add(Conflict(studentSerializer.toUrl(student), projectIds)) + conflicts.add(Conflict(studentSerializer.toUrl(student), projectUrls)) } } return conflicts From b43042dadc7f37ddb99e66c80b9ebc4038c46269 Mon Sep 17 00:00:00 2001 From: Tom Alard Date: Mon, 16 May 2022 14:12:55 +0200 Subject: [PATCH 120/425] chore: requested changes --- frontend/pages/login.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index c54b313b1..6f55a5c07 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -81,9 +81,11 @@ const Login = () => { if (typeof window !== 'undefined' && editionName) { localStorage.setItem('edition', editionName); } - router.push(`/${editionName}/projects`); + router.push(`/${editionName}${Endpoints.PROJECTS}`); + } else if (user.role === UserRole.Admin) { + router.push(Endpoints.EDITIONS); } else { - router.push('/editions'); + router.push('/wait'); } } } else { From 5e016e5ebdb8e44601ff0f57ed56c68388296714 Mon Sep 17 00:00:00 2001 From: Lars Van Cauter Date: Mon, 16 May 2022 14:26:27 +0200 Subject: [PATCH 121/425] chore: changed name --- .../src/main/kotlin/be/osoc/team1/backend/entities/Student.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt index 542c020cf..b393d427f 100644 --- a/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt +++ b/backend/src/main/kotlin/be/osoc/team1/backend/entities/Student.kt @@ -168,8 +168,8 @@ class Student( @JsonSerialize(using = CommunicationListSerializer::class) val communications: MutableList = mutableListOf() - @JsonGetter("statusSuggestionsPercentage") - fun calculateStatusSuggestionPercentage(): Map = statusSuggestions.groupingBy { it.status }.eachCount() + @JsonGetter("statusSuggestionsCount") + fun calculateStatusSuggestionCount(): Map = statusSuggestions.groupingBy { it.status }.eachCount() } /** From a260884753a17097e02005608940169650dec1b3 Mon Sep 17 00:00:00 2001 From: Michael M Date: Mon, 16 May 2022 16:22:28 +0200 Subject: [PATCH 122/425] feat: make frontend use student view Basic for sidebar --- frontend/components/StudentSidebar.tsx | 27 +- frontend/components/student/StudentHolder.tsx | 8 +- frontend/components/student/StudentView.tsx | 322 ++++++++++-------- frontend/components/students/StudentTile.tsx | 125 ++----- frontend/lib/conversionUtils.ts | 64 +++- frontend/lib/types.ts | 22 +- frontend/pages/[editionName]/students.tsx | 4 +- 7 files changed, 298 insertions(+), 274 deletions(-) diff --git a/frontend/components/StudentSidebar.tsx b/frontend/components/StudentSidebar.tsx index f59297638..96c814a33 100644 --- a/frontend/components/StudentSidebar.tsx +++ b/frontend/components/StudentSidebar.tsx @@ -2,7 +2,7 @@ import { Fragment, useEffect, useState } from 'react'; import usePoll from 'react-use-poll'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; -import { StudentBase, StudentData } from '../lib/types'; +import { StudentBaseBasic, StudentData } from '../lib/types'; import useAxiosAuth from '../hooks/useAxiosAuth'; import { axiosAuthenticated } from '../lib/axios'; import Endpoints from '../lib/endpoints'; @@ -22,7 +22,7 @@ const magnifying_glass = ; */ type StudentsSidebarProps = { setError: (error: string) => void; - setStudentBase: (studentBase: StudentBase) => void; + setStudentBase: (studentBase: StudentBaseBasic) => void; }; /** @@ -45,7 +45,7 @@ async function searchStudent( studentNameSearch: string, skills: Array<{ value: string; label: string }>, studentSearchParameters: Record, - setStudents: (students: StudentBase[]) => void, + setStudents: (students: StudentBaseBasic[]) => void, setFilterAmount: (filterAmount: number) => void, state: { hasMoreItems: boolean; @@ -73,7 +73,7 @@ async function searchStudent( newState.hasMoreItems = false; newState.loading = false; setState(newState); - setStudents([] as StudentBase[]); + setStudents([] as StudentBaseBasic[]); setFilterAmount(0 as number); setLoading(false); return; @@ -92,6 +92,7 @@ async function searchStudent( unassignedOnly: studentSearchParameters.ExcludeAssigned, pageNumber: state.page, pageSize: state.pageSize, + view: 'Basic', }, signal: signal, }) @@ -103,7 +104,7 @@ async function searchStudent( newState.loading = false; setState(newState); // VERY IMPORTANT TO CHANGE STATE FIRST!!!! - setStudents(response.data.collection as StudentBase[]); + setStudents(response.data.collection as StudentBaseBasic[]); setFilterAmount(response.data.totalLength as number); setLoading(false); }) @@ -167,9 +168,9 @@ const StudentSidebar: React.FC = ({ ] = useState(0); const [students, setStudents]: [ - StudentBase[], - (students: StudentBase[]) => void - ] = useState([] as StudentBase[]); + StudentBaseBasic[], + (students: StudentBaseBasic[]) => void + ] = useState([] as StudentBaseBasic[]); const [loading, setLoading]: [boolean, (loading: boolean) => void] = useState(true); @@ -205,12 +206,10 @@ const StudentSidebar: React.FC = ({ * function to add new student results instead of overwriting old results * @param studentsList - list of students to add to all students */ - const updateStudents: (param: StudentBase[]) => void = ( - studentsList: StudentBase[] + const updateStudents: (param: StudentBaseBasic[]) => void = ( + studentsList: StudentBaseBasic[] ) => { - const newStudents = students - ? [...students] - : ([] as StudentBase[] as StudentBase[]); + const newStudents = students ? [...students] : ([] as StudentBaseBasic[]); newStudents.push(...studentsList); setStudents(newStudents); }; @@ -633,7 +632,7 @@ const StudentSidebar: React.FC = ({
    ( + renderItem={(student: StudentBaseBasic) => ( void; + studentBase: StudentBaseBasic; + setStudentBase: (studentBase: StudentBaseBasic) => void; }; const StudentHolder: React.FC = ({ @@ -26,7 +26,7 @@ const StudentHolder: React.FC = ({ ); }, drop: (item) => { - setStudentBase(item as StudentBase); + setStudentBase(item as StudentBaseBasic); }, collect: (monitor) => ({ isOver: monitor.isOver(), diff --git a/frontend/components/student/StudentView.tsx b/frontend/components/student/StudentView.tsx index f0c960cb6..c4860583b 100644 --- a/frontend/components/student/StudentView.tsx +++ b/frontend/components/student/StudentView.tsx @@ -4,6 +4,7 @@ import { StatusSuggestionBase, Student, StudentBase, + StudentBaseBasic, Url, User, UserRole, @@ -17,7 +18,11 @@ import { faQuestion, faXmark, } from '@fortawesome/free-solid-svg-icons'; -import { convertStudentBase } from '../../lib/conversionUtils'; +import { + convertStudentBase, + convertStudentBaseBasic, + convertStudentFullToBasic, +} from '../../lib/conversionUtils'; import { getUrlList, getUrlMap, parseError } from '../../lib/requestUtils'; import { NextRouter } from 'next/dist/client/router'; import { useRouter } from 'next/router'; @@ -30,8 +35,8 @@ const question_mark = ; const x_mark = ; type StudentViewProp = { - studentInput: StudentBase; - setOriginalStudentBase: (originalStudentBase: StudentBase) => void; + studentInput: StudentBaseBasic; + setOriginalStudentBase: (originalStudentBase: StudentBaseBasic) => void; }; type StatusSuggestionProp = { @@ -169,10 +174,13 @@ const StudentView: React.FC = ({ }: StudentViewProp) => { const [user] = useUser(); // Needed to reload student when a suggestion is done or status is changed + const [studentBaseBasic, setStudentBaseBasic] = useState( + studentInput as StudentBaseBasic + ); // TODO don't reload everything when only status or suggestions are changed, save the rest somewhere - const [studentBase, setStudentBase] = useState(studentInput as StudentBase); + const [studentBase, setStudentBase] = useState({} as StudentBase); const [myStudent, setMyStudent]: [Student, (myStudent: Student) => void] = - useState(convertStudentBase(studentBase) as Student); + useState(convertStudentBaseBasic(studentInput) as Student); const [status, setStatus] = useState({ value: '', @@ -186,17 +194,35 @@ const StudentView: React.FC = ({ let controller = new AbortController(); useEffect(() => { - setMotivation(''); - setStudentBase(studentInput); + setStudentBaseBasic(studentInput); }, [studentInput]); useEffect(() => { + setMotivation(''); controller.abort(); controller = new AbortController(); const signal = controller.signal; + reloadStudent( + studentBaseBasic.id, + setStudentBase, + signal, + setError, + router + ); + return () => { + controller.abort(); + }; + }, [studentBaseBasic]); + + useEffect(() => { + controller.abort(); + controller = new AbortController(); + const signal = controller.signal; + if (studentBase.id !== undefined && studentBase.id !== studentInput.id) { + setOriginalStudentBase(convertStudentFullToBasic(studentBase)); + } // This is a safety check, not really needed right now but it avoids accidents if (studentBase.id !== undefined) { - setOriginalStudentBase(studentBase); setStatus({ value: '', label: studentBase.status } as { value: string; label: string; @@ -222,110 +248,54 @@ const StudentView: React.FC = ({ }, [myStudent]); return ( -
    +
    {error && } - {/* hold the student information */} -
    -
    -

    - {myStudent.firstName + ' ' + myStudent.lastName} -

    -
    -
    -
    Suggestions
    - {myStudent.statusSuggestions.map((statusSuggestion) => ( - - ))} -
    -
    -
    Answers:
    - {myStudent.answers.map((answer) => ( -
    -

    {answer.question}

    -

    {answer.answer}

    -
    -
    - ))} -
    -
    - - {/* holds suggestion controls */} -
    - {/* regular coach status suggestion form */} -
    { - e.preventDefault(); - e.stopPropagation(); - controller.abort(); - controller = new AbortController(); - const signal = controller.signal; - setStudentSuggestion( - suggestion, - studentBase.id, - user.id, - motivation, - setStudentBase, - signal, - setError, - router - ); - return () => { - controller.abort(); - }; - }} - > -
    - - - +
    + {/* hold the student information */} +
    +
    +

    + {myStudent.firstName + ' ' + myStudent.lastName} +

    +
    +
    +
    Suggestions
    + {myStudent.statusSuggestions.map((statusSuggestion) => ( + + ))}
    -