diff --git a/README-zh.md b/README-zh.md index 3272b18..6fe4540 100644 --- a/README-zh.md +++ b/README-zh.md @@ -8,14 +8,17 @@ | `profiles` | `list` | `["dev"]` | 启动的配置类型。 | | `deployLogLevel` | `string` | `"info"` | 部署脚本的日志级别。 | | `skipTest` | `bool` | `true` | 是否跳过测试。 | -| `createGitUser` | `bool` | `true` | 是否创建 `git` 用户。 | +| `gitUserName` | `string` | `"git"` | 用于保存 `git` 仓库的用户名。 | +| `gitUserPassword` | `string` | `"git"` | 用于保存 `git` 仓库的用户密码。 | +| `gitRepositoryDirectory` | `string` | `"/home/git/repository"` | `git` 仓库存放目录。不要使用 `~`。 | +| `gitServerDomain` | `string` | `"localhost"` | 服务器域名。 | +| `gitRepositorySuffix` | `string` | `".git"` | `git` 仓库后缀。 | | `deployWithDocker` | `bool` | `true` | 是否使用 `Docker` 进行部署。 | | `dockerName` | `string` | `"gcs-backend"` | `Docker` 容器名称。 | | `dockerImage` | `string` | `"ubuntu:latest"` | `Docker` 镜像。 | | `dockerPortMapping` | `list` | `["8080:8080"]` | `Docker` 端口映射。 | | `dockerWithGpu` | `bool` | `false` | `Docker` 是否使用 `GPU`。 | | `dockerSrcPath` | `string` | `"/opt/gcs-back-end-src"` | `Docker` 中源码路径。源码会被拷贝到该路径进行编译。 | -| `repositoryDirectory` | `string` | `"/home/git/repositories"` | `git` 仓库存放目录。 | | `serviceEnable` | `bool` | `true` | 是否启用 `systemd` 服务。 | | `serviceName` | `string` | `"gcs"` | 服务名称。 | | `serviceDescription` | `string` | `"Git server center back-end service"` | 服务描述。 | @@ -25,7 +28,7 @@ | `serviceStartJavaCommand` | `string` | `"/usr/bin/java"` | 服务启动的 `Java` 命令。 | | `serviceStartJavaArgs` | `list` | `["-jar"]` | 服务启动的 `Java` 参数。 | | `serviceStartJarFile` | `string` | `"/opt/gcs/gcs.jar"` | 服务启动的 `Jar` 文件。脚本会将 `maven` 打包出来的文件拷贝到该位置。 | -| `serviceSuffix` | `string` | `"service"` | `systemd` 服务文件后缀。 | +| `serviceSuffix` | `string` | `".service"` | `systemd` 服务文件后缀。 | | `serviceWorkingDirectory` | `string` | `"/opt/gcs"` | `systemd` 服务工作目录。 | | `serviceRestartPolicy` | `string` | `"always"` | `systemd` 服务重启策略。 | | `serviceRestartDelaySeconds` | `int` | `5` | `systemd` 服务重启延迟时间。 | @@ -44,3 +47,5 @@ | `druidLoginUsername` | `string` | `"druid"` | `Druid` 登录用户名。 | | `druidLoginPassword` | `string` | `"druid"` | `Druid` 登录密码。 | | `frontEndUrl` | `string` | `"http://localhost:3000"` | 前端地址。 | +| `deleteGitUser` | `bool` | `true` | 清理时是否删除 `git` 用户。 | +| `deleteServiceUser` | `bool` | `true` | 清理时是否删除 `service` 用户。 | diff --git a/config_debug.json b/config_debug.json index de8e133..588d6cd 100644 --- a/config_debug.json +++ b/config_debug.json @@ -9,5 +9,7 @@ ], "postgresqlUserName": "gcs_debug", "postgresqlUserPassword": "gcs_debug", - "postgresqlDatabaseName": "gcs_debug" + "postgresqlDatabaseName": "gcs_debug", + "deleteGitUser": false, + "deleteServiceUser": false } diff --git a/config_default.json b/config_default.json index 8a39ac7..2c13d0e 100644 --- a/config_default.json +++ b/config_default.json @@ -5,7 +5,11 @@ ], "deployLogLevel": "info", "skipTest": true, - "createGitUser": true, + "gitUserName": "git", + "gitUserPassword": "git", + "gitRepositoryDirectory": "/home/git/repository", + "gitServerDomain": "localhost", + "gitRepositorySuffix": ".git", "deployWithDocker": true, "dockerName": "gcs-back-end", "dockerImage": "ubuntu:20.04", @@ -14,7 +18,6 @@ ], "dockerWithGpu": false, "dockerSrcPath": "/opt/gcs-back-end-src", - "repositoryDirectory": "/home/git/repositories", "serviceEnable": true, "serviceName": "gcs", "serviceDescription": "Git server center back-end service", @@ -26,7 +29,7 @@ "-jar" ], "serviceStartJarFile": "/opt/gcs/gcs.jar", - "serviceSuffix": "service", + "serviceSuffix": ".service", "serviceWorkingDirectory": "/opt/gcs", "serviceRestartPolicy": "always", "serviceRestartDelaySeconds": 5, @@ -48,5 +51,7 @@ "postgresqlPort": 5432, "druidLoginUsername": "druid", "druidLoginPassword": "druid", - "frontEndUrl": "http://localhost:3000" + "frontEndUrl": "http://localhost:3000", + "deleteGitUser": true, + "deleteServiceUser": true } diff --git a/script/deploy_helper.py b/script/deploy_helper.py index ac92c73..c26b17f 100644 --- a/script/deploy_helper.py +++ b/script/deploy_helper.py @@ -14,7 +14,7 @@ import inspect essential_packages = ['python-is-python3', 'postgresql postgresql-client', - 'openjdk-17-jdk-headless', 'maven', 'systemd'] + 'openjdk-17-jdk-headless', 'maven', 'systemd', 'sudo'] sudo_cmd = os.popen('command -v sudo').read().strip() apt_updated = False message_tmp = '''\ @@ -90,7 +90,7 @@ def create_systemd_service(config): [config.serviceStartJarFile]) wanted_by = parse_iterable_into_str(config.serviceWantedBy) after = parse_iterable_into_str(config.serviceAfter) - service_full_path = f'{config.serviceSystemdDirectory}/{config.serviceName}.{config.serviceSuffix}' + service_full_path = f'{config.serviceSystemdDirectory}/{config.serviceName}{config.serviceSuffix}' gcs_file_content = f"""\ [Unit] Description={config.serviceDescription} @@ -334,7 +334,8 @@ def create_or_update_user(username, password): if username == None or username == "": return if os.system(f"cat /etc/passwd | grep -w -E '^{username}'") != 0: - command = f'{sudo_cmd} useradd {username}' + # use -m to create the home directory for user + command = f'{sudo_cmd} useradd -m {username}' res = os.system(command) message = message_tmp.format(command, res) command_checker(res, message) @@ -355,7 +356,11 @@ def create_or_update_user(username, password): def write_other_config(config): other_config_map = { - "frontEndUrl": "front-end.url" + "frontEndUrl": "front-end.url", + "gitServerDomain": "git.server.domain", + "gitUserName": "git.user.name", + "gitRepositoryDirectory": "git.repository.directory", + "gitRepositorySuffix": "git.repository.suffix", } try: lines = None @@ -374,6 +379,7 @@ def write_other_config(config): except Exception as e: command_checker(1, f"Error: {e}") + def deploy_on_ubuntu(config): assert(config != None) if config.inDocker: @@ -393,9 +399,19 @@ def deploy_on_ubuntu(config): res = os.system(command) message = message_tmp.format(command, res) command_checker(res, message) + create_or_update_user(config.gitUserName, config.gitUserPassword) + create_or_update_user(config.serviceUser, config.serviceUserPassword) + # let the service user can use git command as the git user without password + sudoers_entry = f"{config.serviceUser} ALL=(git) NOPASSWD: /usr/bin/git" + try: + res = subprocess.run(f"echo '{sudoers_entry}' | {sudo_cmd} tee /etc/sudoers.d/{config.serviceUser}", shell=True); + command_checker(res.returncode, f"Failed to create /etc/sudoers.d/{config.serviceUser}") + res = subprocess.run(f"{sudo_cmd} chmod 440 /etc/sudoers.d/{config.serviceUser}", shell=True) + command_checker(res.returncode, f"Failed to chmod 440 /etc/sudoers.d/{config.serviceUser}") + except subprocess.CalledProcessError as e: + command_checker(1, f"Error: {e}") if config.deploy: - create_or_update_user(config.serviceUser, config.serviceUserPassword) if not os.path.exists(os.path.dirname(config.serviceStartJarFile)): command = f'{sudo_cmd} mkdir -p {os.path.dirname(config.serviceStartJarFile)}' res = os.system(command) @@ -411,6 +427,15 @@ def deploy_on_ubuntu(config): deploy_with_systemd(config) +def delete_user(username): + if username == None or username == "": + return + if os.system(f"cat /etc/passwd | grep -w -E '^{username}'") == 0: + command = f'{sudo_cmd} userdel {username}' + res = os.system(command) + message = message_tmp.format(command, res) + command_checker(res, message) + def clean(config): if config.deployWithDocker: res = os.system(f"docker stop {config.dockerName}") @@ -426,8 +451,8 @@ def clean(config): res = os.system(command) message = message_tmp.format(command, res) command_checker(res, message) - if os.path.exists(f'{config.serviceSystemdDirectory}/{config.serviceName}.{config.serviceSuffix}'): - command = f'''{sudo_cmd} rm -rf {config.serviceSystemdDirectory}/{config.serviceName}.{config.serviceSuffix} && \\ + if os.path.exists(f'{config.serviceSystemdDirectory}/{config.serviceName}{config.serviceSuffix}'): + command = f'''{sudo_cmd} rm -rf {config.serviceSystemdDirectory}/{config.serviceName}{config.serviceSuffix} && \\ {sudo_cmd} systemctl daemon-reload''' res = os.system(command) message = message_tmp.format(command, res) @@ -451,11 +476,15 @@ def clean(config): res = os.system(command) message = message_tmp.format(command, res) command_checker(res, message) - if os.system(f"cat /etc/passwd | grep -w -E '^{config.serviceUser}'") == 0: - command = f'{sudo_cmd} userdel {config.serviceUser}' + if os.path.exists(f'/etc/sudoers.d/{config.serviceUser}'): + command = f'{sudo_cmd} rm -rf /etc/sudoers.d/{config.serviceUser}' res = os.system(command) message = message_tmp.format(command, res) command_checker(res, message) + if config.deleteGitUser: + delete_user(config.gitUserName) + if config.deleteServiceUser: + delete_user(config.serviceUser) command = f'mvn clean' res = os.system(command) message = message_tmp.format(command, res) diff --git a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java index 8bc2d0d..2ace9a0 100644 --- a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java +++ b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java @@ -28,4 +28,7 @@ public class ApiPathConstant { public static final String USER_DELETE_USER_API_PATH = USER_API_PREFIX + "/delete"; public static final String USER_PAGE_USER_REPOSITORY_API_PATH = USER_API_PREFIX + "/page/repository"; + + public static final String REPOSITORY_API_PREFIX = ALL_API_PREFIX + "/repository"; + public static final String REPOSITORY_CREATE_REPOSITORY_API_PATH = REPOSITORY_API_PREFIX + "/create"; } diff --git a/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java b/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java index e1805ab..d8ffc60 100644 --- a/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java +++ b/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java @@ -77,7 +77,7 @@ public void signUp(@Validated(CreateGroup.class) @RequestBody UserDTO user) { } boolean res = userService.save(new UserPO(user)); if (!res) { - throw new RuntimeException("Failed to sign up user"); + throw new GenericException(ErrorCodeEnum.USER_CREATE_FAILED, user); } } @@ -149,7 +149,7 @@ public ResponseEntity refreshToken( @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken, @RequestHeader(HeaderParameter.REFRESH_TOKEN) String refreshToken) { JwtUtil.blacklistToken(accessToken); - HttpHeaders headers = JwtUtil.generateHeaders(JwtUtil.getID(refreshToken), false); + HttpHeaders headers = JwtUtil.generateHeaders(JwtUtil.getId(refreshToken), false); return ResponseEntity.ok().headers(headers).build(); } } diff --git a/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java b/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java new file mode 100644 index 0000000..f9205bc --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java @@ -0,0 +1,61 @@ +package edu.cmipt.gcs.controller; + +import edu.cmipt.gcs.service.RepositoryService; +import edu.cmipt.gcs.util.JwtUtil; +import edu.cmipt.gcs.validation.group.CreateGroup; +import edu.cmipt.gcs.constant.ApiPathConstant; +import edu.cmipt.gcs.constant.HeaderParameter; +import edu.cmipt.gcs.enumeration.ErrorCodeEnum; +import edu.cmipt.gcs.exception.GenericException; +import edu.cmipt.gcs.pojo.repository.RepositoryDTO; +import edu.cmipt.gcs.pojo.repository.RepositoryPO; +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.media.Schema; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +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.RestController; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +@RestController +@Tag(name = "Repository", description = "Repository Related APIs") +public class RepositoryController { + @Autowired private RepositoryService repositoryService; + + @PostMapping(ApiPathConstant.REPOSITORY_CREATE_REPOSITORY_API_PATH) + @Operation( + summary = "Create a repository", + description = "Create a repository with the given information", + tags = {"Repository", "Post Method"}) + @Parameters({ + @Parameter( + name = HeaderParameter.ACCESS_TOKEN, + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)) + }) + @ApiResponse(responseCode = "200", description = "Repository created successfully") + public void createRepository(@Validated(CreateGroup.class) @RequestBody RepositoryDTO repository, @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + String userId = JwtUtil.getId(accessToken); + RepositoryPO repositoryPO = new RepositoryPO(repository, userId); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("user_id", repositoryPO.getUserId()); + queryWrapper.eq("repository_name", repositoryPO.getRepositoryName()); + if (repositoryService.exists(queryWrapper)) { + throw new GenericException(ErrorCodeEnum.REPOSITORY_ALREADY_EXISTS, repository); + } + if (!repositoryService.save(repositoryPO)) { + throw new GenericException(ErrorCodeEnum.REPOSITORY_CREATE_FAILED, repository); + } + } +} diff --git a/src/main/java/edu/cmipt/gcs/controller/UserController.java b/src/main/java/edu/cmipt/gcs/controller/UserController.java index b0a9bc1..49211ac 100644 --- a/src/main/java/edu/cmipt/gcs/controller/UserController.java +++ b/src/main/java/edu/cmipt/gcs/controller/UserController.java @@ -1,6 +1,7 @@ package edu.cmipt.gcs.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import edu.cmipt.gcs.constant.ApiPathConstant; @@ -129,8 +130,7 @@ public ResponseEntity updateUser( } // for the null fields, mybatis-plus will ignore by default assert user.id() != null; - boolean res = userService.updateById(new UserPO(user)); - if (!res) { + if (!userService.updateById(new UserPO(user))) { throw new GenericException(ErrorCodeEnum.USER_UPDATE_FAILED, user); } UserVO userVO = new UserVO(userService.getById(Long.valueOf(user.id()))); @@ -181,8 +181,7 @@ public void deleteUser( if (userService.getById(id) == null) { throw new GenericException(ErrorCodeEnum.USER_NOT_FOUND, id); } - boolean res = userService.removeById(id); - if (!res) { + if (!userService.removeById(id)) { throw new GenericException(ErrorCodeEnum.USER_DELETE_FAILED, id); } JwtUtil.blacklistToken(accessToken, refreshToken); @@ -224,20 +223,20 @@ public void deleteUser( schema = @Schema(implementation = Integer.class)) }) @ApiResponse(responseCode = "200", description = "User repositories paged successfully") - public List pageUserRepositories( + public List pageUserRepository( @RequestParam("id") Long userId, @RequestParam("page") Integer page, @RequestParam("size") Integer size, @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { QueryWrapper wrapper = new QueryWrapper(); - String idInToken = JwtUtil.getID(accessToken); + String idInToken = JwtUtil.getId(accessToken); assert idInToken != null; if (!idInToken.equals(userId.toString())) { // the user only can see the public repositories of others wrapper.eq("is_private", false); } wrapper.eq("user_id", userId); - return repositoryService.page(new Page<>(page, size), wrapper).getRecords().stream() + return repositoryService.list(new Page<>(page, size), wrapper).stream() .map(RepositoryVO::new) .collect(Collectors.toList()); } diff --git a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java index 89d618a..553f58f 100644 --- a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java +++ b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java @@ -31,6 +31,7 @@ public enum ErrorCodeEnum { USER_NOT_FOUND("USER_NOT_FOUND"), + USER_CREATE_FAILED("USER_CREATE_FAILED"), USER_UPDATE_FAILED("USER_UPDATE_FAILED"), USER_DELETE_FAILED("USER_DELETE_FAILED"), @@ -39,11 +40,16 @@ public enum ErrorCodeEnum { REPOSITORYDTO_REPOSITORYNAME_SIZE("RepositoryDTO.repositoryName.Size"), REPOSITORYDTO_REPOSITORYNAME_NOTBLANK("RepositoryDTO.repositoryName.NotBlank"), REPOSITORYDTO_REPOSITORYDESCRIPTION_SIZE("RepositoryDTO.repositoryDescription.Size"), + REPOSITORYDTO_STAR_NULL("RepositoryDTO.star.Null"), REPOSITORYDTO_STAR_MIN("RepositoryDTO.star.Min"), + REPOSITORYDTO_FORK_NULL("RepositoryDTO.fork.Null"), REPOSITORYDTO_FORK_MIN("RepositoryDTO.fork.Min"), + REPOSITORYDTO_WATCHER_NULL("RepositoryDTO.watcher.Null"), REPOSITORYDTO_WATCHER_MIN("RepositoryDTO.watcher.Min"), - REPOSITORYNAME_PATTERN_MISMATCH("REPOSITORYNAME_PATTERN_MISMATCH"); + REPOSITORYNAME_PATTERN_MISMATCH("REPOSITORYNAME_PATTERN_MISMATCH"), + REPOSITORY_ALREADY_EXISTS("REPOSITORY_ALREADY_EXISTS"), + REPOSITORY_CREATE_FAILED("REPOSITORY_CREATE_FAILED"); // code means the error code in the message.properties private String code; diff --git a/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java b/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java index 6962c84..429a2ab 100644 --- a/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java +++ b/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java @@ -162,7 +162,7 @@ private void authorize(HttpServletRequest request, String accessToken, String re throw new GenericException(ErrorCodeEnum.TOKEN_NOT_FOUND); } // User can not update other user's information - String idInToken = JwtUtil.getID(accessToken); + String idInToken = JwtUtil.getId(accessToken); String idInBody = getFromRequestBody(request, "id"); if (!idInToken.equals(idInBody)) { logger.info("User[{}] tried to update user[{}]", idInToken, idInBody); @@ -173,6 +173,8 @@ private void authorize(HttpServletRequest request, String accessToken, String re && refreshToken == null) { // for refresh token, both access token and refresh token are needed throw new GenericException(ErrorCodeEnum.TOKEN_NOT_FOUND); + } else if (request.getRequestURI().equals(ApiPathConstant.REPOSITORY_CREATE_REPOSITORY_API_PATH)) { + // pass } else { throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); } @@ -187,7 +189,7 @@ private void authorize(HttpServletRequest request, String accessToken, String re throw new GenericException(ErrorCodeEnum.TOKEN_NOT_FOUND); } // User can not delete other user - String idInToken = JwtUtil.getID(accessToken); + String idInToken = JwtUtil.getId(accessToken); String idInParam = request.getParameter("id"); if (!idInToken.equals(idInParam)) { logger.info("User[{}] tried to delete user[{}]", idInToken, idInParam); diff --git a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java index a6e5957..f2472d5 100644 --- a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java +++ b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryDTO.java @@ -35,7 +35,7 @@ public record RepositoryDTO( "REPOSITORYDTO_REPOSITORYNAME_SIZE" + " {RepositoryDTO.repositoryName.Size}") @NotBlank( - groups = {CreateGroup.class}, + groups = CreateGroup.class, message = "REPOSITORYDTO_REPOSITORYNAME_NOTBLANK" + " {RepositoryDTO.repositoryName.NotBlank}") @@ -56,20 +56,29 @@ public record RepositoryDTO( String repositoryDescription, @Schema(description = "Whether or Not Private Repo") Boolean isPrivate, @Schema(description = "Star Count") + @Null( + groups = CreateGroup.class, + message = "REPOSITORYDTO_STAR_NULL {RepositoryDTO.star.Null}") @Min( - groups = {CreateGroup.class, UpdateGroup.class}, + groups = UpdateGroup.class, value = 0, message = "REPOSITORYDTO_STAR_MIN {RepositoryDTO.star.Min}") Integer star, @Schema(description = "Fork Count") + @Null( + groups = CreateGroup.class, + message = "REPOSITORYDTO_FORK_NULL {RepositoryDTO.fork.Null}") @Min( - groups = {CreateGroup.class, UpdateGroup.class}, + groups = UpdateGroup.class, value = 0, message = "REPOSITORYDTO_FORK_MIN {RepositoryDTO.fork.Min}") Integer fork, @Schema(description = "Watcher Count") + @Null( + groups = CreateGroup.class, + message = "REPOSITORYDTO_WATCHER_NULL {RepositoryDTO.watcher.Null}") @Min( - groups = {CreateGroup.class, UpdateGroup.class}, + groups = UpdateGroup.class, value = 0, message = "REPOSITORYDTO_WATCHER_MIN {RepositoryDTO.watcher.Min}") Integer watcher) {} diff --git a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java index 4e35bbe..7781c23 100644 --- a/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java +++ b/src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java @@ -3,11 +3,13 @@ import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; import lombok.Data; import java.time.LocalDateTime; @Data +@AllArgsConstructor @TableName("t_repository") public class RepositoryPO { private Long id; @@ -22,7 +24,7 @@ public class RepositoryPO { private LocalDateTime gmtUpdated; @TableLogic private LocalDateTime gmtDeleted; - public RepositoryPO(RepositoryDTO repositoryDTO, Long userId) { + public RepositoryPO(RepositoryDTO repositoryDTO, String userId) { try { this.id = Long.valueOf(repositoryDTO.id()); } catch (NumberFormatException e) { @@ -30,8 +32,13 @@ public RepositoryPO(RepositoryDTO repositoryDTO, Long userId) { } this.repositoryName = repositoryDTO.repositoryName(); this.repositoryDescription = repositoryDTO.repositoryDescription(); + if (this.repositoryDescription == null) { this.repositoryDescription = ""; } this.isPrivate = repositoryDTO.isPrivate(); - this.userId = userId; + try { + this.userId = Long.valueOf(userId); + } catch (NumberFormatException e) { + this.userId = null; + } this.star = repositoryDTO.star(); this.fork = repositoryDTO.fork(); this.watcher = repositoryDTO.watcher(); diff --git a/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java b/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java index be7124b..b27a5c5 100644 --- a/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java +++ b/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java @@ -3,10 +3,63 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import edu.cmipt.gcs.dao.RepositoryMapper; +import edu.cmipt.gcs.dao.UserMapper; import edu.cmipt.gcs.pojo.repository.RepositoryPO; +import java.nio.file.Paths; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Service public class RepositoryServiceImpl extends ServiceImpl - implements RepositoryService {} +implements RepositoryService { + private static final Logger logger = LoggerFactory.getLogger(RepositoryServiceImpl.class); + + @Autowired + private UserMapper userMapper; + @Value("${git.user.name}") + private String gitUserName; + @Value("${git.repository.directory}") + private String gitRepositoryDirectory; + @Value("${git.repository.suffix}") + private String gitRepositorySuffix; + @Value("${git.server.domain}") + private String gitServerDomain; + + @Override + public boolean save(RepositoryPO repositoryPO) { + String repositorySavePath = Paths.get(gitRepositoryDirectory, userMapper.selectById(repositoryPO.getUserId()).getUsername(), repositoryPO.getRepositoryName() + gitRepositorySuffix).toString(); + logger.info("Repository save path: {}", repositorySavePath); + try { + ProcessBuilder repositoryInitializer = new ProcessBuilder("sudo", "-u", gitUserName, "git", "init", "--bare", repositorySavePath); + Process process = repositoryInitializer.start(); + if (process.waitFor() != 0) { + logger.error("Failed to initialize repository"); + process.errorReader().lines().forEach(logger::error); + return false; + } + // TODO: add url in the repositoryPO + } catch (Exception e) { + logger.error("Failed to initialize repository: {}", e.getMessage()); + return false; + } + if (!super.save(repositoryPO)) { + try { + ProcessBuilder dirRemover = new ProcessBuilder("sudo", "-u", gitUserName, "rm", "-rf", repositorySavePath); + Process process = dirRemover.start(); + if(process.waitFor() != 0) { + logger.error("Failed to remove repository directory"); + process.errorReader().lines().forEach(logger::error); + } + } catch (Exception e) { + logger.error("Failed to remove repository directory: {}", e.getMessage()); + } + return false; + } + return true; + } +} diff --git a/src/main/java/edu/cmipt/gcs/util/JwtUtil.java b/src/main/java/edu/cmipt/gcs/util/JwtUtil.java index 169ad86..3767283 100644 --- a/src/main/java/edu/cmipt/gcs/util/JwtUtil.java +++ b/src/main/java/edu/cmipt/gcs/util/JwtUtil.java @@ -53,7 +53,7 @@ public static String generateToken(String id, TokenTypeEnum tokenType) { return generateToken(Long.valueOf(id), tokenType); } - public static String getID(String token) { + public static String getId(String token) { try { return String.valueOf( Jwts.parser() diff --git a/src/main/resources/message/message.properties b/src/main/resources/message/message.properties index 48edbd8..b994d89 100644 --- a/src/main/resources/message/message.properties +++ b/src/main/resources/message/message.properties @@ -27,6 +27,7 @@ MESSAGE_CONVERSION_ERROR=Error occurs while converting message USER_NOT_FOUND=User not found: {} +USER_CREATE_FAILED=User create failed: {} USER_UPDATE_FAILED=User update failed: {} USER_DELETE_FAILED=User delete failed: {} @@ -35,8 +36,13 @@ RepositoryDTO.id.NotNull=Repository id cannot be null RepositoryDTO.name.Size=Repository name must be between {min} and {max} characters RepositoryDTO.name.NotBlank=Repository name cannot be blank RepositoryDTO.description.Size=Repository description must be between {min} and {max} characters +RepositoryDTO.star.Null=Start must be null when creating a new repository RepositoryDTO.star.Min=Star must be greater than or equal to {value} +RepositoryDTO.fork.Null=Fork must be null when creating a new repository RepositoryDTO.fork.Min=Fork must be greater than or equal to {value} +RepositoryDTO.watcher.Null=Watcher must be null when creating a new repository RepositoryDTO.watcher.Min=Watcher must be greater than or equal to {value} REPOSITORYNAME_PATTERN_MISMATCH=Repository name can only be alphanumeric, hyphen or underline +REPOSITORY_ALREADY_EXISTS=Repository already exists: {} +REPOSITORY_CREATE_FAILED=Repository create failed: {} diff --git a/src/test/java/edu/cmipt/gcs/GcsApplicationTest.java b/src/test/java/edu/cmipt/gcs/GcsApplicationTest.java deleted file mode 100644 index 0a13a39..0000000 --- a/src/test/java/edu/cmipt/gcs/GcsApplicationTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package edu.cmipt.gcs; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class GcsApplicationTest { - - @Test - void contextLoads() {} -} diff --git a/src/test/java/edu/cmipt/gcs/SpringBootTestClassOrderer.java b/src/test/java/edu/cmipt/gcs/SpringBootTestClassOrderer.java new file mode 100644 index 0000000..2b7009a --- /dev/null +++ b/src/test/java/edu/cmipt/gcs/SpringBootTestClassOrderer.java @@ -0,0 +1,34 @@ +package edu.cmipt.gcs; + +import org.junit.jupiter.api.ClassDescriptor; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.ClassOrdererContext; + +import edu.cmipt.gcs.controller.AuthenticationControllerTest; +import edu.cmipt.gcs.controller.RepositoryControllerTest; +import edu.cmipt.gcs.controller.UserControllerTest; + +import java.util.Comparator; + +public class SpringBootTestClassOrderer implements ClassOrderer { + + private static final Class[] classOrder = new Class[] { + AuthenticationControllerTest.class, + RepositoryControllerTest.class, + UserControllerTest.class + }; + + @Override + public void orderClasses(ClassOrdererContext classOrdererContext) { + classOrdererContext.getClassDescriptors().sort(Comparator.comparingInt(SpringBootTestClassOrderer::getOrder)); + } + + private static int getOrder(ClassDescriptor classDescriptor) { + for (int i = 0; i < classOrder.length; i++) { + if (classDescriptor.getTestClass().equals(classOrder[i])) { + return i; + } + } + return Integer.MAX_VALUE; + } +} diff --git a/src/test/java/edu/cmipt/gcs/constant/TestConstant.java b/src/test/java/edu/cmipt/gcs/constant/TestConstant.java index 517eb51..40319a1 100644 --- a/src/test/java/edu/cmipt/gcs/constant/TestConstant.java +++ b/src/test/java/edu/cmipt/gcs/constant/TestConstant.java @@ -9,4 +9,11 @@ public class TestConstant { public static String EMAIL = USERNAME + "@cmipt.edu"; public static String ACCESS_TOKEN; public static String REFRESH_TOKEN; + public static String OTHER_ID; + public static String OTHER_USERNAME = new Date().getTime() + "other"; + public static String OTHER_USER_PASSWORD = "123456"; + public static String OTHER_EMAIL = OTHER_USERNAME + "@cmipt.edu"; + public static String OTHER_ACCESS_TOKEN; + public static String OTHER_REFRESH_TOKEN; + public static Integer REPOSITORY_SIZE = 10; } diff --git a/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java b/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java index 6c97cda..71f6afc 100644 --- a/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java +++ b/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java @@ -34,7 +34,6 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -@Order(Ordered.HIGHEST_PRECEDENCE) public class AuthenticationControllerTest { @Autowired private MockMvc mvc; @@ -48,6 +47,18 @@ public class AuthenticationControllerTest { """ .formatted( TestConstant.USERNAME, TestConstant.EMAIL, TestConstant.USER_PASSWORD); + private static String otherUserDTO = + """ + { + "username": "%s", + "email": "%s", + "userPassword": "%s" + } + """ + .formatted( + TestConstant.OTHER_USERNAME, + TestConstant.OTHER_EMAIL, + TestConstant.OTHER_USER_PASSWORD); private static String userSignInDTO = """ { @@ -56,6 +67,14 @@ public class AuthenticationControllerTest { } """ .formatted(TestConstant.USERNAME, TestConstant.USER_PASSWORD); + public static String otherUserSignInDTO = + """ + { + "username": "%s", + "userPassword": "%s" + } + """ + .formatted(TestConstant.OTHER_USERNAME, TestConstant.OTHER_USER_PASSWORD); private static String invalidUserDTO = """ @@ -89,6 +108,11 @@ public void testSignUpValid() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(userDTO)) .andExpect(status().isOk()); + mvc.perform( + post(ApiPathConstant.AUTHENTICATION_SIGN_UP_API_PATH) + .contentType(MediaType.APPLICATION_JSON) + .content(otherUserDTO)) + .andExpect(status().isOk()); } /** @@ -123,6 +147,27 @@ public void testSignInValid() throws Exception { .parseMap(response.getContentAsString()) .get("id") .toString(); + var otherResponse = + mvc.perform( + post(ApiPathConstant.AUTHENTICATION_SIGN_IN_API_PATH) + .contentType(MediaType.APPLICATION_JSON) + .content(otherUserSignInDTO)) + .andExpectAll( + status().isOk(), + jsonPath("$.username", is(TestConstant.OTHER_USERNAME)), + jsonPath("$.email", is(TestConstant.OTHER_EMAIL)), + jsonPath("$.id").isString(), + header().exists(HeaderParameter.ACCESS_TOKEN), + header().exists(HeaderParameter.REFRESH_TOKEN)) + .andReturn() + .getResponse(); + TestConstant.OTHER_ID = + JsonParserFactory.getJsonParser() + .parseMap(otherResponse.getContentAsString()) + .get("id") + .toString(); + TestConstant.OTHER_ACCESS_TOKEN = otherResponse.getHeader(HeaderParameter.ACCESS_TOKEN); + TestConstant.OTHER_REFRESH_TOKEN = otherResponse.getHeader(HeaderParameter.REFRESH_TOKEN); } /** diff --git a/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java b/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java new file mode 100644 index 0000000..537767f --- /dev/null +++ b/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java @@ -0,0 +1,50 @@ +package edu.cmipt.gcs.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import edu.cmipt.gcs.constant.ApiPathConstant; +import edu.cmipt.gcs.constant.HeaderParameter; +import edu.cmipt.gcs.constant.TestConstant; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * Tests for RepositoryController + * + * @author Kaiser + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class RepositoryControllerTest { + @Autowired private MockMvc mvc; + + @Test + public void testCreateRepositoryValid() throws Exception { + String isPrivate = ""; + String repositoryName = ""; + for (int i = 0; i < TestConstant.REPOSITORY_SIZE; i++) { + if (i % 2 == 0) { isPrivate = "true"; } + else { isPrivate = "false"; } + repositoryName = String.valueOf(i); + mvc.perform( + post(ApiPathConstant.REPOSITORY_CREATE_REPOSITORY_API_PATH) + .header(HeaderParameter.ACCESS_TOKEN, TestConstant.ACCESS_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "repositoryName": "%s", + "isPrivate": %s + } + """ + .formatted(repositoryName, isPrivate))) + .andExpect(status().isOk()); + } + } +} diff --git a/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java b/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java index 60d5513..a7ad2dd 100644 --- a/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java +++ b/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java @@ -36,7 +36,6 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -@Order(Ordered.LOWEST_PRECEDENCE) public class UserControllerTest { @Autowired private MockMvc mvc; @@ -255,4 +254,30 @@ public void testDeleteUserValid() throws Exception { .param("id", TestConstant.ID)) .andExpectAll(status().isOk()); } + + @Test + public void testPageUserRepositoryValid() throws Exception { + mvc.perform( + get(ApiPathConstant.USER_PAGE_USER_REPOSITORY_API_PATH) + .header(HeaderParameter.ACCESS_TOKEN, TestConstant.ACCESS_TOKEN) + .param("id", TestConstant.ID) + .param("page", "1") + .param("size", TestConstant.REPOSITORY_SIZE.toString())) + .andExpectAll(status().isOk(), + jsonPath("$").isArray(), + jsonPath("$.length()").value(TestConstant.REPOSITORY_SIZE)); + } + + @Test + public void testPageOtherUserRepositoryValid() throws Exception { + mvc.perform( + get(ApiPathConstant.USER_PAGE_USER_REPOSITORY_API_PATH) + .header(HeaderParameter.ACCESS_TOKEN, TestConstant.OTHER_ACCESS_TOKEN) + .param("id", TestConstant.ID) + .param("page", "1") + .param("size", TestConstant.REPOSITORY_SIZE.toString())) + .andExpectAll(status().isOk(), + jsonPath("$").isArray(), + jsonPath("$.length()").value(TestConstant.REPOSITORY_SIZE / 2)); + } } diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..5a0cb8f --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testclass.order.default=edu.cmipt.gcs.SpringBootTestClassOrderer