diff --git a/build.gradle b/build.gradle index f5bcbcb..8b2a080 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,19 @@ dependencies { annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // Thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + } //sourceSets { diff --git a/build/classes/java/main/umc/spring/domain/Member$MemberBuilder.class b/build/classes/java/main/umc/spring/domain/Member$MemberBuilder.class index f1082b8..0640fdd 100644 Binary files a/build/classes/java/main/umc/spring/domain/Member$MemberBuilder.class and b/build/classes/java/main/umc/spring/domain/Member$MemberBuilder.class differ diff --git a/build/classes/java/main/umc/spring/domain/Member.class b/build/classes/java/main/umc/spring/domain/Member.class index 67d0e52..a806133 100644 Binary files a/build/classes/java/main/umc/spring/domain/Member.class and b/build/classes/java/main/umc/spring/domain/Member.class differ diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html index b4ce4af..6d2a77f 100644 --- a/build/reports/problems/problems-report.html +++ b/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ diff --git a/build/resources/main/application.yml b/build/resources/main/application.yml index 5fb539e..72f08a9 100644 --- a/build/resources/main/application.yml +++ b/build/resources/main/application.yml @@ -25,4 +25,10 @@ spring: use_sql_comments: true hbm2ddl: auto: update - default_batch_fetch_size: 1000 \ No newline at end of file + default_batch_fetch_size: 1000 + +jwt: + token: + secretKey: umceightfightingjwttokenauthentication + expiration: + access: 14400000 \ No newline at end of file diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin index 344081a..d4527d7 100644 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and b/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/src/main/java/umc/spring/apiPayload/code/status/ErrorStatus.java b/src/main/java/umc/spring/apiPayload/code/status/ErrorStatus.java index efb8634..1a73979 100644 --- a/src/main/java/umc/spring/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/umc/spring/apiPayload/code/status/ErrorStatus.java @@ -26,7 +26,9 @@ public enum ErrorStatus implements BaseErrorCode { REGION_NOT_FOUND(HttpStatus.NOT_FOUND, "REGION40001", "지역이 존재하지 않습니다."), - RESTAURANT_NOT_FOUND(HttpStatus.NOT_FOUND, "RESTAURANT4001", "식당이 존재하지 않습니다."); + RESTAURANT_NOT_FOUND(HttpStatus.NOT_FOUND, "RESTAURANT4001", "식당이 존재하지 않습니다."), + + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN401", "인증이 필요합니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/umc/spring/apiPayload/exception/handler/MemberHandler.java b/src/main/java/umc/spring/apiPayload/exception/handler/MemberHandler.java new file mode 100644 index 0000000..d193c2a --- /dev/null +++ b/src/main/java/umc/spring/apiPayload/exception/handler/MemberHandler.java @@ -0,0 +1,11 @@ +package umc.spring.apiPayload.exception.handler; + +import umc.spring.apiPayload.code.BaseErrorCode; +import umc.spring.apiPayload.exception.GeneralException; + +public class MemberHandler extends GeneralException { + + public MemberHandler(BaseErrorCode baseErrorCode) { + super(baseErrorCode); + } +} diff --git a/src/main/java/umc/spring/config/properties/Constants.java b/src/main/java/umc/spring/config/properties/Constants.java new file mode 100644 index 0000000..8e4d562 --- /dev/null +++ b/src/main/java/umc/spring/config/properties/Constants.java @@ -0,0 +1,7 @@ +package umc.spring.config.properties; + +public final class Constants { + + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; +} diff --git a/src/main/java/umc/spring/config/properties/JwtProperties.java b/src/main/java/umc/spring/config/properties/JwtProperties.java new file mode 100644 index 0000000..eb6173b --- /dev/null +++ b/src/main/java/umc/spring/config/properties/JwtProperties.java @@ -0,0 +1,24 @@ +package umc.spring.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties("jwt.token") +public class JwtProperties { + + private String secretKey = ""; + private Expiration expiration; + + @Getter + @Setter + public static class Expiration { + + private Long access; + // TODO : refreshToken + } +} diff --git a/src/main/java/umc/spring/config/security/CustomUserDetailsService.java b/src/main/java/umc/spring/config/security/CustomUserDetailsService.java new file mode 100644 index 0000000..6701d01 --- /dev/null +++ b/src/main/java/umc/spring/config/security/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package umc.spring.config.security; + +import lombok.RequiredArgsConstructor; +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 umc.spring.domain.Member; +import umc.spring.repository.MemberRepository.MemberRepository; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("해당 이메일을 가진 유저가 존재하지 않습니다.: " + username)); + + return org.springframework.security.core.userdetails.User + .withUsername(member.getEmail()) + .password(member.getPassword()) + .roles(member.getRole().name()) + .build(); + } +} diff --git a/src/main/java/umc/spring/config/security/SecurityConfig.java b/src/main/java/umc/spring/config/security/SecurityConfig.java new file mode 100644 index 0000000..6de0625 --- /dev/null +++ b/src/main/java/umc/spring/config/security/SecurityConfig.java @@ -0,0 +1,45 @@ +package umc.spring.config.security; + +import lombok.RequiredArgsConstructor; +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.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import umc.spring.config.security.jwt.JwtAuthenticationFilter; +import umc.spring.config.security.jwt.JwtTokenProvider; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests( + (requests) -> requests + .requestMatchers("/", "/members/join", "/members/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.disable()) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/umc/spring/config/security/jwt/JwtAuthenticationFilter.java b/src/main/java/umc/spring/config/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..bc6bb96 --- /dev/null +++ b/src/main/java/umc/spring/config/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,43 @@ +package umc.spring.config.security.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import umc.spring.config.properties.Constants; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String token = resolveToken(request); + + if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } +} diff --git a/src/main/java/umc/spring/config/security/jwt/JwtTokenProvider.java b/src/main/java/umc/spring/config/security/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..ba3a00a --- /dev/null +++ b/src/main/java/umc/spring/config/security/jwt/JwtTokenProvider.java @@ -0,0 +1,91 @@ +package umc.spring.config.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import umc.spring.apiPayload.code.status.ErrorStatus; +import umc.spring.apiPayload.exception.handler.MemberHandler; +import umc.spring.config.properties.Constants; +import umc.spring.config.properties.JwtProperties; + +import java.security.Key; +import java.util.Collections; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtProperties jwtProperties; + + private Key getSigningKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes()); + } + + public String generateToken(Authentication authentication) { + + String email = authentication.getName(); + + return Jwts.builder() + .setSubject(email) + .claim("role", authentication.getAuthorities().iterator().next().getAuthority()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getAccess())) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean validateToken(String token) { + + try { + Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + String email = claims.getSubject(); + String role = claims.get("role", String.class); + + User principal = new User(email, "", Collections.singleton(() -> role)); + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } + + public static String resolveToken(HttpServletRequest request) { + + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } + + public Authentication extractAuthentication(HttpServletRequest request) { + + String accessToken = resolveToken(request); + if (accessToken == null || !validateToken(accessToken)) { + throw new MemberHandler(ErrorStatus.INVALID_TOKEN); + } + return getAuthentication(accessToken); + } +} diff --git a/src/main/java/umc/spring/converter/MemberConverter.java b/src/main/java/umc/spring/converter/MemberConverter.java index e861d68..de538d7 100644 --- a/src/main/java/umc/spring/converter/MemberConverter.java +++ b/src/main/java/umc/spring/converter/MemberConverter.java @@ -17,6 +17,21 @@ public static MemberResponseDTO.MemberJoinResultDTO toJoinResultDTO(Member membe .build(); } + public static MemberResponseDTO.LoginResultDTO toLoginResultDTO(Long memberID, String accessToken) { + return MemberResponseDTO.LoginResultDTO.builder() + .memberId(memberID) + .accessToken(accessToken) + .build(); + } + + public static MemberResponseDTO.MemberInfoDTO toMemberInfoDTO(Member member){ + return MemberResponseDTO.MemberInfoDTO.builder() + .name(member.getName()) + .email(member.getEmail()) + .gender(member.getGender().name()) + .build(); + } + public static Member toMember(MemberRequestDTO.MemberJoinDto request){ Gender gender = null; @@ -34,11 +49,14 @@ public static Member toMember(MemberRequestDTO.MemberJoinDto request){ return Member.builder() .address(request.getAddress()) + .email(request.getEmail()) + .password(request.getPassword()) .gender(gender) .name(request.getName()) .birthYear(request.getBirthYear()) .birthMonth(request.getBirthMonth()) .birthDay(request.getBirthDay()) + .role(request.getRole()) .favoriteFoodList(new ArrayList<>()) .build(); } diff --git a/src/main/java/umc/spring/domain/Member.java b/src/main/java/umc/spring/domain/Member.java index 66eebc6..72871cd 100644 --- a/src/main/java/umc/spring/domain/Member.java +++ b/src/main/java/umc/spring/domain/Member.java @@ -8,6 +8,7 @@ import umc.spring.domain.common.BaseEntity; import umc.spring.domain.enums.Gender; import umc.spring.domain.enums.MemberStatus; +import umc.spring.domain.enums.Role; import umc.spring.domain.mapping.FavoriteFood; import umc.spring.domain.mapping.MemberMission; import umc.spring.domain.mapping.MemberTerm; @@ -73,6 +74,16 @@ public class Member extends BaseEntity { private String profileImage; + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + + public void encodePassword(String password) { + this.password = password; + } + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List memberTermList = new ArrayList<>(); diff --git a/src/main/java/umc/spring/domain/enums/Role.java b/src/main/java/umc/spring/domain/enums/Role.java new file mode 100644 index 0000000..61d3b50 --- /dev/null +++ b/src/main/java/umc/spring/domain/enums/Role.java @@ -0,0 +1,5 @@ +package umc.spring.domain.enums; + +public enum Role { + ADMIN, USER +} diff --git a/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java b/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java index a3666c7..44e7852 100644 --- a/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java +++ b/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java @@ -3,5 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import umc.spring.domain.Member; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); } diff --git a/src/main/java/umc/spring/service/MemberService/MemberCommandService.java b/src/main/java/umc/spring/service/MemberService/MemberCommandService.java index 9d6df28..788b0c8 100644 --- a/src/main/java/umc/spring/service/MemberService/MemberCommandService.java +++ b/src/main/java/umc/spring/service/MemberService/MemberCommandService.java @@ -2,7 +2,10 @@ import umc.spring.domain.Member; import umc.spring.web.dto.MemberRequestDTO; +import umc.spring.web.dto.MemberResponseDTO; public interface MemberCommandService { Member joinMember(MemberRequestDTO.MemberJoinDto request); + + MemberResponseDTO.LoginResultDTO loginMember(MemberRequestDTO.LoginRequestDTO request); } diff --git a/src/main/java/umc/spring/service/MemberService/MemberCommandServiceImpl.java b/src/main/java/umc/spring/service/MemberService/MemberCommandServiceImpl.java index 8328d70..250cfed 100644 --- a/src/main/java/umc/spring/service/MemberService/MemberCommandServiceImpl.java +++ b/src/main/java/umc/spring/service/MemberService/MemberCommandServiceImpl.java @@ -2,9 +2,14 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import umc.spring.apiPayload.code.status.ErrorStatus; import umc.spring.apiPayload.exception.handler.FoodHandler; +import umc.spring.apiPayload.exception.handler.MemberHandler; +import umc.spring.config.security.jwt.JwtTokenProvider; import umc.spring.converter.FavoriteFoodConverter; import umc.spring.converter.MemberConverter; import umc.spring.domain.Food; @@ -13,7 +18,9 @@ import umc.spring.repository.FoodRepository.FoodRepository; import umc.spring.repository.MemberRepository.MemberRepository; import umc.spring.web.dto.MemberRequestDTO; +import umc.spring.web.dto.MemberResponseDTO; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -23,12 +30,17 @@ public class MemberCommandServiceImpl implements MemberCommandService{ private final MemberRepository memberRepository; private final FoodRepository foodRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; @Override @Transactional public Member joinMember(MemberRequestDTO.MemberJoinDto request) { Member newMember = MemberConverter.toMember(request); + + newMember.encodePassword(passwordEncoder.encode(request.getPassword())); // 암호화 + List foodList = request.getPreferCategory().stream() .map(category -> { return foodRepository.findById(category).orElseThrow(() -> new FoodHandler(ErrorStatus.FOOD_CATEGORY_NOT_FOUND)); @@ -40,4 +52,25 @@ public Member joinMember(MemberRequestDTO.MemberJoinDto request) { return memberRepository.save(newMember); } + + @Override + public MemberResponseDTO.LoginResultDTO loginMember(MemberRequestDTO.LoginRequestDTO request) { + Member member = memberRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new MemberHandler(ErrorStatus.INVALID_TOKEN); + } + + Authentication authentication = new UsernamePasswordAuthenticationToken( + member.getEmail(), null, Collections.singleton(() -> member.getRole().name()) + ); + + String accessToken = jwtTokenProvider.generateToken(authentication); + + return MemberConverter.toLoginResultDTO( + member.getId(), + accessToken + ); + } } \ No newline at end of file diff --git a/src/main/java/umc/spring/service/MemberService/MemberQueryService.java b/src/main/java/umc/spring/service/MemberService/MemberQueryService.java index b072127..6545682 100644 --- a/src/main/java/umc/spring/service/MemberService/MemberQueryService.java +++ b/src/main/java/umc/spring/service/MemberService/MemberQueryService.java @@ -1,8 +1,12 @@ package umc.spring.service.MemberService; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.data.domain.Page; import umc.spring.domain.Review; +import umc.spring.web.dto.MemberResponseDTO; public interface MemberQueryService { Page getReviewList(Long memberId, Integer page); + + MemberResponseDTO.MemberInfoDTO getMemberInfo(HttpServletRequest request); } diff --git a/src/main/java/umc/spring/service/MemberService/MemberQueryServiceImpl.java b/src/main/java/umc/spring/service/MemberService/MemberQueryServiceImpl.java index 5737ab9..4b72059 100644 --- a/src/main/java/umc/spring/service/MemberService/MemberQueryServiceImpl.java +++ b/src/main/java/umc/spring/service/MemberService/MemberQueryServiceImpl.java @@ -1,14 +1,22 @@ package umc.spring.service.MemberService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import umc.spring.apiPayload.code.status.ErrorStatus; +import umc.spring.apiPayload.exception.handler.MemberHandler; +import umc.spring.config.security.jwt.JwtTokenProvider; +import umc.spring.converter.MemberConverter; import umc.spring.domain.Member; import umc.spring.domain.Review; import umc.spring.repository.MemberRepository.MemberRepository; import umc.spring.repository.ReviewRepository.ReviewRepository; +import umc.spring.web.dto.MemberResponseDTO; @Service @@ -18,6 +26,7 @@ public class MemberQueryServiceImpl implements MemberQueryService { private final MemberRepository memberRepository; private final ReviewRepository reviewRepository; + private final JwtTokenProvider jwtTokenProvider; @Override public Page getReviewList(Long memberId, Integer page) { @@ -27,4 +36,15 @@ public Page getReviewList(Long memberId, Integer page) { Page RestaurantPage = reviewRepository.findAllByMember(member, PageRequest.of(page, 10)); return RestaurantPage; } + + @Override + @Transactional(readOnly = true) + public MemberResponseDTO.MemberInfoDTO getMemberInfo(HttpServletRequest request) { + Authentication authentication = jwtTokenProvider.extractAuthentication(request); + String email = authentication.getName(); + + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + return MemberConverter.toMemberInfoDTO(member); + } } \ No newline at end of file diff --git a/src/main/java/umc/spring/web/controller/MemberRestController.java b/src/main/java/umc/spring/web/controller/MemberRestController.java index d5c4cb7..bd9886a 100644 --- a/src/main/java/umc/spring/web/controller/MemberRestController.java +++ b/src/main/java/umc/spring/web/controller/MemberRestController.java @@ -6,6 +6,8 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -40,7 +42,7 @@ public class MemberRestController { private final MemberQueryService memberQueryService; private final MemberMissionQueryService memberMissionQueryService; - @PostMapping("/") + @PostMapping("/join") public ApiResponse join(@RequestBody @Valid MemberRequestDTO.MemberJoinDto request){ Member member = memberCommandService.joinMember(request); return ApiResponse.onSuccess(MemberConverter.toJoinResultDTO(member)); @@ -84,4 +86,19 @@ public ApiResponse getMemb return ApiResponse.onSuccess(MemberMissionConverter.toMemberMissionPreViewListDTO(memberMissionList)); } + @PostMapping("/login") + @Operation(summary = "유저 로그인 API", description = "유저가 로그인하는 API입니다.") + public ApiResponse login(@RequestBody @Valid MemberRequestDTO.LoginRequestDTO request) { + return ApiResponse.onSuccess(memberCommandService.loginMember(request)); + } + + @GetMapping("/info") + @Operation(summary = "유저 내 정보 조회 API - 인증 필요", + description = "유저가 내 정보를 조회하는 API입니다.", + security = { @SecurityRequirement(name = "JWT TOKEN") } + ) + public ApiResponse getMyInfo(HttpServletRequest request) { + return ApiResponse.onSuccess(memberQueryService.getMemberInfo(request)); + } + } \ No newline at end of file diff --git a/src/main/java/umc/spring/web/dto/MemberRequestDTO.java b/src/main/java/umc/spring/web/dto/MemberRequestDTO.java index 77aa329..6b937fd 100644 --- a/src/main/java/umc/spring/web/dto/MemberRequestDTO.java +++ b/src/main/java/umc/spring/web/dto/MemberRequestDTO.java @@ -1,12 +1,18 @@ package umc.spring.web.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; +import lombok.Setter; +import umc.spring.domain.enums.Role; import umc.spring.validation.annotation.ExistCategories; import java.util.List; public class MemberRequestDTO { @Getter + @Setter public static class MemberJoinDto { String name; Integer gender; @@ -16,5 +22,23 @@ public static class MemberJoinDto { String address; @ExistCategories List preferCategory; + @NotBlank + @Email + String email; + @NotBlank + String password; + @NotNull + Role role; + } + + @Getter + @Setter + public static class LoginRequestDTO { + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이어야 합니다.") + private String email; + + @NotBlank(message = "패스워드는 필수입니다.") + private String password; } } \ No newline at end of file diff --git a/src/main/java/umc/spring/web/dto/MemberResponseDTO.java b/src/main/java/umc/spring/web/dto/MemberResponseDTO.java index 3d5b867..b56184d 100644 --- a/src/main/java/umc/spring/web/dto/MemberResponseDTO.java +++ b/src/main/java/umc/spring/web/dto/MemberResponseDTO.java @@ -17,4 +17,23 @@ public static class MemberJoinResultDTO { Long memberId; LocalDateTime createdAt; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LoginResultDTO { + Long memberId; + String accessToken; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MemberInfoDTO { + String name; + String email; + String gender; + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5fb539e..72f08a9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,4 +25,10 @@ spring: use_sql_comments: true hbm2ddl: auto: update - default_batch_fetch_size: 1000 \ No newline at end of file + default_batch_fetch_size: 1000 + +jwt: + token: + secretKey: umceightfightingjwttokenauthentication + expiration: + access: 14400000 \ No newline at end of file diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 0000000..55dbff1 --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,10 @@ + + + + Admin Page + + +

Admin Page

+

관리자만 접근할 수 있는 페이지입니다.

+ + \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..8c10cb1 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,20 @@ + + + + Home + + +

Welcome to Home Page!

+ +

+ + + + + +
+ +
+ \ 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..7804a3a --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,26 @@ + + + + Login + + +

Login

+
+
+ + +
+
+ + +
+ +
+ +

사용자 이름 또는 비밀번호가 잘못되었습니다.

+

로그아웃되었습니다.

+ + +

계정이 없나요? Sign up

+ + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..18d82ec --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,72 @@ + + + + 회원가입 + + + +

회원가입

+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + +
+ +
+ + + +
+
+
+ + +
+ +
+ + \ No newline at end of file