first commit

This commit is contained in:
2023-08-14 01:56:22 +08:00
parent 12c94732bc
commit 3f816e3ffd
35 changed files with 508 additions and 80 deletions

View File

@@ -5,9 +5,13 @@ import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.miuna.subman.convert.UserConv;
import com.miuna.subman.entity.Resp;
import com.miuna.subman.entity.user.dto.AccessClaims;
import com.miuna.subman.entity.user.dto.RefreshClaims;
import com.miuna.subman.entity.user.enums.UserStatusEnum;
import com.miuna.subman.entity.user.pojo.User;
import com.miuna.subman.exception.AccountInactivatedException;
import com.miuna.subman.exception.BasicException;
import com.miuna.subman.exception.UnauthorizedException;
import com.miuna.subman.service.UserServ;
import com.miuna.subman.util.JsonUtil;
@@ -19,6 +23,8 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -29,6 +35,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@Slf4j
@@ -55,19 +62,12 @@ public class JwtFilter extends OncePerRequestFilter {
return;
}
String newAccessToken = renewAccessToken(authorization.accessToken());
String newRefreshToken;
if (validToken(authorization.refreshToken())) {
DecodedJWT decodedJWT = JwtUtil.parseValidToken(authorization.refreshToken());
RefreshClaims claims = RefreshClaims.fromClaimMap(decodedJWT.getClaims());
setSecurityContext(claims.email(), claims.getAuthorities());
newRefreshToken = "";
} else {
DecodedJWT decodedJWT = JwtUtil.parseValidToken(authorization.accessToken());
AccessClaims claims = AccessClaims.fromClaimMap(decodedJWT.getClaims());
User user = getValidUserByEmail(claims.email());
setSecurityContext(user.getEmail(), user.getAuthorities());
newRefreshToken = JwtUtil.generateRefreshToken(userConv.convUserToRefreshClaims(user));
try {
newRefreshToken = renewRefreshToken(authorization.refreshToken(), authorization.accessToken());
} catch (BasicException e) {
writeException2Resp(e, response);
return;
}
Authorization newToken = new Authorization(newAccessToken, newRefreshToken);
writeNewToken2Resp(newToken, response);
@@ -83,16 +83,24 @@ public class JwtFilter extends OncePerRequestFilter {
}
}
private void writeException2Resp(BasicException e, HttpServletResponse response) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.OK.value());
response.getOutputStream().write(JsonUtil.toJson(Resp.failure(e)).getBytes());
}
/*
"{token}": the renewed token
"": don't need renew
*/
private String renewAccessToken(String accessToken) throws JWTVerificationException {
private String renewAccessToken(String accessToken) {
DecodedJWT decodedJwt = JwtUtil.parseValidToken(accessToken);
AccessClaims accessClaims = AccessClaims.fromClaimMap(decodedJwt.getClaims());
setSecurityContext(accessClaims.email(), List.of());
Instant expiresAt = decodedJwt.getExpiresAtAsInstant();
if (JwtUtil.needRenew(expiresAt, ACCESS_TOKEN_RENEW_TIME)) {
return JwtUtil.generateAccessToken(AccessClaims.fromClaimMap(decodedJwt.getClaims()));
return JwtUtil.generateAccessToken(accessClaims);
}
return "";
}
@@ -102,9 +110,27 @@ public class JwtFilter extends OncePerRequestFilter {
}
private User getValidUserByEmail(String email) throws UnauthorizedException {
return userServ.findUserByEmail(email)
.filter(v -> !v.isEnabled())
User user = userServ.findUserByEmail(email)
.filter(User::isEnabled)
.orElseThrow(UnauthorizedException::new);
if (UserStatusEnum.INACTIVATED.getStatus().equals(user.getStatus())) {
throw new AccountInactivatedException();
}
return user;
}
private String renewRefreshToken(String refreshToken, String accessToken) throws UnauthorizedException, AccountInactivatedException {
if (validToken(refreshToken)) {
DecodedJWT decodedJWT = JwtUtil.parseValidToken(refreshToken);
RefreshClaims claims = RefreshClaims.fromClaimMap(decodedJWT.getClaims());
setSecurityContext(claims.email(), claims.getAuthorities());
return "";
}
DecodedJWT decodedJWT = JwtUtil.parseValidToken(accessToken);
AccessClaims claims = AccessClaims.fromClaimMap(decodedJWT.getClaims());
User user = getValidUserByEmail(claims.email());
setSecurityContext(user.getEmail(), user.getAuthorities());
return JwtUtil.generateRefreshToken(userConv.convUserToRefreshClaims(user));
}
private void setSecurityContext(String email, Collection<? extends GrantedAuthority> authority) {

View File

@@ -1,34 +1,98 @@
package com.miuna.subman.controller;
import com.miuna.subman.entity.AuthenticatedReq;
import com.miuna.subman.entity.Resp;
import com.miuna.subman.entity.user.pojo.User;
import com.miuna.subman.entity.user.req.LoginReq;
import com.miuna.subman.entity.user.req.RegisterReq;
import com.miuna.subman.entity.user.req.*;
import com.miuna.subman.entity.user.resp.GetUserResp;
import com.miuna.subman.service.UserServ;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Validated
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserCont {
private final UserServ userServ;
@GetMapping("/findOne")
@PreAuthorize("hasRole('SYSADMIN')")
public User findOne() {
return userServ.findUserByEmail("1853633282@qq.com").orElse(null);
}
@PostMapping("/register")
public Resp<Boolean> register(@RequestBody RegisterReq req) {
userServ.register(req);
return Resp.success(null);
return Resp.success();
}
@PostMapping("/login")
public Resp<String> login(@RequestBody LoginReq req) {
return Resp.success(userServ.login(req));
}
@PutMapping("/forgetPwd")
public Resp<Object> forgetPwd(@RequestBody @Valid ForgetPwdReq req) {
userServ.forgetPwd(req);
return Resp.success();
}
@PutMapping("/addUser")
private Resp<Object> addUser(@RequestBody @Valid AddUserReq req) {
userServ.addUser(req);
return Resp.success();
}
@PutMapping("/updateUser")
public Resp<Object> updateUser(@RequestBody @Valid UpdateUserReq req) {
userServ.updateUser(req);
return Resp.success();
}
@PutMapping("/resetPwd/{email}")
public Resp<Object> resetPwd(@PathVariable @Email String email) {
userServ.resetPwd(email);
return Resp.success();
}
@PutMapping("/activate")
public Resp<Object> activate(@RequestBody @Valid ActivateReq req) {
req.setEmail(SecurityContextHolder.getContext().getAuthentication().getName());
userServ.activate(req);
return Resp.success();
}
@PutMapping("/changeUser")
public Resp<Object> changeUser(@RequestBody @Valid ChangeUserReq req) {
req.setEmail(SecurityContextHolder.getContext().getAuthentication().getName());
userServ.changeUserInfo(req);
return Resp.success();
}
@PutMapping("/changeEmail")
public Resp<Object> changeEmail(@RequestBody @Valid ChangeEmailReq req) {
req.setEmail(SecurityContextHolder.getContext().getAuthentication().getName());
userServ.changeEmail(req);
return Resp.success();
}
@PutMapping("/changeUuid")
public Resp<String> changeUuid(@RequestBody @Valid AuthenticatedReq req) {
req.setEmail(SecurityContextHolder.getContext().getAuthentication().getName());
return Resp.success(userServ.changeUuid(req));
}
@PutMapping("/changePwd")
public Resp<Object> changePwd(@RequestBody @Valid ChangePwdReq req) {
req.setEmail(SecurityContextHolder.getContext().getAuthentication().getName());
userServ.changePwd(req);
return Resp.success();
}
@GetMapping("/listAll")
public Resp<List<GetUserResp>> listAllUser() {
return Resp.success(userServ.listAllUser());
}
}

View File

@@ -4,9 +4,16 @@ import com.miuna.subman.entity.user.dto.AccessClaims;
import com.miuna.subman.entity.user.dto.RefreshClaims;
import com.miuna.subman.entity.user.pojo.User;
import com.miuna.subman.entity.user.req.AddUserReq;
import com.miuna.subman.entity.user.req.ChangeUserReq;
import com.miuna.subman.entity.user.req.RegisterReq;
import com.miuna.subman.entity.user.req.UpdateUserReq;
import com.miuna.subman.entity.user.resp.GetUserResp;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import java.util.List;
@Mapper(componentModel = "spring")
public interface UserConv {
@@ -15,18 +22,34 @@ public interface UserConv {
RefreshClaims convUserToRefreshClaims(User user);
@Mapping(target = "id", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "timestamp", ignore = true)
@Mapping(target = "deleted", constant = "false")
@Mapping(target = "uuid", expression = "java(com.miuna.subman.util.CodeUtil.generateUUID())")
@Mapping(target = "password", expression = "java(com.miuna.subman.util.PwdUtil.encode(req.getPassword()))")
@Mapping(target = "role", expression = "java(com.miuna.subman.entity.user.enums.RoleEnum.USER.getRoleId())")
@Mapping(target = "status", expression = "java(com.miuna.subman.entity.user.enums.UserStatusEnum.INACTIVATED.getStatus())")
@Mapping(target = "status", expression = "java(com.miuna.subman.entity.user.enums.UserStatusEnum.ACTIVATED.getStatus())")
User convRegister2User(RegisterReq req);
@Mapping(target = "id", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "timestamp", ignore = true)
@Mapping(target = "deleted", constant = "false")
@Mapping(target = "password", expression = "java(com.miuna.subman.util.PwdUtil.encodeDefault())")
@Mapping(target = "uuid", expression = "java(com.miuna.subman.util.CodeUtil.generateUUID())")
@Mapping(target = "status", expression = "java(com.miuna.subman.entity.user.enums.UserStatusEnum.INACTIVATED.getStatus())")
User convAddUser2User(AddUserReq req);
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "role")
@Mapping(target = "email")
@Mapping(target = "status")
@Mapping(target = "username")
void fillUserWithUpdateUser(@MappingTarget User user, UpdateUserReq req);
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "username")
void fillUserWithChangeUser(@MappingTarget User user, ChangeUserReq req);
List<GetUserResp> convUser2GetUsers(List<User> users);
}

View File

@@ -0,0 +1,10 @@
package com.miuna.subman.entity;
import jakarta.validation.constraints.Null;
import lombok.Data;
@Data
public class AuthenticatedReq {
@Null
private String email;
}

View File

@@ -18,5 +18,5 @@ public class Code {
private String code;
@UpdateTimestamp
private LocalDateTime createTime;
private LocalDateTime timestamp;
}

View File

@@ -2,16 +2,13 @@ package com.miuna.subman.entity.user.pojo;
import com.miuna.subman.entity.user.enums.RoleEnum;
import com.miuna.subman.entity.user.enums.UserStatusEnum;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.hibernate.annotations.Where;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@@ -26,6 +23,7 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Where(clause = "deleted = false")
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -41,13 +39,12 @@ public class User implements UserDetails, Serializable {
private Integer role;
private String uuid;
private Boolean deleted = Boolean.FALSE;
@CreationTimestamp
private LocalDateTime createTime;
@UpdateTimestamp
private LocalDateTime updateTime;
private LocalDateTime timestamp;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
@@ -70,6 +67,6 @@ public class User implements UserDetails, Serializable {
@Override
public boolean isEnabled() {
return UserStatusEnum.ACTIVATED.getStatus().equals(this.status) && !deleted;
return !UserStatusEnum.DISABLE.getStatus().equals(this.status) && !deleted;
}
}

View File

@@ -0,0 +1,13 @@
package com.miuna.subman.entity.user.req;
import com.miuna.subman.entity.AuthenticatedReq;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ActivateReq extends AuthenticatedReq {
@NotEmpty
private String password;
}

View File

@@ -1,5 +1,7 @@
package com.miuna.subman.entity.user.req;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -10,8 +12,9 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
@Accessors(chain = true)
public class AddUserReq {
@NotEmpty
private String username;
@Email
private String email;
private String role;
private String password;
}

View File

@@ -0,0 +1,18 @@
package com.miuna.subman.entity.user.req;
import com.miuna.subman.entity.AuthenticatedReq;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ChangeEmailReq extends AuthenticatedReq {
@Email
private String newEmail;
@NotEmpty
@Size(min = 6, max = 6)
private String code;
}

View File

@@ -0,0 +1,15 @@
package com.miuna.subman.entity.user.req;
import com.miuna.subman.entity.AuthenticatedReq;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ChangePwdReq extends AuthenticatedReq {
@NotEmpty
private String oldPassword;
@NotEmpty
private String newPassword;
}

View File

@@ -0,0 +1,13 @@
package com.miuna.subman.entity.user.req;
import com.miuna.subman.entity.AuthenticatedReq;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ChangeUserReq extends AuthenticatedReq {
@NotEmpty
private String username;
}

View File

@@ -0,0 +1,17 @@
package com.miuna.subman.entity.user.req;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class ForgetPwdReq {
@Email
private String email;
@NotEmpty
private String password;
@NotEmpty
@Size(min = 6, max = 6)
private String code;
}

View File

@@ -1,5 +1,8 @@
package com.miuna.subman.entity.user.req;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -10,6 +13,9 @@ import lombok.experimental.Accessors;
@NoArgsConstructor
@Accessors(chain = true)
public class LoginReq {
@Email
private String email;
@NotEmpty
@Size(min = 6, max = 6)
private String password;
}

View File

@@ -1,5 +1,8 @@
package com.miuna.subman.entity.user.req;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -10,8 +13,13 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
@Accessors(chain = true)
public class RegisterReq {
@NotEmpty
private String username;
@NotEmpty
private String password;
@Email
private String email;
@NotEmpty
@Size(min = 6, max = 6)
private String code;
}

View File

@@ -0,0 +1,18 @@
package com.miuna.subman.entity.user.req;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UpdateUserReq {
@Email
private String email;
@NotEmpty
private String username;
@NotNull
private Integer role;
@NotNull
private Integer status;
}

View File

@@ -0,0 +1,24 @@
package com.miuna.subman.entity.user.resp;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class GetUserResp {
private Long id;
private String username;
private String email;
private Integer status;
private Integer role;
private String uuid;
private Boolean deleted;
private LocalDateTime timestamp;
}

View File

@@ -1,4 +0,0 @@
package com.miuna.subman.entity.user.resp;
public class LoginResp {
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class AccountInactivatedException extends BasicException {
public AccountInactivatedException() {
super(ErrorEnum.ACCOUNT_INACTIVATED.getCode(), ErrorEnum.ACCOUNT_INACTIVATED.getDesc());
}
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class CodeIncorrectException extends BasicException{
public CodeIncorrectException() {
super(ErrorEnum.CODE_INCORRECT.getCode(), ErrorEnum.CODE_INCORRECT.getDesc());
}
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class CodeNotFoundException extends BasicException {
public CodeNotFoundException() {
super(ErrorEnum.CODE_NOT_FOUND.getCode(), ErrorEnum.CODE_NOT_FOUND.getDesc());
}
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class DuplicateEmailException extends BasicException{
public DuplicateEmailException() {
super(ErrorEnum.DUPLICATE_EMAIL.getCode(), ErrorEnum.DUPLICATE_EMAIL.getDesc());
}
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class EmailServiceException extends BasicException {
public EmailServiceException() {
super(ErrorEnum.EMAIL_SERVICE_UNAVAILABLE.getCode(), ErrorEnum.EMAIL_SERVICE_UNAVAILABLE.getDesc());
}
}

View File

@@ -0,0 +1,21 @@
package com.miuna.subman.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum ErrorEnum {
PASSWORD_INCORRECT(40001, "Password Incorrect"),
CODE_INCORRECT(40002, "Code Incorrect"),
DUPLICATE_EMAIL(40011, "Duplicate Email"),
RATE_LIMIT_EXCEEDED(40301, "Rate Limit Exceeded"),
USER_NOT_FOUND(40401, "User Not Found"),
CODE_NOT_FOUND(40402, "Code Not Found"),
ACCOUNT_INACTIVATED(40301, "Account Inactivated"),
EMAIL_SERVICE_UNAVAILABLE(50011, "Email Service Unavailable");
private final Integer code;
private final String desc;
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class PasswordIncorrectException extends BasicException{
public PasswordIncorrectException() {
super(ErrorEnum.PASSWORD_INCORRECT.getCode(), ErrorEnum.PASSWORD_INCORRECT.getDesc());
}
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class RateLimitException extends BasicException{
public RateLimitException() {
super(ErrorEnum.RATE_LIMIT_EXCEEDED.getCode(), ErrorEnum.RATE_LIMIT_EXCEEDED.getDesc());
}
}

View File

@@ -0,0 +1,13 @@
package com.miuna.subman.exception;
import com.miuna.subman.entity.Resp;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class SubManExceptionHandler {
@ExceptionHandler(BasicException.class)
public Resp<Object> handleBasicException(BasicException e) {
return Resp.failure(e);
}
}

View File

@@ -0,0 +1,7 @@
package com.miuna.subman.exception;
public class UserNotFountException extends BasicException {
public UserNotFountException() {
super(ErrorEnum.USER_NOT_FOUND.getCode(), ErrorEnum.USER_NOT_FOUND.getDesc());
}
}

View File

@@ -11,6 +11,6 @@ import java.time.LocalDateTime;
public interface CodeRepo extends JpaRepository<Code, String> {
@Modifying
@Transactional
@Query("delete from Code where createTime < :createTime")
@Query("delete from Code where timestamp < :createTime")
void deleteByCreateTimeBefore(LocalDateTime createTime);
}

View File

@@ -10,4 +10,6 @@ import java.util.Optional;
@Repository
public interface UserRepo extends JpaRepository<User, Long> {
Optional<User> findByEmail(@NonNull String email);
boolean existsByEmail(String email);
}

View File

@@ -2,12 +2,15 @@ package com.miuna.subman.service;
import com.miuna.subman.entity.Constant;
import com.miuna.subman.entity.user.pojo.Code;
import com.miuna.subman.exception.CodeNotFoundException;
import com.miuna.subman.exception.RateLimitException;
import com.miuna.subman.repository.CodeRepo;
import com.miuna.subman.util.CodeUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@@ -19,14 +22,18 @@ public class CodeServ {
private static final long RESEND_SECOND = 60;
public String generateCode(String email) {
Code code = codeRepo.findById(email).filter(this::canResend).orElseThrow();
codeRepo.save(new Code().setCode(CodeUtil.generateCode()).setEmail(email));
Optional<Code> code = codeRepo.findById(email);
if (!canResend(code)){
throw new RateLimitException();
}
String newCode = CodeUtil.generateCode();
codeRepo.save(new Code().setCode(newCode).setEmail(email));
lazyDeleteCode();
return code.getCode();
return newCode;
}
public String getCode(String email) {
Code code = codeRepo.findById(email).filter(this::isNotExpired).orElseThrow();
Code code = codeRepo.findById(email).filter(this::isNotExpired).orElseThrow(CodeNotFoundException::new);
return code.getCode();
}
@@ -43,11 +50,11 @@ public class CodeServ {
}
private boolean isNotExpired(Code code) {
return code.getCreateTime().isBefore(LocalDateTime.now().minusSeconds(Constant.VALID_CODE_EXPIRE_SECONDS));
return code.getTimestamp().isAfter(LocalDateTime.now().minusSeconds(Constant.VALID_CODE_EXPIRE_SECONDS));
}
private boolean canResend(Code code) {
return code.getCreateTime().isAfter(LocalDateTime.now().minusSeconds(RESEND_SECOND));
private boolean canResend(Optional<Code> code) {
return code.isEmpty() || code.get().getTimestamp().isBefore(LocalDateTime.now().minusSeconds(RESEND_SECOND));
}
}

View File

@@ -2,11 +2,13 @@ package com.miuna.subman.service;
import com.miuna.subman.entity.Constant;
import com.miuna.subman.entity.system.pojo.System;
import com.miuna.subman.exception.EmailServiceException;
import lombok.AllArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
// TODO: valid email service switch
@Service
@AllArgsConstructor
public class EmailServ {
@@ -32,7 +34,7 @@ public class EmailServ {
try {
sender.send(smm);
} catch (Exception e) {
throw new RuntimeException();
throw new EmailServiceException();
}
}

View File

@@ -1,18 +1,24 @@
package com.miuna.subman.service;
import com.miuna.subman.convert.UserConv;
import com.miuna.subman.entity.AuthenticatedReq;
import com.miuna.subman.entity.user.enums.UserStatusEnum;
import com.miuna.subman.entity.user.pojo.User;
import com.miuna.subman.entity.user.req.AddUserReq;
import com.miuna.subman.entity.user.req.LoginReq;
import com.miuna.subman.entity.user.req.RegisterReq;
import com.miuna.subman.entity.user.req.*;
import com.miuna.subman.entity.user.resp.GetUserResp;
import com.miuna.subman.exception.CodeIncorrectException;
import com.miuna.subman.exception.DuplicateEmailException;
import com.miuna.subman.exception.PasswordIncorrectException;
import com.miuna.subman.exception.UserNotFountException;
import com.miuna.subman.repository.UserRepo;
import com.miuna.subman.util.CodeUtil;
import com.miuna.subman.util.JwtUtil;
import com.miuna.subman.util.PwdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -25,24 +31,22 @@ public class UserServ {
private final UserConv userConv;
private final CodeServ codeServ;
private static final PasswordEncoder PWD_ENCODER = new BCryptPasswordEncoder();
public Optional<User> findUserByEmail(String email) {
return userRepo.findByEmail(email);
}
public void register(RegisterReq req) {
// TODO: register switch open
validEmail(req.getEmail());
validCode(req.getEmail(), req.getCode());
User user = userConv.convRegister2User(req);
user.setPassword(PWD_ENCODER.encode(req.getPassword()));
userRepo.save(user);
}
public String login(LoginReq req) {
User user = findUserByEmail(req.getEmail()).orElseThrow(RuntimeException::new);
if (!PWD_ENCODER.matches(req.getPassword(), user.getPassword())) {
throw new RuntimeException();
User user = findUserByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
if (!PwdUtil.match(req.getPassword(), user.getPassword())) {
throw new PasswordIncorrectException();
}
return JwtUtil.generateAccessToken(userConv.convUser2AccessClaims(user));
}
@@ -50,33 +54,78 @@ public class UserServ {
public void addUser(AddUserReq req) {
validEmail(req.getEmail());
User user = userConv.convAddUser2User(req);
user.setPassword(PWD_ENCODER.encode(user.getPassword()));
userRepo.save(user);
}
public void updateUser() {}
public void forgetPwd(ForgetPwdReq req) {
validCode(req.getEmail(), req.getCode());
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
user.setPassword(PwdUtil.encode(req.getPassword()));
userRepo.save(user);
}
public void resetPassword() {}
public void resetPwd(String email) {
User user = userRepo.findByEmail(email).orElseThrow(UserNotFountException::new);
user.setPassword(PwdUtil.encodeDefault()).setStatus(UserStatusEnum.INACTIVATED.ordinal());
userRepo.save(user);
}
public void changeUserInfo() {}
public void activate(ActivateReq req) {
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
user.setPassword(PwdUtil.encode(req.getPassword())).setStatus(UserStatusEnum.ACTIVATED.getStatus());
userRepo.save(user);
}
public void changeEmail() {}
public void updateUser(UpdateUserReq req) {
validEmail(req.getEmail());
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
userConv.fillUserWithUpdateUser(user, req);
userRepo.save(user);
}
public void changePassword(){}
public void changePwd(ChangePwdReq req) {
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
if (!PwdUtil.match(req.getOldPassword(), user.getPassword())) {
throw new PasswordIncorrectException();
}
user.setPassword(PwdUtil.encode(req.getNewPassword()));
userRepo.save(user);
}
public void listAllUser() {}
public void changeUserInfo(ChangeUserReq req) {
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
userConv.fillUserWithChangeUser(user, req);
userRepo.save(user);
}
public void changeEmail(ChangeEmailReq req) {
validCode(req.getNewEmail(), req.getCode());
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
user.setEmail(req.getEmail());
userRepo.save(user);
}
public String changeUuid(AuthenticatedReq req) {
User user = userRepo.findByEmail(req.getEmail()).orElseThrow(UserNotFountException::new);
String uuid = CodeUtil.generateUUID();
user.setUuid(uuid);
return uuid;
}
public List<GetUserResp> listAllUser() {
return userConv.convUser2GetUsers(userRepo.findAll());
}
private void validCode(String email, String code) {
if (!Objects.equals(code, codeServ.getCode(email))) {
throw new RuntimeException();
throw new CodeIncorrectException();
}
codeServ.deleteCode(email);
}
private void validEmail(String email) {
boolean isExist = userRepo.findByEmail(email).filter(v -> !v.getDeleted()).isPresent();
if (isExist) {
throw new RuntimeException();
if (!userRepo.existsByEmail(email)) {
throw new DuplicateEmailException();
}
}

View File

@@ -1,6 +1,7 @@
package com.miuna.subman.util;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
public class CodeUtil {
@@ -8,4 +9,8 @@ public class CodeUtil {
return new Random().ints(6, 0, 10)
.mapToObj(String::valueOf).collect(Collectors.joining());
}
public static String generateUUID() {
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,22 @@
package com.miuna.subman.util;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class PwdUtil {
private static final PasswordEncoder PWD_ENCODER = new BCryptPasswordEncoder();
private static final String DEFAULT_PWD = "SubMan123";
public static String encode(String raw) {
return PWD_ENCODER.encode(raw);
}
public static String encodeDefault() {
return PWD_ENCODER.encode(DEFAULT_PWD);
}
public static boolean match(String raw, String encode) {
return PWD_ENCODER.matches(raw, encode);
}
}

View File

@@ -5,7 +5,7 @@ spring:
name: subman
devtools:
restart:
enabled: true
enabled: false
datasource:
url: jdbc:sqlite:${user.home}/.config/subman/subman.db
jpa: