diff --git a/src/main/java/com/alist/api/common/modules/file/FileController.java b/src/main/java/com/alist/api/common/modules/file/FileController.java new file mode 100644 index 0000000..b3b865c --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/FileController.java @@ -0,0 +1,67 @@ +package com.alist.api.common.modules.file; + +import com.alist.api.common.modules.file.dto.UploadTokenDto; +import com.alist.api.common.modules.file.form.UploadTokenForm; +import com.alist.api.common.response.ApiResponse; +import com.alist.api.common.response.ApiResponseCode; +import com.alist.api.common.modules.file.dto.UploadStartDto; +import com.alist.api.common.modules.file.form.UploadStartForm; +import com.alist.api.common.modules.file.service.FileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@Tag(name = "99. 파일 관리", description = "파일 관리") +@RestController +@RequestMapping("/files") +public class FileController { + private final FileService fileService; + + public FileController(FileService fileService) { + this.fileService = fileService; + } + + @Operation( + summary = "파일 업로드 시작", + description = "업로드 토큰 획득 및 업로드 정보 db 기록" + ) + @PostMapping("/uploadStart") + public ResponseEntity> uploadStart(@Valid @RequestBody UploadStartForm uploadStartForm, HttpServletRequest request) { + HttpSession session = request.getSession(false); + Integer userTokenIdx = (session == null) ? null : (Integer) session.getAttribute("userTokenIdx"); + Integer userIdx = (session == null) ? null : (Integer) session.getAttribute("userIdx"); + + if (userTokenIdx == null || userIdx == null) { + return ApiResponse.entity(new UploadStartDto(), ApiResponseCode.CODE_401); + } + + UploadStartDto uploadInitDto = fileService.insertFileMasterUpload(userIdx, userTokenIdx, uploadStartForm.uploadStartDto()); + + return ApiResponse.entity(uploadInitDto, ApiResponseCode.CODE_200); + } + + @Operation( + summary = "업로드 권한 체크", + description = "업로드 토큰 발급 후 실제 업로드 권한이 있는지 확인 과정입니다." + ) + @PostMapping("/uploadAuthCheck") + public ResponseEntity> uploadAuthCheck(@RequestHeader(value = "Authorization", required = false) String uploadToken, UploadTokenForm uploadTokenForm) { + uploadTokenForm.setUploadToken(uploadToken.substring(7).trim()); + + UploadTokenDto uploadTokenInfo = fileService.isUploadTokenCheck(uploadTokenForm.uploadTokenDto()); + + if (uploadTokenInfo.getResultCode() == 401) { + return ApiResponse.entity("", ApiResponseCode.CODE_401); + } else if (uploadTokenInfo.getResultCode() == 403) { + return ApiResponse.entity("", ApiResponseCode.CODE_403); + } + + return ApiResponse.entity("", ApiResponseCode.CODE_200); + } +} diff --git a/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java b/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java new file mode 100644 index 0000000..3eeeb73 --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java @@ -0,0 +1,28 @@ +package com.alist.api.common.modules.file.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UploadStartDto { + private String fileMasterKey; + private String tusEndpoint; + private String uploadToken; + + @JsonIgnore + String originName; + @JsonIgnore + String contentType; + @JsonIgnore + Long sizeBytes; + @JsonIgnore + String fileCategory; + @JsonIgnore + String folderPath; + @JsonIgnore + Integer userIdx; + @JsonIgnore + int status; +} diff --git a/src/main/java/com/alist/api/common/modules/file/dto/UploadTokenDto.java b/src/main/java/com/alist/api/common/modules/file/dto/UploadTokenDto.java new file mode 100644 index 0000000..4717fa9 --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/dto/UploadTokenDto.java @@ -0,0 +1,16 @@ +package com.alist.api.common.modules.file.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UploadTokenDto { + private Integer userTokenIdx; + private String fileMasterKey; + private String uploadToken; + + @JsonIgnore + private int resultCode; +} diff --git a/src/main/java/com/alist/api/common/modules/file/form/UploadCompleteForm.java b/src/main/java/com/alist/api/common/modules/file/form/UploadCompleteForm.java new file mode 100644 index 0000000..7fff8e2 --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/form/UploadCompleteForm.java @@ -0,0 +1,33 @@ +package com.alist.api.common.modules.file.form; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UploadCompleteForm { + @NotBlank + String fileMastryKey; + + @NotBlank + String tusUploadUrl; + + @NotBlank + String originName; + + @NotBlank + String saveName; + + @NotBlank + String savePath; + + @NotBlank + String contentType; + + @NotNull + @Positive + Long sizeBytes; +} diff --git a/src/main/java/com/alist/api/common/modules/file/form/UploadStartForm.java b/src/main/java/com/alist/api/common/modules/file/form/UploadStartForm.java new file mode 100644 index 0000000..e1c99d8 --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/form/UploadStartForm.java @@ -0,0 +1,39 @@ +package com.alist.api.common.modules.file.form; + +import com.alist.api.common.modules.file.dto.UploadStartDto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UploadStartForm { + @NotBlank + private String originName; + + @NotBlank + private String contentType; + + @NotNull + @Positive + private Long sizeBytes; + + @NotBlank + private String fileCategory; + + private String folderPath; + + public UploadStartDto uploadStartDto() { + UploadStartDto uploadStartDto = new UploadStartDto(); + + uploadStartDto.setOriginName(this.originName); + uploadStartDto.setContentType(this.contentType); + uploadStartDto.setSizeBytes(this.sizeBytes); + uploadStartDto.setFileCategory(this.fileCategory); + uploadStartDto.setFolderPath(this.folderPath); + + return uploadStartDto; + } +} diff --git a/src/main/java/com/alist/api/common/modules/file/form/UploadTokenForm.java b/src/main/java/com/alist/api/common/modules/file/form/UploadTokenForm.java new file mode 100644 index 0000000..e566a1d --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/form/UploadTokenForm.java @@ -0,0 +1,20 @@ +package com.alist.api.common.modules.file.form; + +import com.alist.api.common.modules.file.dto.UploadTokenDto; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class UploadTokenForm { + private String fileMasterKey; + private String uploadToken; + + public UploadTokenDto uploadTokenDto() { + UploadTokenDto uploadTokenDto = new UploadTokenDto(); + uploadTokenDto.setFileMasterKey(this.fileMasterKey); + uploadTokenDto.setUploadToken(this.uploadToken); + + return uploadTokenDto; + } +} diff --git a/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java b/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java new file mode 100644 index 0000000..6e32ac4 --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java @@ -0,0 +1,12 @@ +package com.alist.api.common.modules.file.mapper; + +import com.alist.api.common.modules.file.dto.UploadStartDto; +import com.alist.api.common.modules.file.dto.UploadTokenDto; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileMapper { + void insertFileMasterUpload(UploadStartDto uploadStartDto); + + int selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(UploadTokenDto uploadTokenDto); +} diff --git a/src/main/java/com/alist/api/common/modules/file/service/FileService.java b/src/main/java/com/alist/api/common/modules/file/service/FileService.java new file mode 100644 index 0000000..af16dd9 --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/service/FileService.java @@ -0,0 +1,70 @@ +package com.alist.api.common.modules.file.service; + +import com.alist.api.common.modules.file.dto.UploadStartDto; +import com.alist.api.common.modules.file.dto.UploadTokenDto; +import com.alist.api.common.modules.file.mapper.FileMapper; +import com.alist.api.config.jwt.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class FileService { + @Value("${file.upload.tus-endpoint}") + private String tusEndpoint; + + @Value("${file.upload.public-base-url}") + private String publicBaseUrl; + + private final FileMapper fileMapper; + private final JwtTokenProvider jwtTokenProvider; + + public FileService(FileMapper fileMapper, JwtTokenProvider jwtTokenProvider) { + this.fileMapper = fileMapper; + this.jwtTokenProvider = jwtTokenProvider; + } + + public UploadStartDto insertFileMasterUpload(Integer userIdx, Integer userTokenIdx, UploadStartDto uploadStartDto) { + uploadStartDto.setFileMasterKey(UUID.randomUUID().toString()); + uploadStartDto.setUserIdx(userIdx); + uploadStartDto.setStatus(1); + uploadStartDto.setFolderPath(uploadStartDto.getFolderPath() == null ? "/" : uploadStartDto.getFolderPath()); + + fileMapper.insertFileMasterUpload(uploadStartDto); + + uploadStartDto.setUploadToken(jwtTokenProvider.createUploadToken(userTokenIdx)); + uploadStartDto.setTusEndpoint(tusEndpoint); + + return uploadStartDto; + } + + public UploadTokenDto isUploadTokenCheck(UploadTokenDto uploadTokenDto) { + if (!jwtTokenProvider.validateToken(uploadTokenDto.getUploadToken())) { + uploadTokenDto.setResultCode(401); + return uploadTokenDto; + } + + if (!jwtTokenProvider.hasUploadScope(uploadTokenDto.getUploadToken())) { + uploadTokenDto.setResultCode(403); + return uploadTokenDto; + } + + try { + uploadTokenDto.setUserTokenIdx(Integer.parseInt(jwtTokenProvider.getUserId(uploadTokenDto.getUploadToken()))); + } catch (NumberFormatException e) { + uploadTokenDto.setResultCode(401); + return uploadTokenDto; + } + + int isOk = fileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(uploadTokenDto); + + if (isOk > 0) { + uploadTokenDto.setResultCode(200); + } else { + uploadTokenDto.setResultCode(403); + } + + return uploadTokenDto; + } +} diff --git a/src/main/java/com/alist/api/config/SecurityConfig.java b/src/main/java/com/alist/api/config/SecurityConfig.java index 1fda778..5b42419 100644 --- a/src/main/java/com/alist/api/config/SecurityConfig.java +++ b/src/main/java/com/alist/api/config/SecurityConfig.java @@ -12,7 +12,6 @@ import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; diff --git a/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java b/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java index 90ad555..81157dd 100644 --- a/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java +++ b/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java @@ -90,4 +90,28 @@ public class JwtTokenProvider { .getBody(); } + + /* 업로드 전용 토큰*/ + public String createUploadToken(long userTokenIdx) { + Instant now = Instant.now(); + Instant expiry = now.plusSeconds(300); // 5분 + + return Jwts.builder() + .setSubject(String.valueOf(userTokenIdx)) + .claim("scope", "UPLOAD") + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(expiry)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public boolean hasUploadScope(String token) { + try { + Claims claims = parseClaims(token); + return "UPLOAD".equals(claims.get("scope", String.class)); + } catch (Exception e) { + return false; + } + } + } diff --git a/src/main/java/com/alist/api/modules/auth/dto/TestLoginDto.java b/src/main/java/com/alist/api/modules/auth/dto/TestLoginDto.java index 0485b0f..a7f5347 100644 --- a/src/main/java/com/alist/api/modules/auth/dto/TestLoginDto.java +++ b/src/main/java/com/alist/api/modules/auth/dto/TestLoginDto.java @@ -22,11 +22,11 @@ public class TestLoginDto { @JsonIgnore private String userApiKey; @JsonIgnore - private int userTokenIdx; + private Integer userTokenIdx; @JsonIgnore private Instant expiresAt; @JsonIgnore - private int userIdx; + private Integer userIdx; @JsonIgnore private int resultCode; } diff --git a/src/main/java/com/alist/api/modules/auth/vo/TestLoginTokenVo.java b/src/main/java/com/alist/api/modules/auth/vo/TestLoginTokenVo.java index 2ba0f39..11799be 100644 --- a/src/main/java/com/alist/api/modules/auth/vo/TestLoginTokenVo.java +++ b/src/main/java/com/alist/api/modules/auth/vo/TestLoginTokenVo.java @@ -4,7 +4,7 @@ import lombok.Getter; @Getter public class TestLoginTokenVo { - private int userTokenIdx; + private Integer userTokenIdx; private String userApiKey; private String userRole; private String refreshToken; diff --git a/src/main/java/com/alist/api/modules/auth/vo/TestLoginVo.java b/src/main/java/com/alist/api/modules/auth/vo/TestLoginVo.java index 6d9a4e5..68821b5 100644 --- a/src/main/java/com/alist/api/modules/auth/vo/TestLoginVo.java +++ b/src/main/java/com/alist/api/modules/auth/vo/TestLoginVo.java @@ -4,7 +4,7 @@ import lombok.Getter; @Getter public class TestLoginVo { - private int userIdx; + private Integer userIdx; private String id; private String password; } diff --git a/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java b/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java index 2e89ec7..c4bb693 100644 --- a/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java +++ b/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java @@ -6,8 +6,8 @@ import lombok.Setter; @Setter @Getter public class TestUserTokenDto { - private int userTokenIdx; - private int userIdx; + private Integer userTokenIdx; + private Integer userIdx; private String userApiKey; private String userRole; private int resultCode; diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 0d524ef..b11ccb0 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -42,6 +42,11 @@ swagger: id: alist password: "1qaz2wsx!@" +file: + upload: + tus-endpoint: https://file-alist.pjt.kr/tus/files/ + public-base-url: https://file-alist.pjt.kr + springdoc: api-docs: enabled: true diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index 4617b6b..da59e09 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -44,6 +44,11 @@ swagger: id: ${SWAGGER_ID} password: ${SWAGGER_PASSWORD} +file: + upload: + tus-endpoint: https://file-alist.pjt.kr/tus/files/ + public-base-url: https://file-alist.pjt.kr + springdoc: api-docs: enabled: true diff --git a/src/main/resources/mapper/common/file/FileMapper.xml b/src/main/resources/mapper/common/file/FileMapper.xml new file mode 100644 index 0000000..171c8a6 --- /dev/null +++ b/src/main/resources/mapper/common/file/FileMapper.xml @@ -0,0 +1,23 @@ + + + + + + /*FileMapper.insertFileMasterUpload*/ + INSERT INTO ALISTLMS.FILE_MASTER ( + FILE_MASTER_KEY, USER_IDX, FILE_CATEGORY, FOLDER_PATH, STATUS, CREATE_MEMBER, UPDATE_MEMBER + ) values ( + #{fileMasterKey}, #{userIdx}, #{fileCategory}, #{folderPath}, #{status}, #{userIdx}, #{userIdx} + ) + + + + \ No newline at end of file