diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..611e7c8 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4819c40 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..4d9f242 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/android-lesson-07 b/android-lesson-07 new file mode 160000 index 0000000..fddad1b --- /dev/null +++ b/android-lesson-07 @@ -0,0 +1 @@ +Subproject commit fddad1bac1a64241484e6538bbb17062ccdbcf2f diff --git a/build.gradle b/build.gradle index 8a10cce..b36129e 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,18 @@ dependencies { implementation('org.springframework.boot:spring-boot-starter-thymeleaf') + + // ================================================== + // AWS SDK + // ================================================== + + // AWS SDK를 사용하기 전, 전체적인 버전 관리를 위해 BOM(Build Of Material)을 추가합니다. + // 해당 프로젝트는 AWS SDK 1.12.529 버전의 BOM을 사용하기에, 다른 AWS 의존성 또한 해당 버전으로 고정됩니다. + implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.529') + // AWS S3을 사용하기 위해 S3 의존성을 추가합니다. + implementation('com.amazonaws:aws-java-sdk-s3') + + // ================================================== // JWT // ================================================== diff --git a/src/main/java/kr/easw/lesson07/Constants.java b/src/main/java/kr/easw/lesson07/Constants.java new file mode 100644 index 0000000..0581cd2 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/Constants.java @@ -0,0 +1,7 @@ +package kr.easw.lesson07; + +public class Constants { + public static final String AUTHORITY_GUEST = "GUEST"; + public static final String AUTHORITY_ADMIN = "ADMIN"; + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/Lesson07Example.java b/src/main/java/kr/easw/lesson07/Lesson07Example.java index b881584..f4ede68 100644 --- a/src/main/java/kr/easw/lesson07/Lesson07Example.java +++ b/src/main/java/kr/easw/lesson07/Lesson07Example.java @@ -1,5 +1,6 @@ package kr.easw.lesson07; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; diff --git a/src/main/java/kr/easw/lesson07/auth/JpaUserDetailsService.java b/src/main/java/kr/easw/lesson07/auth/JpaUserDetailsService.java new file mode 100644 index 0000000..d3dc210 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/auth/JpaUserDetailsService.java @@ -0,0 +1,32 @@ +package kr.easw.lesson07.auth; + +import kr.easw.lesson07.Constants; +import kr.easw.lesson07.model.dto.UserDataEntity; +import kr.easw.lesson07.model.repository.UserDataRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class JpaUserDetailsService implements UserDetailsService { + private final UserDataRepository userDataRepository; + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserDataEntity user = userDataRepository.findUserDataEntityByUserId(username.toLowerCase()).orElseThrow( + () -> new UsernameNotFoundException("User not found") + ); + return new User(user.getUserId(), user.getPassword(), List.of( + user.isAdmin() ? new SimpleGrantedAuthority(Constants.AUTHORITY_ADMIN) : new SimpleGrantedAuthority(Constants.AUTHORITY_GUEST) + )); + } +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/auth/JwtFilterChain.java b/src/main/java/kr/easw/lesson07/auth/JwtFilterChain.java new file mode 100644 index 0000000..f0c2fac --- /dev/null +++ b/src/main/java/kr/easw/lesson07/auth/JwtFilterChain.java @@ -0,0 +1,65 @@ +package kr.easw.lesson07.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kr.easw.lesson07.service.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +@Component +public class JwtFilterChain extends OncePerRequestFilter { + private final JwtService jwtService; + + private final JpaUserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 만약 Authorization 헤더가 없다면, 필터 체인을 계속 진행합니다. + if (request.getHeader("Authorization") == null) { + filterChain.doFilter(request, response); + return; + } + System.out.println("Jwt filter chain"); + String token = request.getHeader("Authorization"); + System.out.println("Token: " + token); + // 토큰을 검증합니다. + switch (jwtService.validate(token)) { + case VALID: + // 토큰이 유효하다면, 토큰에서 유저 이름을 추출합니다. + String userName = jwtService.extractUsername(token); + // 유저 이름을 통해 유저 정보를 가져옵니다. + UserDetails details = userDetailsService.loadUserByUsername(userName); + System.out.println(details.getAuthorities()); + // 유저 정보를 통해 인증 객체를 생성합니다. + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken( + details, + details.getPassword(), + details.getAuthorities() + )); + System.out.println("Token validated / Role: " + details.getAuthorities()); + // 필터 체인을 계속 진행합니다. + filterChain.doFilter(request, response); + return; + case EXPIRED: + response.sendError(401, "Expired token"); + break; + case UNSUPPORTED: + response.sendError(401, "Unsupported token"); + break; + case INVALID: + response.sendError(401, "Invalid token"); + break; + } + System.out.println("Token invalid"); + System.out.println(jwtService.validate(token)); + } +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/configurations/SpringSecurityConfiguration.java b/src/main/java/kr/easw/lesson07/configurations/SpringSecurityConfiguration.java new file mode 100644 index 0000000..4d97e86 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/configurations/SpringSecurityConfiguration.java @@ -0,0 +1,89 @@ +package kr.easw.lesson07.configurations; + +import kr.easw.lesson07.Constants; +import kr.easw.lesson07.auth.JwtFilterChain; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +// 스프링 웹 시큐리티를 활성화합니다. +@EnableWebSecurity +// Configuration 어노테이션을 사용하여 해당 클래스가 스프링 설정 클래스임을 선언합니다. +@Configuration +@AllArgsConstructor +public class SpringSecurityConfiguration { + // 어플리케이션에 사용할 비밀번호 인코더를 미리 생성합니다. + private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + private final JwtFilterChain filterChain; + + // @Bean 어노테이션을 사용하여 해당 메서드가 스프링 빈임을 선언합니다. + @Bean + // @SneakyThrows를 사용하여 예외를 무시합니다. + // 이는 lombok의 API입니다. + @SneakyThrows + // HttpSecurity를 파라미터로 받아 SecurityFilterChain을 반환하는 메서드입니다. + SecurityFilterChain configureHttpSecurity(HttpSecurity security) { + security + // csrf 보호를 비활성화합니다. + // 활성화되었을 경우, 페이지 내의 API 호출이 실패할 수 있습니다. + .csrf(csrf -> csrf.disable()) + .cors(AbstractHttpConfigurer::disable) + // 모든 요청에 대해 인증을 넘깁니다. + .authorizeHttpRequests(registry -> { + // /dashboard 엔드포인트에 대해, 관리자와 게스트 권한을 가진 사용자만 접근할 수 있도록 설정합니다. + registry.requestMatchers("/dashboard").hasAnyAuthority(Constants.AUTHORITY_ADMIN, Constants.AUTHORITY_GUEST) + // /admin와 /management 엔드포인트에 대해, 관리자 권한을 가진 사용자만 접근할 수 있도록 설정합니다. + .requestMatchers("/admin", "/management").hasAnyAuthority(Constants.AUTHORITY_ADMIN) + // /api/v1/data/admin 엔드포인트에 대해, 관리자 권한을 가진 사용자만 접근할 수 있도록 설정합니다. + .requestMatchers("/api/v1/data/admin/**").hasAnyAuthority(Constants.AUTHORITY_ADMIN) + // /api/v1/data 엔드포인트에 대해, 관리자와 게스트 권한을 가진 사용자만 접근할 수 있도록 설정합니다. + .requestMatchers("/api/v1/data/**").hasAnyAuthority(Constants.AUTHORITY_ADMIN, Constants.AUTHORITY_GUEST) + // /api/v1/data 엔드포인트에 대해, 관리자 권한을 가진 사용자만 접근할 수 있도록 설정합니다. + .requestMatchers("/api/v1/user/**").hasAnyAuthority(Constants.AUTHORITY_ADMIN) + // /api/v1/auth 엔드포인트에 대해, 모든 사용자가 접근할 수 있도록 설정합니다. + .requestMatchers("/api/v1/auth/**").permitAll() + // 다른 모든 링크는 허용합니다. + .anyRequest().permitAll() + ; + }) + // 로그아웃 엔드포인트를 설정합니다. + .logout(customizer -> { + customizer.logoutUrl("/logout"); + customizer.logoutSuccessUrl("/?logout=true"); + }) + // 로그인 엔드포인트를 설정합니다. + .formLogin(customizer -> { + customizer + // 로그인 페이지를 /login으로 설정합니다. + .loginPage("/login") + // 로그인 페이지에 대해 모든 사용자가 접근할 수 있도록 설정합니다. + .permitAll() + // 로그인 성공시 리다이렉트할 페이지를 설정합니다. + .defaultSuccessUrl("/dashboard") + // 로그인 실패시 리다이렉트할 페이지를 설정합니다. + .failureUrl("/login?error=true"); + }) + // JWT 필터를 추가합니다. + // JWT 필터는 로그인 전 수행됩니다. + .addFilterBefore(filterChain, UsernamePasswordAuthenticationFilter.class) + ; + return security.build(); + } + + + // @Bean 어노테이션을 사용하여 해당 메서드가 스프링 빈임을 선언합니다. + @Bean + public BCryptPasswordEncoder encoder() { + // 사용되는 비밀번호 인코더를 BCrypt로 설정합니다. + return encoder; + } + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/controller/AWSConroller.java b/src/main/java/kr/easw/lesson07/controller/AWSConroller.java new file mode 100644 index 0000000..2cb0a06 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/controller/AWSConroller.java @@ -0,0 +1,90 @@ +package kr.easw.lesson07.controller; + +import kr.easw.lesson07.model.dto.AWSKeyDto; +import kr.easw.lesson07.service.AWSService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/rest/aws") +public class AWSConroller { + private final AWSService awsController; + + @PostMapping("/auth") + private ModelAndView onAuth(AWSKeyDto awsKey) { + try { + awsController.initAWSAPI(awsKey); + return new ModelAndView("redirect:/upload"); // 업로드 페이지로 리디렉션 + } catch (Exception ex) { + ex.printStackTrace(); + return new ModelAndView("redirect:/server-error?errorStatus=" + ex.getMessage()); + } + } + + @GetMapping("/list") + private List onFileList() { + return awsController.getFileList(); + } + + @PostMapping("/upload") + private ModelAndView onUpload(@RequestParam MultipartFile file) { + try { + awsController.upload(file); + return new ModelAndView("redirect:/upload?success=true"); // 업로드 페이지로 리디렉션 + } catch (Exception ex) { + ex.printStackTrace(); + return new ModelAndView("redirect:/server-error?errorStatus=" + ex.getMessage()); + } + } + + + // @GetMapping("/download") +// private ModelAndView onDownload(@RequestParam("filename") String fileName) { +// try { +// log.info("filename={}", fileName); +// awsController.downloadFile(fileName); +// throw new IllegalStateException("기능이 구현되지 않았습니다."); +// } catch (Throwable e) { +// return new ModelAndView("redirect:/server-error?errorStatus=" + e.getMessage()); +// } +// } + @GetMapping("/download") + public ResponseEntity onDownload(@RequestParam("filename") String fileName) { + try { + File file = awsController.downloadFile(fileName); + + if (file == null || !file.exists()) { + throw new IllegalArgumentException("다운로드할 파일이 존재하지 않습니다."); + } + + // 파일을 byte 배열로 읽어옵니다. + byte[] fileBytes = Files.readAllBytes(file.toPath()); + + // 파일의 MIME 타입을 설정합니다. + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDispositionFormData("attachment", fileName); // 파일 다운로드 헤더 설정 + + // ResponseEntity를 사용하여 다운로드할 파일과 헤더 정보를 반환합니다. + return ResponseEntity.ok() + .headers(headers) + .body(fileBytes); + } catch (IOException e) { + e.printStackTrace(); + return ResponseEntity.status(500).body("오류"); + } + } +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/controller/AdminDataRestEndpoint.java b/src/main/java/kr/easw/lesson07/controller/AdminDataRestEndpoint.java new file mode 100644 index 0000000..a59de29 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/controller/AdminDataRestEndpoint.java @@ -0,0 +1,28 @@ +package kr.easw.lesson07.controller; + +import kr.easw.lesson07.model.dto.TextDataDto; +import kr.easw.lesson07.service.TextDataService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +// @RestController 어노테이션을 사용하여 이 클래스가 REST 컨트롤러임을 선언합니다. +@RestController +// @RequestMapping 어노테이션을 사용하여 이 클래스의 기반 엔드포인트를 /api/v1/data/admin으로 설정합니다. +@RequestMapping("/api/v1/data/admin") +// final로 지정된 모든 필드를 파라미터로 가지는 생성자를 생성합니다. +@RequiredArgsConstructor +public class AdminDataRestEndpoint { + private final TextDataService textDataService; + + // 이 메서드의 엔드포인트를 /api/v1/data/add로 설정합니다. POST만 허용됩니다. + @PostMapping("/add") + public ModelAndView addText(@RequestParam("text") String text) { + textDataService.addText(new TextDataDto(0L, text)); + return new ModelAndView("redirect:/admin?success=true"); + } + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/controller/BaseWebController.java b/src/main/java/kr/easw/lesson07/controller/BaseWebController.java new file mode 100644 index 0000000..b4e3d6e --- /dev/null +++ b/src/main/java/kr/easw/lesson07/controller/BaseWebController.java @@ -0,0 +1,86 @@ +package kr.easw.lesson07.controller; + +import kr.easw.lesson07.service.AWSService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.ModelAndView; + +// 이 클래스를 컨트롤러로 선언합니다. +@Controller +@RequiredArgsConstructor +public class BaseWebController { + private final AWSService awsController; + + @RequestMapping("/dashboard") + public ModelAndView onUserDashboard() { + return new ModelAndView("user_dashboard.html"); + } + + @RequestMapping("/login") + public ModelAndView onLogin() { + return new ModelAndView("login.html"); + } + + + @RequestMapping("/register") + public ModelAndView onRegister() { + return new ModelAndView("register.html"); + } + + @RequestMapping("/admin") + public ModelAndView onAdminDashboard() { + return new ModelAndView("admin_dashboard.html"); + } + + @RequestMapping("/register_success") + public ModelAndView onRegisterSuccess() { + return new ModelAndView("register_check.html"); // 파일명 오타 수정 + } + + @RequestMapping("/management") + public ModelAndView onManagementDashboard() { + return new ModelAndView("management.html"); + } + + // 이 메서드의 엔드포인트를 /server-error로 설정합니다. + @RequestMapping("/server-error") + public ModelAndView onErrorTest() { + // 에러 페이지로 리다이렉트합니다. + return new ModelAndView("error.html"); + } + + @RequestMapping("/index") + public String index() { + return "index"; + } + + @RequestMapping("/another-error") + public ModelAndView onAnotherError() { + return new ModelAndView("error.html"); + } + + + @RequestMapping("/upload") + public ModelAndView onUpload() { + if (awsController.isInitialized()) { + return new ModelAndView("upload.html"); + } + return new ModelAndView("request_aws_key.html"); + } + + @RequestMapping("/download") + public ModelAndView onDownload() { + if (awsController.isInitialized()) { + return new ModelAndView("download.html"); + } + return new ModelAndView("request_aws_key.html"); + } + + @RequestMapping("/") + public ModelAndView serviceSelection() { + return new ModelAndView("service_selection"); + } +} + diff --git a/src/main/java/kr/easw/lesson07/controller/DataRestEndpoint.java b/src/main/java/kr/easw/lesson07/controller/DataRestEndpoint.java new file mode 100644 index 0000000..8c119ee --- /dev/null +++ b/src/main/java/kr/easw/lesson07/controller/DataRestEndpoint.java @@ -0,0 +1,26 @@ +package kr.easw.lesson07.controller; + +import kr.easw.lesson07.model.dto.TextDataDto; +import kr.easw.lesson07.service.TextDataService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +// @RestController 어노테이션을 사용하여 이 클래스가 REST 컨트롤러임을 선언합니다. +@RestController +// @RequestMapping 어노테이션을 사용하여 이 클래스의 기반 엔드포인트를 /api/v1/data로 설정합니다. +@RequestMapping("/api/v1/data") +// final로 지정된 모든 필드를 파라미터로 가지는 생성자를 생성합니다. +@RequiredArgsConstructor +public class DataRestEndpoint { + private final TextDataService textDataService; + + // 이 메서드의 엔드포인트를 /api/v1/data/list로 설정합니다. GET만 허용됩니다. + @GetMapping("/list") + public List listText() { + return textDataService.listText(); + } +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/controller/UserAuthEndpoint.java b/src/main/java/kr/easw/lesson07/controller/UserAuthEndpoint.java new file mode 100644 index 0000000..91319cd --- /dev/null +++ b/src/main/java/kr/easw/lesson07/controller/UserAuthEndpoint.java @@ -0,0 +1,53 @@ +package kr.easw.lesson07.controller; + +import kr.easw.lesson07.model.dto.ExceptionalResultDto; +import kr.easw.lesson07.model.dto.UserDataEntity; +import kr.easw.lesson07.service.UserDataService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class UserAuthEndpoint { + private final UserDataService userDataService; + + + // JWT 인증을 위해 사용되는 엔드포인트입니다. + @PostMapping("/login") + public ResponseEntity login(@RequestBody UserDataEntity entity) { + try { + // 로그인을 시도합니다. + return ResponseEntity.ok(userDataService.createTokenWith(entity)); + } catch (Exception ex) { + // 만약 로그인에 실패했다면, 400 Bad Request를 반환합니다. + return ResponseEntity.badRequest().body(new ExceptionalResultDto(ex.getMessage())); + } + } + + @PostMapping("/register") + public void register(@RequestBody UserDataEntity entity) { + // 사용자가 이미 존재하는지 확인합니다. + if (userDataService.isUserExists(entity.getUserId())) { + // 이미 존재하는 경우 예외를 던집니다. + throw new ResponseStatusException(HttpStatus.CONFLICT, "User already exists"); + } + + // BCryptPasswordEncoder를 사용하여 비밀번호를 암호화합니다. + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String encodedPassword = encoder.encode(entity.getPassword()); + + // 새 사용자를 생성합니다. + userDataService.createUser(new UserDataEntity(0, entity.getUserId(), encodedPassword, false)); + // 성공적으로 생성되면 메서드는 정상적으로 완료됩니다. 반환 값은 없습니다. + } + + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/controller/UserDataEndpoint.java b/src/main/java/kr/easw/lesson07/controller/UserDataEndpoint.java new file mode 100644 index 0000000..7ca612b --- /dev/null +++ b/src/main/java/kr/easw/lesson07/controller/UserDataEndpoint.java @@ -0,0 +1,37 @@ +package kr.easw.lesson07.controller; + +import kr.easw.lesson07.model.dto.RemoveUserDto; +import kr.easw.lesson07.model.dto.UserDataEntity; +import kr.easw.lesson07.service.UserDataService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/user") +@RequiredArgsConstructor +public class UserDataEndpoint { + private final UserDataService userDataService; + + @GetMapping("/list") + public List listUsers() { + // Fetch all users and convert them to a list of user IDs. + return userDataService.getAllUsers().stream() + .map(UserDataEntity::getUserId) + .collect(Collectors.toList()); + } + + @PostMapping("/remove") + public ResponseEntity removeUser(@RequestBody RemoveUserDto removeUserDto) { + boolean isRemoved = userDataService.removeUser(removeUserDto.getUserId()); + if (isRemoved) { + return ResponseEntity.ok("User removed successfully"); + } else { + return ResponseEntity.badRequest().body("Failed to remove user"); + } + } + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/AWSKeyDto.java b/src/main/java/kr/easw/lesson07/model/dto/AWSKeyDto.java new file mode 100644 index 0000000..875cf22 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/AWSKeyDto.java @@ -0,0 +1,13 @@ +package kr.easw.lesson07.model.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AWSKeyDto { + @Getter + private final String apiKey; + + @Getter + private final String apiSecretKey; +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/DownloadFileDto.java b/src/main/java/kr/easw/lesson07/model/dto/DownloadFileDto.java new file mode 100644 index 0000000..57e06d9 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/DownloadFileDto.java @@ -0,0 +1,8 @@ +package kr.easw.lesson07.model.dto; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class DownloadFileDto { + private final String fileName; +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/ExceptionalResultDto.java b/src/main/java/kr/easw/lesson07/model/dto/ExceptionalResultDto.java new file mode 100644 index 0000000..4762a75 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/ExceptionalResultDto.java @@ -0,0 +1,3 @@ +package kr.easw.lesson07.model.dto; + +public record ExceptionalResultDto(String message) { } \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/RemoveUserDto.java b/src/main/java/kr/easw/lesson07/model/dto/RemoveUserDto.java new file mode 100644 index 0000000..02aed43 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/RemoveUserDto.java @@ -0,0 +1,17 @@ + +package kr.easw.lesson07.model.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class RemoveUserDto { + private String userId; + + public RemoveUserDto(String userId) { + this.userId = userId; + } +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/TextDataDto.java b/src/main/java/kr/easw/lesson07/model/dto/TextDataDto.java new file mode 100644 index 0000000..58e4871 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/TextDataDto.java @@ -0,0 +1,32 @@ +package kr.easw.lesson07.model.dto; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +// 이 클래스를 엔티티로 선언합니다. +// 엔티티로 선언된 클래스는 DB의 테이블과 매핑됩니다. +@Entity +// 모든 필드를 인자로 받는 생성자를 자동으로 생성합니다. +@AllArgsConstructor +// 인자가 없는 생성자를 자동으로 생성합니다. +// 엔티티 클래스는 반드시 인자가 없는 생성자가 있어야 합니다. +@NoArgsConstructor +// 모든 필드에 대한 Getter를 자동으로 생성합니다. +@Getter +public class TextDataDto { + // id 필드를 기본키로 지정합니다. + @Id + // @GeneratedValue 어노테이션을 통해 이 값을 자동 증가(auto-increment)로 지정합니다. + @GeneratedValue + private long id; + + // @Column 어노테이션으로 이 필드가 DB의 어떤 컬럼과 매핑되는지 지정합니다. + // nullable = false로 지정하면 이 필드는 null이 될 수 없습니다. + @Column(nullable = false) + private String text; +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/UploadFileDto.java b/src/main/java/kr/easw/lesson07/model/dto/UploadFileDto.java new file mode 100644 index 0000000..7a3568b --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/UploadFileDto.java @@ -0,0 +1,11 @@ +package kr.easw.lesson07.model.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +public class UploadFileDto { + @Getter + private final MultipartFile file; +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/UserAuthenticationDto.java b/src/main/java/kr/easw/lesson07/model/dto/UserAuthenticationDto.java new file mode 100644 index 0000000..ef85e79 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/UserAuthenticationDto.java @@ -0,0 +1,10 @@ +package kr.easw.lesson07.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class UserAuthenticationDto { + private final String token; +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/dto/UserDataEntity.java b/src/main/java/kr/easw/lesson07/model/dto/UserDataEntity.java new file mode 100644 index 0000000..d35c6f8 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/dto/UserDataEntity.java @@ -0,0 +1,27 @@ +package kr.easw.lesson07.model.dto; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +public class UserDataEntity { + @Id + @GeneratedValue + private long id; + + @Getter + private String userId; + + @Getter + private String password; + + @Getter + private boolean isAdmin; + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/repository/TextDataRepository.java b/src/main/java/kr/easw/lesson07/model/repository/TextDataRepository.java new file mode 100644 index 0000000..27eb6f8 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/repository/TextDataRepository.java @@ -0,0 +1,12 @@ +package kr.easw.lesson07.model.repository; + +import kr.easw.lesson07.model.dto.TextDataDto; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +// @Repository 어노테이션을 통해 이 인터페이스가 레포지토리임을 선언합니다. +// 레포지토리는 SQL에서의 테이블과 매핑됩니다. +@Repository +public interface TextDataRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/model/repository/UserDataRepository.java b/src/main/java/kr/easw/lesson07/model/repository/UserDataRepository.java new file mode 100644 index 0000000..7c1bb1c --- /dev/null +++ b/src/main/java/kr/easw/lesson07/model/repository/UserDataRepository.java @@ -0,0 +1,14 @@ + +package kr.easw.lesson07.model.repository; + +import kr.easw.lesson07.model.dto.UserDataEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserDataRepository extends JpaRepository { + Optional findUserDataEntityByUserId(String userId); + Optional findByUserId(String userId); +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/service/AWSService.java b/src/main/java/kr/easw/lesson07/service/AWSService.java new file mode 100644 index 0000000..610d789 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/service/AWSService.java @@ -0,0 +1,76 @@ +package kr.easw.lesson07.service; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.*; +import kr.easw.lesson07.model.dto.AWSKeyDto; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.util.List; +import java.util.UUID; + +@Service +public class AWSService { + private static final String BUCKET_NAME = "easw-random-bucket-" + UUID.randomUUID(); + private AmazonS3 s3Client = null; + + public void initAWSAPI(AWSKeyDto awsKey) { + s3Client = AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(awsKey.getApiKey(), awsKey.getApiSecretKey()))) + .withRegion(Regions.AP_NORTHEAST_2) + .build(); + for (Bucket bucket : s3Client.listBuckets()) { + if (bucket.getName().startsWith("easw-random-bucket-")) { + s3Client.listObjects(bucket.getName()) + .getObjectSummaries() + .forEach(it -> s3Client.deleteObject(bucket.getName(), it.getKey())); + } + } + s3Client.createBucket(BUCKET_NAME); + } + + public boolean isInitialized() { + return s3Client != null; + } + + public List getFileList() { + return s3Client.listObjects(BUCKET_NAME).getObjectSummaries().stream().map(S3ObjectSummary::getKey).toList(); + } + + @SneakyThrows + public void upload(MultipartFile file) { + s3Client.putObject(BUCKET_NAME, file.getOriginalFilename(), new ByteArrayInputStream(file.getResource().getContentAsByteArray()), new ObjectMetadata()); + } + @SneakyThrows + public File downloadFile(String fileName) { + if (!isInitialized()) { + throw new IllegalStateException("AWS S3 클라이언트가 초기화되지 않았습니다."); + } + // S3에서 파일 다운로드 + S3Object s3Object = s3Client.getObject(BUCKET_NAME, fileName); + + if (s3Object == null) { + throw new IllegalArgumentException("다운로드할 파일이 존재하지 않습니다."); + } + + try (S3ObjectInputStream inputStream = s3Object.getObjectContent()) { + File file = File.createTempFile("downloaded-", "-" + fileName); + try (OutputStream outputStream = new FileOutputStream(file)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + return file; + } catch (IOException e) { + throw new RuntimeException("파일 다운로드 중 오류가 발생했습니다.", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/service/TextDataService.java b/src/main/java/kr/easw/lesson07/service/TextDataService.java new file mode 100644 index 0000000..32053c9 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/service/TextDataService.java @@ -0,0 +1,32 @@ +package kr.easw.lesson07.service; + +import kr.easw.lesson07.model.dto.TextDataDto; +import kr.easw.lesson07.model.repository.TextDataRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +// @Service 어노테이션을 통해 이 클래스가 서비스임을 선언합니다. +@Service +// @RequiredArgsConstructor 어노테이션을 통해 final로 선언된 필드를 초기화하는 생성자를 만듭니다. +@RequiredArgsConstructor +public class TextDataService { + + // @RequiredArgsConstructor 어노테이션을 통해 초기화된 필드는 final로 선언되어 있어서 setter를 만들지 않아도 됩니다. + // 이 필드는 생성자에서 초기화됩니다. + private final TextDataRepository userDataRepository; + + // 이 메소드는 테이블에 데이터를 추가합니다. + public void addText(TextDataDto dto) { + System.out.println("Adding text"); + userDataRepository.saveAndFlush(dto); + } + + // 이 메소드는 테이블의 모든 데이터를 가져옵니다. + public List listText() { + return userDataRepository.findAll(); + } + + +} \ No newline at end of file diff --git a/src/main/java/kr/easw/lesson07/service/UserDataService.java b/src/main/java/kr/easw/lesson07/service/UserDataService.java new file mode 100644 index 0000000..716fb66 --- /dev/null +++ b/src/main/java/kr/easw/lesson07/service/UserDataService.java @@ -0,0 +1,74 @@ +package kr.easw.lesson07.service; + +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import kr.easw.lesson07.model.dto.UserAuthenticationDto; +import kr.easw.lesson07.model.dto.UserDataEntity; +import kr.easw.lesson07.model.repository.UserDataRepository; +import lombok.AllArgsConstructor; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@AllArgsConstructor +public class UserDataService { + private final UserDataRepository repository; + + private final BCryptPasswordEncoder encoder; + + private final JwtService jwtService; + + // @PostConstruct 어노테이션을 사용하여 이 메서드가 빈 생성 후에 실행되도록 합니다. + @PostConstruct + public void init() { + // 이 메서드는 애플리케이션이 시작될 때 실행됩니다. + System.out.println("Creating admin user"); + // 만약 admin이라는 아이디를 가진 유저가 없다면, admin이라는 아이디를 가진 유저를 생성합니다. + createUser(new UserDataEntity(0L, "admin", encoder.encode("admin"), true)); + createUser(new UserDataEntity(0L, "guest", encoder.encode("guest"), false)); + } + + // 이 메서드는 유저가 존재하는지 확인합니다. + public boolean isUserExists(String userId) { + return repository.findUserDataEntityByUserId(userId).isPresent(); + } + + // 이 메서드는 유저를 생성합니다. + public void createUser(UserDataEntity entity) { + repository.save(entity); + } + + // 모든 사용자를 가져오는 메소드 + public List getAllUsers() { + return repository.findAll(); + } + + // 사용자 삭제 메소드 + public boolean removeUser(String userId) { + Optional userOpt = repository.findByUserId(userId); + if (userOpt.isPresent()) { + repository.delete(userOpt.get()); + return true; + } + return false; + } + + // 이 메서드는 유저를 생성하고, 생성된 유저의 토큰을 반환합니다. + @Nullable + public UserAuthenticationDto createTokenWith(UserDataEntity userDataEntity) { + // 만약 유저가 존재하지 않는다면, BadCredentialsException을 던집니다. + Optional entity = repository.findUserDataEntityByUserId(userDataEntity.getUserId()); + if (entity.isEmpty()) throw new BadCredentialsException("Credentials invalid"); + UserDataEntity archivedEntity = entity.get(); + // 만약 유저가 존재한다면, 비밀번호를 비교합니다. + if (encoder.matches(userDataEntity.getPassword(), archivedEntity.getPassword())) + // 만약 비밀번호가 일치한다면, 토큰을 생성하여 반환합니다. + return new UserAuthenticationDto(jwtService.generateToken(archivedEntity.getUserId())); + // 만약 비밀번호가 일치하지 않는다면, BadCredentialsException을 던집니다. + throw new BadCredentialsException("Credentials invalid"); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/admin_dashboard.html b/src/main/resources/templates/admin_dashboard.html new file mode 100644 index 0000000..20b3eda --- /dev/null +++ b/src/main/resources/templates/admin_dashboard.html @@ -0,0 +1,22 @@ + + +

관리자 대시보드 (유저 대시보드로 이동)

+

유저 관리 페이지로 이동하기

+
+ + + 업로드할 내용:
+ +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/download.html b/src/main/resources/templates/download.html new file mode 100644 index 0000000..a35cece --- /dev/null +++ b/src/main/resources/templates/download.html @@ -0,0 +1,40 @@ + + + 파일 다운로드 페이지 + + + + <-- 업로드 페이지로 +

파일 다운로드 페이지

+

REST 요청을 보내 파일 목록을 불러옵니다:

+
    + +
+파일 목록을 불러오는 예제에 대해서는 이전 예제 레포지토리를 참고하세요. + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..26f5a23 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,6 @@ + + + 로그인 + 회원가입 + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..f1cd1ef --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,9 @@ + + +
+ + + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/management.html b/src/main/resources/templates/management.html new file mode 100644 index 0000000..8662488 --- /dev/null +++ b/src/main/resources/templates/management.html @@ -0,0 +1,53 @@ + + + +

유저 관리 페이지 (유저 대시보드로 이동)

+

관리자 대시보드로 이동

+
    +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html new file mode 100644 index 0000000..99d2b6d --- /dev/null +++ b/src/main/resources/templates/register.html @@ -0,0 +1,48 @@ + + + 회원가입 + + + +
+ + + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/register_check.html b/src/main/resources/templates/register_check.html new file mode 100644 index 0000000..6841d9d --- /dev/null +++ b/src/main/resources/templates/register_check.html @@ -0,0 +1,37 @@ + + + + + 회원가입 성공 + + + + +
+ 회원가입에 성공하였습니다. 로그인하여 서비스를 이용해주세요. +
+ + + + + diff --git a/src/main/resources/templates/request_aws_key.html b/src/main/resources/templates/request_aws_key.html new file mode 100644 index 0000000..442aa8c --- /dev/null +++ b/src/main/resources/templates/request_aws_key.html @@ -0,0 +1,33 @@ + + +

AWS API Key가 등록되지 않았습니다.

+AWS API Key가 등록되지 않았습니다.
+보안을 위해, 해당 프로젝트에서는 S3 API에 접근하기 위해 API 키를 어플리케이션 활성화시 1회 요청합니다.
+API 키는 저장되지 않습니다.


+ +API 키를 만들려면 다음 과정을 따라합니다. +
    +
  1. AWS IAM 콘솔에 + 접속합니다. +
  2. +
  3. 사용자 생성을 누릅니다.
  4. +
  5. 중복되지 않는 사용자명을 입력합니다. 해당 프로젝트에서는 s3_user로 생성하십시오.
  6. +
  7. 직접 정책 연결를 선택하고, AmazonS3FullAccess를 검색합니다. 해당 권한은 발급된 키로 자신의 S3 API의 모든 접근을 허용하겠다는 의미입니다. +
  8. +
  9. 사용자 생성을 누릅니다.
  10. +
  11. AWS IAM 콘솔에서 s3_user을 클릭합니다.
  12. +
  13. 요약 창에서 푸른식 액세스 키 만들기를 클릭합니다.
  14. +
  15. AWS 외부에서 실행되는 애플리케이션을 선택하고, 다음을 누릅니다.
  16. +
  17. 액세스 키 만들기를 누릅니다.
  18. +
  19. 출력된 액세스 키를 입력합니다. 경고: 한번 발급된 키는 페이지를 나가면 다시 볼 수 없습니다.
  20. +
+
+ API 액세스 키
+
+ API 비밀 액세스 키
+

+ +
+ + diff --git a/src/main/resources/templates/service_selection.html b/src/main/resources/templates/service_selection.html new file mode 100644 index 0000000..f9c0428 --- /dev/null +++ b/src/main/resources/templates/service_selection.html @@ -0,0 +1,24 @@ + + + + + 서비스 선택 + + +

서비스를 선택하세요

+
+ + +
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/upload.html b/src/main/resources/templates/upload.html new file mode 100644 index 0000000..5565c4f --- /dev/null +++ b/src/main/resources/templates/upload.html @@ -0,0 +1,22 @@ + + + <-- 다운로드 페이지로 +

파일 업로드

+파일 업로드 코드를 작성하십시오. +파일을 업로드하는 예제에 대해서는 이전 예제 레포지토리를 참고하세요. + +
+
+
+
+ + + + + diff --git a/src/main/resources/templates/user_dashboard.html b/src/main/resources/templates/user_dashboard.html new file mode 100644 index 0000000..fcc8c0c --- /dev/null +++ b/src/main/resources/templates/user_dashboard.html @@ -0,0 +1,23 @@ + + +

사용자 대시보드 (관리자 대시보드로 이동)

+ REST 요청을 보내 파일 목록을 불러오고, 다음 li에 다운로드 링크와 함께 삽입하십시오. +REST로 데이터 목록을 불러오는 예제는 1번 예제 레포지토리를 참고하세요. +
    + +
+ + + + \ No newline at end of file