Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 공유보드 조회, 공유 보관함 사진 상세조회 , 공유 URL 생성 API #60

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b3341c7
feat: springboot security oauth2 google authorization 설정 추가, JWT/Cook…
gwanhyeon Apr 16, 2021
6516d03
refactor: 전체적인 리팩토링
kouz95 Apr 16, 2021
5b0b6db
Merge branch 'feature/6' of https://github.com/DDD-5/null-moodof-back…
gwanhyeon Apr 16, 2021
89fbe8c
Merge branch 'develop' of https://github.com/DDD-5/null-moodof-backen…
gwanhyeon Apr 24, 2021
4cc6d3d
Merge branch 'develop' of https://github.com/DDD-5/null-moodof-backen…
gwanhyeon Apr 29, 2021
091335f
Merge branch 'develop' of https://github.com/DDD-5/null-moodof-backen…
gwanhyeon May 1, 2021
4feadb5
Merge branch 'develop' of https://github.com/DDD-5/null-moodof-backen…
gwanhyeon May 6, 2021
1f25555
Merge branch 'develop' of https://github.com/DDD-5/null-moodof-backen…
gwanhyeon May 19, 2021
78fdaa8
feat: 상세 조회 태그 조건 추가 및 조회시 태그 없음 검색 추가
kouz95 May 22, 2021
72ce070
feat: BoardPhoto 순서 추가
kouz95 May 25, 2021
28f23de
feat: BoardPhoto 전체 조회 구현
kouz95 May 25, 2021
55d3be7
feat: storagePhoto 조회 사진 개수 추가
kouz95 May 25, 2021
2044485
Merge branch 'develop' of https://github.com/DDD-5/null-moodof-backen…
gwanhyeon May 27, 2021
f2862a6
fix: code conflict
gwanhyeon May 27, 2021
41344c8
feat: 공유 카테고리 URL생성, 공유 카테고리, 보드 리스트조회
gwanhyeon May 28, 2021
03a0c75
feat: 공유보드 조회, 공유 보관함 사진 상세조회 , 공유 URL 생성 API
gwanhyeon May 29, 2021
0495449
fix: 태그생성시 TagAttachment 생성시 TDD 공유 로직 수정
gwanhyeon May 29, 2021
d81572f
chore: profile dev ignore 추가
gwanhyeon May 29, 2021
ae231f1
fix: 불필요 코드 정리 및 삭제
gwanhyeon May 29, 2021
d45907a
fix: git ignore 불필요 코드 참조 삭제
gwanhyeon May 29, 2021
8f228f9
feat: 공유 URI AES256 -> SHA256 암호화 변경, 코드리뷰 사항 반영
gwanhyeon May 31, 2021
cad9ee6
fix: 공유 URI 응답 DTO 변경, 미사용 코드 제거
gwanhyeon May 31, 2021
9140ddf
Merge branch 'develop' into feature/48
gwanhyeon May 31, 2021
d85766b
refactor: 보드 공유 키 생성 저장 로직 수정
gwanhyeon Jun 13, 2021
7e9400b
refactor: 보드 공유키 조회 URI 변경
gwanhyeon Jun 13, 2021
dbf472f
Merge remote-tracking branch 'origin/feature/48' into feature/48
gwanhyeon Jun 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ out/
### VS Code ###
.vscode/

application-security.yml
application-security.yml
/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;

@RequiredArgsConstructor
@Configuration
Expand Down Expand Up @@ -49,6 +52,19 @@ public void configure(AuthenticationManagerBuilder authenticationManagerBuilder)
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
}

@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}


@Bean
public PasswordEncoder passwordEncoder() {
Expand Down Expand Up @@ -93,7 +109,8 @@ protected void configure(HttpSecurity http) throws Exception {
"/webjars/**",
"/v2/**",
"/swagger-resources/**",
"/api/public/**"
"/api/public/**",
"/api/public/**/**"
).permitAll()
.anyRequest()
.authenticated()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ private List<BoardDTO.BoardResponse> findBoards(Long storagePhotoId) {
board.userId,
board.name,
board.categoryId,
board.sharedKey,
board.createdDate,
board.lastModifiedDate
))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.ddd.moodof.adapter.infrastructure.security.encrypt;

import org.springframework.stereotype.Component;
import java.security.MessageDigest;

