diff --git a/http/login.http b/http/login.http index 5325a55e..26ccb316 100644 --- a/http/login.http +++ b/http/login.http @@ -1,4 +1,4 @@ -POST localhost:8080/login +POST localhost:8080/api/auth/login Content-Type: application/json { diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/admin/controller/AdminController.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/admin/controller/AdminController.kt index 1d6b3da5..12c43739 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/admin/controller/AdminController.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/admin/controller/AdminController.kt @@ -1,19 +1,43 @@ package com.tistory.shanepark.dutypark.admin.controller +import com.tistory.shanepark.dutypark.member.domain.dto.MemberDto +import com.tistory.shanepark.dutypark.member.service.MemberService import com.tistory.shanepark.dutypark.member.service.RefreshTokenService import com.tistory.shanepark.dutypark.security.domain.dto.RefreshTokenDto +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.data.web.PageableDefault +import org.springframework.data.web.SortDefault import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/admin/api/") class AdminController( - private val refreshTokenService: RefreshTokenService + private val refreshTokenService: RefreshTokenService, + private val memberService: MemberService, ) { @GetMapping("/refresh-tokens") fun findAllRefreshTokens(): List { return refreshTokenService.findAllWithMemberOrderByLastUsedDesc() } + @GetMapping("/members") + fun members( + @PageableDefault(page = 0, size = 10) + @SortDefault(sort = ["name"], direction = Sort.Direction.ASC) + page: Pageable, + @RequestParam(required = false, defaultValue = "") name: String, + ): Page { + return memberService.searchMembers(page, name) + } + + @GetMapping("/members-all") + fun findAllMembers(): List { + return memberService.findAll() + } + } diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/common/advice/ViewExceptionControllerAdvice.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/common/advice/ViewExceptionControllerAdvice.kt index 20aa08bc..4d143292 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/common/advice/ViewExceptionControllerAdvice.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/common/advice/ViewExceptionControllerAdvice.kt @@ -17,7 +17,7 @@ class ViewExceptionControllerAdvice { @ExceptionHandler fun notAuthorizedHandler(e: DutyparkAuthException, request: HttpServletRequest): ModelAndView { - val redirectUrl = "redirect:/login?referer=" + URLEncoder.encode(request.requestURI, "UTF-8") + val redirectUrl = "redirect:/auth/login?referer=" + URLEncoder.encode(request.requestURI, "UTF-8") return ModelAndView(redirectUrl) } diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/member/controller/MemberController.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/member/controller/MemberController.kt deleted file mode 100644 index 232e5412..00000000 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/member/controller/MemberController.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tistory.shanepark.dutypark.member.controller - -import com.tistory.shanepark.dutypark.member.domain.dto.MemberDto -import com.tistory.shanepark.dutypark.member.service.MemberService -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.domain.Sort -import org.springframework.data.web.PageableDefault -import org.springframework.data.web.SortDefault -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController - -@RestController -@RequestMapping("/admin/api/members") -class MemberController( - private val memberService: MemberService, -) { - - @GetMapping - fun members( - @PageableDefault(page = 0, size = 10) - @SortDefault(sort = ["name"], direction = Sort.Direction.ASC) - page: Pageable, - @RequestParam(required = false, defaultValue = "") name: String, - ): Page { - return memberService.searchMembers(page, name) - } - -} diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/member/repository/RefreshTokenRepository.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/member/repository/RefreshTokenRepository.kt index bac80745..a4318f50 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/member/repository/RefreshTokenRepository.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/member/repository/RefreshTokenRepository.kt @@ -1,5 +1,6 @@ package com.tistory.shanepark.dutypark.member.repository +import com.tistory.shanepark.dutypark.member.domain.entity.Member import com.tistory.shanepark.dutypark.security.domain.entity.RefreshToken import org.springframework.data.jpa.repository.EntityGraph import org.springframework.data.jpa.repository.JpaRepository @@ -17,6 +18,8 @@ interface RefreshTokenRepository : JpaRepository { @EntityGraph(attributePaths = ["member"]) fun findAllByMemberIdOrderByLastUsedDesc(id: Long): List + fun findAllByMember(member: Member): List + fun findAllByValidUntilIsBefore(now: LocalDateTime): List } diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/member/service/RefreshTokenService.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/member/service/RefreshTokenService.kt index 841da23a..4bbaebbc 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/member/service/RefreshTokenService.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/member/service/RefreshTokenService.kt @@ -1,6 +1,7 @@ package com.tistory.shanepark.dutypark.member.service import com.tistory.shanepark.dutypark.common.exceptions.DutyparkAuthException +import com.tistory.shanepark.dutypark.member.domain.entity.Member import com.tistory.shanepark.dutypark.member.repository.MemberRepository import com.tistory.shanepark.dutypark.member.repository.RefreshTokenRepository import com.tistory.shanepark.dutypark.security.config.JwtConfig @@ -63,4 +64,10 @@ class RefreshTokenService( return refreshTokenRepository.findAllWithMemberOrderByLastUsedDesc().map { RefreshTokenDto.of(it) } } + fun revokeAllRefreshTokensByMember(member: Member) { + val findAllByMember = refreshTokenRepository.findAllByMember(member) + log.info("Revoked {} refresh tokens of member {}", findAllByMember.size, member.email) + refreshTokenRepository.deleteAll(findAllByMember) + } + } diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthController.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthController.kt index 17a1d3b8..7a926934 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthController.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthController.kt @@ -6,6 +6,7 @@ import com.tistory.shanepark.dutypark.member.service.RefreshTokenService import com.tistory.shanepark.dutypark.security.config.JwtConfig import com.tistory.shanepark.dutypark.security.domain.dto.LoginDto import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember +import com.tistory.shanepark.dutypark.security.domain.dto.PasswordChangeDto import com.tistory.shanepark.dutypark.security.domain.entity.RefreshToken import com.tistory.shanepark.dutypark.security.service.AuthService import jakarta.servlet.http.HttpServletRequest @@ -17,6 +18,7 @@ import org.springframework.ui.Model import org.springframework.web.bind.annotation.* @RestController +@RequestMapping("/api/auth") class AuthController( private val authService: AuthService, private val refreshTokenService: RefreshTokenService, @@ -25,7 +27,7 @@ class AuthController( ) { private val log = org.slf4j.LoggerFactory.getLogger(AuthController::class.java) - @PostMapping("/login") + @PostMapping("login") fun login( @RequestBody loginDto: LoginDto, model: Model, @@ -85,6 +87,18 @@ class AuthController( } } + @PutMapping("password") + fun changePassword( + @Login loginMember: LoginMember, + @RequestBody(required = true) param: PasswordChangeDto + ): ResponseEntity { + if (loginMember.id != param.memberId && !loginMember.isAdmin) { + throw DutyparkAuthException("You are not authorized to change this password") + } + authService.changePassword(param, loginMember.isAdmin) + return ResponseEntity.ok().body("Password Changed") + } + @GetMapping("/status") fun loginStatus( @Login(required = false) diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthViewController.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthViewController.kt index 6050a565..64750338 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthViewController.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/security/controller/AuthViewController.kt @@ -7,12 +7,16 @@ import jakarta.servlet.http.HttpSession import org.springframework.http.HttpHeaders import org.springframework.stereotype.Controller import org.springframework.ui.Model -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CookieValue +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping @Controller +@RequestMapping("/auth") class AuthViewController : ViewController() { - @GetMapping("/login") + @GetMapping("login") fun loginPage( @CookieValue(name = "rememberMe", required = false) rememberMe: String?, @RequestHeader(HttpHeaders.REFERER, required = false) referer: String?, diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/security/domain/dto/PasswordChangeDto.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/security/domain/dto/PasswordChangeDto.kt new file mode 100644 index 00000000..11e2b467 --- /dev/null +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/security/domain/dto/PasswordChangeDto.kt @@ -0,0 +1,7 @@ +package com.tistory.shanepark.dutypark.security.domain.dto + +data class PasswordChangeDto( + val memberId: Long, + val currentPassword: String?, + val newPassword: String +) diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/security/filters/AdminAuthFilter.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/security/filters/AdminAuthFilter.kt index de93d3cf..3ae42e25 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/security/filters/AdminAuthFilter.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/security/filters/AdminAuthFilter.kt @@ -25,7 +25,7 @@ class AdminAuthFilter : Filter { log.info("$loginMember is not admin.") response.sendError(HttpServletResponse.SC_FORBIDDEN) } - response.sendRedirect("/login") + response.sendRedirect("/auth/login") } diff --git a/src/main/kotlin/com/tistory/shanepark/dutypark/security/service/AuthService.kt b/src/main/kotlin/com/tistory/shanepark/dutypark/security/service/AuthService.kt index 7382bba3..729c394a 100644 --- a/src/main/kotlin/com/tistory/shanepark/dutypark/security/service/AuthService.kt +++ b/src/main/kotlin/com/tistory/shanepark/dutypark/security/service/AuthService.kt @@ -5,6 +5,7 @@ import com.tistory.shanepark.dutypark.member.repository.MemberRepository import com.tistory.shanepark.dutypark.member.service.RefreshTokenService import com.tistory.shanepark.dutypark.security.domain.dto.LoginDto import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember +import com.tistory.shanepark.dutypark.security.domain.dto.PasswordChangeDto import com.tistory.shanepark.dutypark.security.domain.entity.RefreshToken import com.tistory.shanepark.dutypark.security.domain.enums.TokenStatus import jakarta.servlet.http.Cookie @@ -79,4 +80,24 @@ class AuthService( response.addCookie(cookie) } + fun changePassword(param: PasswordChangeDto, byAdmin: Boolean = false) { + val member = memberRepository.findById(param.memberId).orElseThrow { + log.info("change password failed. member not exist:${param.memberId}") + throw DutyparkAuthException("존재하지 않는 회원입니다.") + } + + if (!byAdmin) { + val passwordMatch = passwordEncoder.matches(param.currentPassword, member.password) + if (!passwordMatch) { + log.info("change password failed. password not match:${param.memberId}") + throw DutyparkAuthException("비밀번호가 일치하지 않습니다.") + } + } + + member.password = passwordEncoder.encode(param.newPassword) + refreshTokenService.revokeAllRefreshTokensByMember(member) + + log.info("Member password changed. member:${param.memberId}") + } + } diff --git a/src/main/resources/static/css/base.css b/src/main/resources/static/css/base.css index f2337872..9e989557 100644 --- a/src/main/resources/static/css/base.css +++ b/src/main/resources/static/css/base.css @@ -1,13 +1,9 @@ @font-face { + src: url('../fonts/NexonMaplestoryLight.woff2') format('woff2'); + font-display: swap; font-family: 'NexonMaplestory'; font-weight: 300; font-style: normal; - src: url('https://cdn.jsdelivr.net/gh/webfontworld/NexonMaplestory/NexonMaplestoryLight.eot'); - src: url('https://cdn.jsdelivr.net/gh/webfontworld/NexonMaplestory/NexonMaplestoryLight.eot?#iefix') format('embedded-opentype'), - url('https://cdn.jsdelivr.net/gh/webfontworld/NexonMaplestory/NexonMaplestoryLight.woff2') format('woff2'), - url('https://cdn.jsdelivr.net/gh/webfontworld/NexonMaplestory/NexonMaplestoryLight.woff') format('woff'), - url('https://cdn.jsdelivr.net/gh/webfontworld/NexonMaplestory/NexonMaplestoryLight.ttf') format("truetype"); - font-display: swap; } [v-cloak] { @@ -15,7 +11,7 @@ } body { - font-family: NexonMaplestory; + font-family: NexonMaplestory, sans-serif; --bs-body-font-family: NexonMaplestory; } diff --git a/src/main/resources/static/fonts/NexonMaplestoryLight.woff2 b/src/main/resources/static/fonts/NexonMaplestoryLight.woff2 new file mode 100644 index 00000000..a68f2ca5 Binary files /dev/null and b/src/main/resources/static/fonts/NexonMaplestoryLight.woff2 differ diff --git a/src/main/resources/templates/admin/admin-home.html b/src/main/resources/templates/admin/admin-home.html index 5d3b81b7..73e8b851 100644 --- a/src/main/resources/templates/admin/admin-home.html +++ b/src/main/resources/templates/admin/admin-home.html @@ -1,34 +1,44 @@ -Active Refresh Tokens +

Active Refresh Tokens

-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ user.name }}
Last activeIpDeviceBrowser
{{ index+1 }}{{ token.lastUsed | fromNow }}{{ token.remoteAddr}}{{ token.userAgent ? token.userAgent.device : '' }}{{ token.userAgent ? token.userAgent.browser : '' }}
-
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ member.name }} + + +
Last activeIpDeviceBrowser
{{ index+1 }}{{ token.lastUsed | fromNow }}{{ token.remoteAddr}}{{ token.userAgent ? token.userAgent.device : '' }}{{ token.userAgent ? token.userAgent.browser : '' }}
+