Skip to content

Commit

Permalink
Add an api for update user information
Browse files Browse the repository at this point in the history
During development, we found that the `Long` can not be expressed
correctly in the `Swagger` document, so we use `String` to represent the
`id` and convert it to `Long` when we use it as the field of `UserPO`.

We also found that the `HttpServletRequest`'s `getReader()` and
`getInputStream()` can only be called once, so we need to cache the
request body in the `JwtFilter` to make it can be read multiple times.

During this commit, we update the header parameter `Token` with the
`Access-Token` and `Refresh-Token` to make it more clear.

See #32.
  • Loading branch information
Kaiser-Yang committed Sep 16, 2024
1 parent ef444f4 commit 9a275ca
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 82 deletions.
1 change: 1 addition & 0 deletions src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ApiPathConstant {
public static final String USER_API_PREFIX = ALL_API_PREFIX + "/user";

public static final String USER_GET_USER_BY_NAME_API_PATH = USER_API_PREFIX + "/{username}";
public static final String USER_UPDATE_USER_API_PATH = USER_API_PREFIX + "/update";
public static final String USER_CHECK_EMAIL_VALIDITY_API_PATH = USER_API_PREFIX + "/email";
public static final String USER_CHECK_USERNAME_VALIDITY_API_PATH =
USER_API_PREFIX + "/username";
Expand Down
1 change: 0 additions & 1 deletion src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package edu.cmipt.gcs.constant;

public class HeaderParameter {
public static final String TOKEN = "Token";
public static final String ACCESS_TOKEN = "Access-Token";
public static final String REFRESH_TOKEN = "Refresh-Token";
}
4 changes: 4 additions & 0 deletions src/main/java/edu/cmipt/gcs/constant/ValidationConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public class ValidationConstant {
public static final int MAX_PASSWORD_LENGTH = 20;
public static final int MIN_USERNAME_LENGTH = 1;
public static final int MAX_USERNAME_LENGTH = 50;
// the size of username and password will be check by the @Size,
// so we just use '*' to ignore the length check
public static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]*$";
public static final String PASSWORD_PATTERN = "^[a-zA-Z0-9_.@]*$";
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -105,13 +106,7 @@ public ResponseEntity<UserVO> signIn(@Validated @RequestBody UserSignInDTO user)
throw new GenericException(ErrorCodeEnum.WRONG_SIGN_IN_INFORMATION);
}
UserVO userVO = new UserVO(userService.getOne(wrapper));
HttpHeaders headers = new HttpHeaders();
headers.add(
HeaderParameter.ACCESS_TOKEN,
JwtUtil.generateToken(userVO.id(), TokenTypeEnum.ACCESS_TOKEN));
headers.add(
HeaderParameter.REFRESH_TOKEN,
JwtUtil.generateToken(userVO.id(), TokenTypeEnum.REFRESH_TOKEN));
HttpHeaders headers = JwtUtil.generateHeaders(userVO.id());
return ResponseEntity.ok().headers(headers).body(userVO);
}

Expand All @@ -130,24 +125,30 @@ public void signOut(@RequestBody List<String> tokenList) {
summary = "Refresh token",
description = "Return an access token with given refresh token",
tags = {"Authentication", "Get Method"})
@Parameter(
name = HeaderParameter.TOKEN,
description = "Refresh token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class))
@Parameters({
@Parameter(
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class)),
@Parameter(
name = HeaderParameter.REFRESH_TOKEN,
description = "Refresh token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class))
})
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Token refreshed successfully",
content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public ResponseEntity<Void> refreshToken(@RequestHeader(HeaderParameter.TOKEN) String token) {
HttpHeaders headers = new HttpHeaders();
headers.add(
HeaderParameter.ACCESS_TOKEN,
JwtUtil.generateToken(JwtUtil.getID(token), TokenTypeEnum.ACCESS_TOKEN));
public ResponseEntity<Void> refreshToken(@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken, @RequestHeader(HeaderParameter.REFRESH_TOKEN) String refreshToken) {
JwtUtil.blacklistToken(accessToken);
HttpHeaders headers = JwtUtil.generateHeaders(JwtUtil.getID(refreshToken), false);
return ResponseEntity.ok().headers(headers).build();
}
}
61 changes: 56 additions & 5 deletions src/main/java/edu/cmipt/gcs/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import edu.cmipt.gcs.constant.HeaderParameter;
import edu.cmipt.gcs.constant.ValidationConstant;
import edu.cmipt.gcs.enumeration.ErrorCodeEnum;
import edu.cmipt.gcs.enumeration.TokenTypeEnum;
import edu.cmipt.gcs.exception.GenericException;
import edu.cmipt.gcs.pojo.error.ErrorVO;
import edu.cmipt.gcs.pojo.user.UserDTO;
import edu.cmipt.gcs.pojo.user.UserPO;
import edu.cmipt.gcs.pojo.user.UserVO;
import edu.cmipt.gcs.service.UserService;

import edu.cmipt.gcs.util.JwtUtil;
import edu.cmipt.gcs.validation.group.UpdateGroup;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
Expand All @@ -21,15 +24,19 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -46,7 +53,7 @@ public class UserController {
tags = {"User", "Get Method"})
@Parameters({
@Parameter(
name = HeaderParameter.TOKEN,
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
Expand Down Expand Up @@ -75,6 +82,50 @@ public UserVO getUserByName(@PathVariable("username") String username) {
return new UserVO(userService.getOne(wrapper));
}

@PostMapping(ApiPathConstant.USER_UPDATE_USER_API_PATH)
@Operation(
summary = "Update user",
description = "Update user information",
tags = {"User", "Post Method"})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User information updated successfully"),
@ApiResponse(
responseCode = "400",
description = "User information update failed",
content = @Content(schema = @Schema(implementation = ErrorVO.class)))
})
@Parameters({
@Parameter(
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class)),
@Parameter(
name = HeaderParameter.REFRESH_TOKEN,
description = "Refresh token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class))
})
public ResponseEntity<UserVO> updateUser(@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken, @RequestHeader(HeaderParameter.REFRESH_TOKEN) String refreshToken, @Validated(UpdateGroup.class) @RequestBody UserDTO user) {
if (user.username() != null) { checkUsernameValidity(user.username()); }
if (user.email() != null) { checkEmailValidity(user.email()); }
// for the null fields, mybatis-plus will ignore by default
assert user.id() != null;
boolean res = userService.updateById(new UserPO(user));
if (!res) {
throw new GenericException(ErrorCodeEnum.USER_UPDATE_FAILED, user.toString());
}
UserVO userVO = new UserVO(userService.getById(Long.valueOf(user.id())));
HttpHeaders headers = null;
if (user.userPassword() != null) {
JwtUtil.blacklistToken(accessToken, refreshToken);
headers = JwtUtil.generateHeaders(userVO.id());
}
return ResponseEntity.ok().headers(headers).body(userVO);
}

@GetMapping(ApiPathConstant.USER_CHECK_EMAIL_VALIDITY_API_PATH)
@Operation(
summary = "Check email validity",
Expand All @@ -90,7 +141,7 @@ public UserVO getUserByName(@PathVariable("username") String username) {
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Email validity checked successfully"),
@ApiResponse(
responseCode = "403",
responseCode = "400",
description = "Email is invalid",
content = @Content(schema = @Schema(implementation = ErrorVO.class)))
})
Expand Down Expand Up @@ -121,7 +172,7 @@ public void checkEmailValidity(
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Username validity checked successfully"),
@ApiResponse(
responseCode = "403",
responseCode = "400",
description = "Username is not valid",
content = @Content(schema = @Schema(implementation = ErrorVO.class)))
})
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public enum ErrorCodeEnum {
USERSIGNINDTO_USERNAME_NOTBLANK("UserSignInDTO.username.NotBlank"),
USERSIGNINDTO_USERPASSWORD_NOTBLANK("UserSignInDTO.userPassword.NotBlank"),

USERNAME_PATTERN_MISMATCH("USERNAME_PATTERN_MISMATCH"),
PASSWORD_PATTERN_MISMATCH("PASSWORD_PATTERN_MISMATCH"),

USERNAME_ALREADY_EXISTS("USERNAME_ALREADY_EXISTS"),
EMAIL_ALREADY_EXISTS("EMAIL_ALREADY_EXISTS"),
WRONG_SIGN_IN_INFORMATION("WRONG_SIGN_IN_INFORMATION"),
Expand All @@ -26,7 +29,9 @@ public enum ErrorCodeEnum {

MESSAGE_CONVERSION_ERROR("MESSAGE_CONVERSION_ERROR"),

USER_NOT_FOUND("USER_NOT_FOUND");
USER_NOT_FOUND("USER_NOT_FOUND"),

USER_UPDATE_FAILED("USER_UPDATE_FAILED");

// code means the error code in the message.properties
private String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.json.JsonParseException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand Down Expand Up @@ -81,6 +82,14 @@ public ResponseEntity<ErrorVO> handleGenericException(
}
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(JsonParseException.class)
public void handleJsonParseException(JsonParseException e, HttpServletRequest request) {
GenericException exception = new GenericException(e.getMessage());
exception.setCode(ErrorCodeEnum.MESSAGE_CONVERSION_ERROR);
handleGenericException(exception, request);
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public void handleException(Exception e) {
Expand Down
Loading

0 comments on commit 9a275ca

Please sign in to comment.