@Component
public class EncryptUtil {

public static String encryptSHA256(String msg){
try{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(msg.getBytes("UTF-8"));
StringBuffer hexString = new StringBuffer();

for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}

return hexString.toString();
} catch(Exception ex){
throw new RuntimeException(ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ public ResponseEntity<BoardDTO.BoardResponse> delete(@LoginUserId Long userId, @
boardService.delete(userId, id);
return ResponseEntity.noContent().build();
}

@GetMapping("/{id}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 GET /boards/{id} 가 공유키 조회로만 사용되고 있는데, 저 주소는 보드 상세 조회라고 생각할 수 있을것 같아요. 보드 정보가 있어야할 것 같고 (title 등), 해당 보드에 존재하는 BoardPhoto 리스트를 응답해야할 것 같은데 어떻게 생각하시나요?

Copy link
Collaborator Author

@gwanhyeon gwanhyeon Jun 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 말씀해주신부분은 사용자가 공유 URI를 누르면 생성된 sharedKey값을 가져와서 해당 sharedKey 데이터를 반환해주는 로직입니다. @GetMapping("/sharedKey/{id}") 로 변경하여 해당 sharedKey를 조회하는 로직은 어떨까요?


@GetMapping("/{sharedKey}") 이 부분이 (지금은 변경된 `@GetMapping /api/public/boards?sharedKey={sharedKey}) 보드에 대한 전체 포토리스트를 가져오는 부분입니다. 밑에 말씀해주신데로 기획 정책상 어떤정보를 보여줄 것인지에 따라 변경될 소지가 있어보입니다.

public ResponseEntity<BoardDTO.BoardSharedResponse> getSharedKey(@LoginUserId Long userId, @PathVariable Long id){
BoardDTO.BoardSharedResponse response = boardService.getSharedURI(userId, id);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.ddd.moodof.adapter.presentation;

import com.ddd.moodof.adapter.presentation.api.PublicBoardAPI;
import com.ddd.moodof.application.BoardPhotoService;
import com.ddd.moodof.application.StoragePhotoService;
import com.ddd.moodof.application.dto.BoardPhotoDTO;
import com.ddd.moodof.application.dto.StoragePhotoDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RequestMapping(PublicBoardController.API_PUBLIC_BOARDS)
@RestController
public class PublicBoardController implements PublicBoardAPI {

public static final String API_PUBLIC_BOARDS = "/api/public/boards";

private final StoragePhotoService storagePhotoService;

private final BoardPhotoService boardPhotoService;

@Override
@GetMapping
public ResponseEntity<List<BoardPhotoDTO.BoardPhotoResponse>> findAllByBoard(@RequestParam String sharedKey) {
List<BoardPhotoDTO.BoardPhotoResponse> responses = boardPhotoService.findAllBySharedKey(sharedKey);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BoardPhoto List 뿐만아니라, Board 정보도 필요하지 않을까요?
아직 공유 받은 사람이 페이지에 접속했을때 어떤 정보들이 필요한지 논의가 안되었기 때문에, 기획이 나오면 바뀌어야할 것 같아요.
예상하기론 보드 정보와, 보드 제작자 (User) 정보, 혹은 카테고리 정보까지 필요할지도 모르겠네요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다. 그부분은 아직 기획상 명확하지 않아서 일단 포토리스트들만 가져오는로직으로 개발을 진행하였습니다 :) 기획에 따라 보드 정보, 보드 포트정보, 카테고리 정보 가 필요해보입니다!

return ResponseEntity.ok(responses);
}

@Override
@GetMapping("/{sharedKey}/detail/{id}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 API는 어떤 상황에서 사용될 것이라 생각하신 건가요?
공유 보드에서 공유 받은 사람이 사진 상세 조회를 한 경우인가요?

Copy link
Collaborator Author

@gwanhyeon gwanhyeon Jun 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다. 공유보드에서 사진 상세조회를 한 경우입니다.

sharedKey를 같이 넘겨줄 필요는 없어보이기는 하는데.. 어떻게 생각하실까요? 진입시에만 sharedKey로 확인하고 그외 나머지 동작들은 sharedKey가 필요없다고 생각합니다.

위에서 말씀해주신것들을 들어보니 @GetMapping("/{id}") 가 적합해보이는데 어떻게 생각하시나요!

public ResponseEntity<StoragePhotoDTO.StoragePhotoDetailResponse> getSharedBoardDetail(
@PathVariable String sharedKey,
@PathVariable Long id,
@RequestParam(required = false, value = "tagIds") List<Long> tagIds){
StoragePhotoDTO.StoragePhotoDetailResponse response = storagePhotoService.findSharedBoardDetail(sharedKey, id, tagIds);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ddd.moodof.adapter.presentation;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SharedBoardId {
String value() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ public interface BoardAPI {
@ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token")
@DeleteMapping("/{id}")
ResponseEntity<BoardDTO.BoardResponse> delete(@ApiIgnore @LoginUserId Long userId, @PathVariable Long id);

@ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token")
@GetMapping("{id}")
ResponseEntity<BoardDTO.BoardSharedResponse> getSharedKey(@LoginUserId Long userId, @PathVariable Long id);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ddd.moodof.adapter.presentation.api;
import com.ddd.moodof.application.dto.BoardPhotoDTO;
import com.ddd.moodof.application.dto.StoragePhotoDTO;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

public interface PublicBoardAPI {
@GetMapping("/{sharedKey}")
ResponseEntity<List<BoardPhotoDTO.BoardPhotoResponse>> findAllByBoard(@PathVariable String sharedKey);

@GetMapping("/{sharedKey}/detail/{id}")
ResponseEntity<StoragePhotoDTO.StoragePhotoDetailResponse> getSharedBoardDetail(@PathVariable String sharedKey, @PathVariable Long id, @RequestParam(required = false, value = "tagIds") List<Long> tagIds);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.ddd.moodof.application.dto.BoardPhotoDTO;
import com.ddd.moodof.application.verifier.BoardPhotoVerifier;
import com.ddd.moodof.domain.model.board.Board;
import com.ddd.moodof.domain.model.board.BoardRepository;
import com.ddd.moodof.domain.model.board.photo.BoardPhoto;
import com.ddd.moodof.domain.model.board.photo.BoardPhotoRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -19,6 +21,7 @@ public class BoardPhotoService {

private final BoardPhotoRepository boardPhotoRepository;
private final BoardPhotoVerifier boardPhotoVerifier;
private final BoardRepository boardRepository;

public List<BoardPhotoDTO.BoardPhotoResponse> addPhotos(Long userId, BoardPhotoDTO.AddBoardPhoto request) {
// TODO: 2021/05/25 StoragePhoto 삭제 대응
Expand Down Expand Up @@ -61,4 +64,10 @@ public List<BoardPhotoDTO.BoardPhotoResponse> findAllByBoardId(Long boardId, Lon
List<BoardPhoto> boardPhotos = boardPhotoRepository.findAllByBoardIdAndUserId(boardId, userId);
return BoardPhotoDTO.BoardPhotoResponse.listFrom(boardPhotos);
}
public List<BoardPhotoDTO.BoardPhotoResponse> findAllBySharedKey(String sharedKey) {
Board board = boardRepository.findBySharedKey(sharedKey)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey =" + sharedKey));
List<BoardPhoto> boardPhotos = boardPhotoRepository.findAllByBoardIdAndUserId(board.getId(), board.getUserId());
return BoardPhotoDTO.BoardPhotoResponse.listFrom(boardPhotos);
}
}
24 changes: 20 additions & 4 deletions src/main/java/com/ddd/moodof/application/BoardService.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package com.ddd.moodof.application;

import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil;
import com.ddd.moodof.application.dto.BoardDTO;
import com.ddd.moodof.application.verifier.BoardVerifier;
import com.ddd.moodof.domain.model.board.Board;
import com.ddd.moodof.domain.model.board.BoardRepository;
import com.ddd.moodof.domain.model.board.BoardSequenceUpdater;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import org.springframework.stereotype.Service;
import javax.transaction.Transactional;

@RequiredArgsConstructor
@Service
public class BoardService {

public static final int MAX_BOARD_IN_CATEGORY_COUNT = 10;

private final BoardRepository boardRepository;

private final BoardVerifier boardVerifier;

private final BoardSequenceUpdater boardSequenceUpdater;

@Transactional
Expand All @@ -27,13 +31,17 @@ public BoardDTO.BoardResponse create(Long userId, BoardDTO.CreateBoard request)

Board board = boardVerifier.toEntity(request.getPreviousBoardId(), request.getCategoryId(), request.getName(), userId);
Board saved = boardRepository.save(board);

encryptByBoardId(userId, saved);
boardRepository.findByUserIdAndPreviousBoardIdAndIdNot(userId, request.getPreviousBoardId(), saved.getId())
.ifPresent(it -> it.changePreviousBoardId(saved.getId(), userId));

return BoardDTO.BoardResponse.from(saved);
}

public void encryptByBoardId(Long userId, Board saved) {
String sharedKey = EncryptUtil.encryptSHA256(Long.toString(saved.getId()));
saved.updateSharedkey(sharedKey, userId);
}

public BoardDTO.BoardResponse changeName(Long userId, Long id, BoardDTO.ChangeBoardName request) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 보드입니다. id = " + id));
Expand All @@ -58,4 +66,12 @@ public void delete(Long userId, Long id) {
}
boardRepository.deleteById(id);
}

public BoardDTO.BoardSharedResponse getSharedURI(Long userId, Long id) {
Board board = boardRepository.findByIdAndUserId(id, userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Board, id = " + id));
String sharedKey = board.getSharedKey();
return BoardDTO.BoardSharedResponse.from(id, sharedKey);
}

}
16 changes: 16 additions & 0 deletions src/main/java/com/ddd/moodof/application/StoragePhotoService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.ddd.moodof.adapter.infrastructure.persistence.PaginationUtils;
import com.ddd.moodof.application.dto.StoragePhotoDTO;
import com.ddd.moodof.domain.model.board.Board;
import com.ddd.moodof.domain.model.board.BoardRepository;
import com.ddd.moodof.domain.model.storage.photo.StoragePhoto;
import com.ddd.moodof.domain.model.storage.photo.StoragePhotoCreator;
import com.ddd.moodof.domain.model.storage.photo.StoragePhotoQueryRepository;
Expand All @@ -11,6 +13,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor
Expand All @@ -20,6 +23,7 @@ public class StoragePhotoService {
private final StoragePhotoQueryRepository storagePhotoQueryRepository;
private final StoragePhotoCreator storagePhotoCreator;
private final PaginationUtils paginationUtils;
private final BoardRepository boardRepository;

public StoragePhotoDTO.StoragePhotoResponse create(StoragePhotoDTO.CreateStoragePhoto request, Long userId) {
StoragePhoto saved = storagePhotoCreator.create(request.getUri(), request.getRepresentativeColor(), userId);
Expand All @@ -30,6 +34,12 @@ public StoragePhotoDTO.StoragePhotoPageResponse findPage(Long userId, int page,
return storagePhotoQueryRepository.findPageExcludeTrash(userId, PageRequest.of(page, size, paginationUtils.getSort(sortBy, descending)), tagIds);
}

public StoragePhotoDTO.StoragePhotoPageResponse findSharedPage(String sharedKey, int page, int size, String sortBy, boolean descending, List<Long> tagIds) {
Board board = boardRepository.findBySharedKey(sharedKey)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey));
return storagePhotoQueryRepository.findPageExcludeTrash(board.getUserId(), PageRequest.of(page, size, paginationUtils.getSort(sortBy, descending)), tagIds);
}

public boolean existsByIdAndUserId(Long id, Long userId) {
return storagePhotoRepository.existsByIdAndUserId(id, userId);
}
Expand All @@ -45,4 +55,10 @@ public void delete(Long userId, StoragePhotoDTO.DeleteStoragePhotos request) {
public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id, List<Long> tagIds) {
return storagePhotoQueryRepository.findDetail(userId, id, tagIds);
}

public StoragePhotoDTO.StoragePhotoDetailResponse findSharedBoardDetail(String sharedKey, Long id, List<Long> tagIds) {
Board board = boardRepository.findBySharedKey(sharedKey)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey));
return storagePhotoQueryRepository.findDetail(board.getUserId(), id, tagIds);
}
}
31 changes: 29 additions & 2 deletions src/main/java/com/ddd/moodof/application/dto/BoardDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ public static class CreateBoard {
private String name;

public Board toEntity(Long userId) {
return new Board(null, previousBoardId, userId, name, categoryId, null, null);
return setSharedKey(new Board(null, previousBoardId, userId, name, categoryId, "",null, null));
}
public Board setSharedKey(Board board){
return new Board(board.getId(),board.getPreviousBoardId(),board.getUserId(), board.getName(),board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate());
}

}

@NoArgsConstructor
Expand All @@ -35,11 +39,12 @@ public static class BoardResponse {
private Long userId;
private String name;
private Long categoryId;
private String sharedKey;
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;

public static BoardResponse from(Board board) {
return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getCreatedDate(), board.getLastModifiedDate());
return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate());
}
}

Expand All @@ -59,4 +64,26 @@ public static class ChangeBoardSequence {
private Long categoryId;
private Long previousBoardId;
}

@NoArgsConstructor
@AllArgsConstructor
@Getter
public static class BoardSharedRequest{
private Long id;
}

@NoArgsConstructor
@Getter
@AllArgsConstructor
public static class BoardSharedResponse {
private Long id;

private String sharedKey;

public static BoardDTO.BoardSharedResponse from(Long id, String sharedKey) {
return new BoardDTO.BoardSharedResponse(id, sharedKey);
}
}


}
3 changes: 3 additions & 0 deletions src/main/java/com/ddd/moodof/application/dto/TagDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public Tag toEntity(Long id, Long userId, String name) {
}
}


@NoArgsConstructor
@AllArgsConstructor
@Getter
Expand Down Expand Up @@ -74,6 +75,8 @@ public static List<TagResponse> listFrom(List<Tag> tagList) {
.lastModifiedDate(tag.getLastModifiedDate())
.build()).collect(Collectors.toList());
}


}

@NoArgsConstructor
Expand Down
Loading