Skip to content

Commit

Permalink
Merge pull request #16 from Wedit-project/feat/#5-임시-로그인-기능-구현
Browse files Browse the repository at this point in the history
[feat] 임시 로그인 기능 구현
  • Loading branch information
dogsub authored Jan 18, 2025
2 parents c417ab5 + 04d6310 commit f92dd8c
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.wedit.weditapp.domain.member.controller;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.member.dto.LoginRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberResponseDto;
import com.wedit.weditapp.domain.member.service.MemberService;
import com.wedit.weditapp.global.response.GlobalResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MemberController {

private final MemberService memberService;

// 회원 가입 API
@Operation(summary = "회원가입", description = "새로운 유저를 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "회원 가입 성공"),
@ApiResponse(responseCode = "409", description = "이미 가입된 이메일입니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@PostMapping("/signup")
public ResponseEntity<GlobalResponseDto<MemberResponseDto>> createMember(
@Valid @RequestBody MemberRequestDto requestDto) {
memberService.createMember(requestDto); // 반환값 사용 X -> 추후 OAuth2 구현하면서 변동 가능성 O
return ResponseEntity.status(HttpStatus.OK).body(GlobalResponseDto.success());
}

// 임시 로그인 API
@Operation(summary = "임시 로그인", description = "해당 서비스에 로그인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "로그인 성공"),
@ApiResponse(responseCode = "401", description = "로그인에 실패하였습니다."),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@PostMapping("/login")
public ResponseEntity<GlobalResponseDto<String>> login(
@RequestBody LoginRequestDto loginRequest) {
// 로그인 로직 → 토큰 발급
String token = memberService.login(loginRequest);

// 토큰을 응답 바디로 전달
return ResponseEntity.ok(GlobalResponseDto.success(token));
}

// 모든 회원 조회 API
@Operation(summary = "모든 회원 조회", description = "등록된 모든 회원의 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@GetMapping
public ResponseEntity<GlobalResponseDto<List<MemberResponseDto>>> findAllMembers() {
List<MemberResponseDto> memberResponses = memberService.findAllMembers();
return ResponseEntity.ok(GlobalResponseDto.success(memberResponses));
}

// 단일 회원 조회 API
@Operation(summary = "단일 회원 조회", description = "해당 id를 가진 회원의 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@GetMapping("/{userId}")
public ResponseEntity<GlobalResponseDto<MemberResponseDto>> findMember(
@PathVariable Long userId) {
return ResponseEntity.ok(GlobalResponseDto.success(memberService.findMember(userId)));
}
}
47 changes: 32 additions & 15 deletions src/main/java/com/wedit/weditapp/domain/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.wedit.weditapp.domain.shared.BaseTimeEntity;
import com.wedit.weditapp.domain.shared.MemberRole;
import com.wedit.weditapp.domain.shared.MemberStatus;
import com.wedit.weditapp.global.error.ErrorCode;
import com.wedit.weditapp.global.error.exception.CommonException;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
Expand All @@ -22,45 +24,60 @@ public class Member extends BaseTimeEntity {
@Column(nullable = false)
private String email;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String name;

@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
private MemberRole role;

@Column(nullable = false, unique = true)
private String refresh_token;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private MemberStatus status;

// Builder를 통해서만 객체를 생성하도록 (일반 생성자는 protected)
@Builder
private Member(String kakaoId, String email, String name, MemberRole role, String password, MemberStatus status) {
private Member(String email, String name) {
this.email = email;
this.name = name;
this.role = role != null ? role : MemberRole.USER; // 유저 역할 기본값 USER
this.password = password;
this.status = status != null ? status : MemberStatus.ACTIVE; // 유저 상태 기본값 ACTIVE
this.role = MemberRole.USER;
this.status = MemberStatus.ACTIVE;
}

public static Member createUser(String email, String name, String password) {
// 사용자 생성 Method
public static Member createUser(String email, String name) {
return Member.builder()
.email(email)
.name(name)
.password(password)
.role(MemberRole.USER) // 기본값 USER
.status(MemberStatus.ACTIVE) // 기본값 ACTIVE
.build();
}

// 상태 변경 예시 메서드
// 사용자 이메일 변경 Method
public void updateEmail(String newEmail) {
if (newEmail == null || newEmail.trim().isEmpty()) {
throw new CommonException(ErrorCode.EMPTY_FIELD);
}
this.email = newEmail;
}

// 사용자 이름 변경 Method
public void updateName(String newName) {
if (newName == null || newName.trim().isEmpty()) {
throw new CommonException(ErrorCode.EMPTY_FIELD);
}
this.name = newName;
}

// 사용자 상태 변경 Method
public void deactivate() {
this.status = MemberStatus.INACTIVE;
}

// 사용자 역할 변경 Method
public void updateRole(MemberRole newRole) {
if (newRole == null) {
throw new CommonException(ErrorCode.EMPTY_FIELD);
}
this.role = newRole;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.wedit.weditapp.domain.member.domain.repository;

import com.wedit.weditapp.domain.member.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
// 임시 로그인용 : 이메일로 회원 찾기
Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.wedit.weditapp.domain.member.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class LoginRequestDto {
private String email;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.wedit.weditapp.domain.member.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;


@Getter
@NoArgsConstructor
public class MemberRequestDto {
@Email(message = "이메일 형식을 지켜야 합니다.")
@NotBlank(message = "이메일을 반드시 입력해야 합니다.")
private String email;

@NotBlank(message = "이름을 반드시 입력해야 합니다.")
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.wedit.weditapp.domain.member.dto;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.shared.MemberRole;
import com.wedit.weditapp.domain.shared.MemberStatus;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MemberResponseDto {
private Long id;
private String email;
private String name;
private MemberRole role;
private MemberStatus status;

@Builder
private MemberResponseDto(Long id, String email, String name, MemberRole role, MemberStatus status) {
this.id = id;
this.email = email;
this.name = name;
this.role = role;
this.status = status;
}

public static MemberResponseDto from(Member member) {
return MemberResponseDto.builder()
.id(member.getId())
.email(member.getEmail())
.name(member.getName())
.role(member.getRole())
.status(member.getStatus())
.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.wedit.weditapp.domain.member.service;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.member.domain.repository.MemberRepository;
import com.wedit.weditapp.domain.member.dto.LoginRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberResponseDto;
import com.wedit.weditapp.global.error.ErrorCode;
import com.wedit.weditapp.global.error.exception.CommonException;
import com.wedit.weditapp.global.security.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
private final JwtProvider jwtProvider; // JWT 발급을 위해

// [회원가입 관련] - 이미 존재하는 이메일인지 검사 + Member 엔티티 생성 + DB저장 및 반환
public Member createMember(MemberRequestDto requestDto) {
if (memberRepository.findByEmail(requestDto.getEmail()).isPresent()) {
throw new CommonException(ErrorCode.EMAIL_ALREADY_EXISTS);
}

Member member = Member.createUser(
requestDto.getEmail(),
requestDto.getName()
);

return memberRepository.save(member);
}

public String login(LoginRequestDto loginRequest) {
// 이메일로 회원 조회
Member member = memberRepository.findByEmail(loginRequest.getEmail())
.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND));

// JWT Access Token 생성
return jwtProvider.createAccessToken(member.getEmail());
}

// [모든 회원 조회]
public List<MemberResponseDto> findAllMembers() {
return memberRepository.findAll()
.stream()
.map(MemberResponseDto::from)
.collect(Collectors.toList());
}

// [단일 회원 조회]
public MemberResponseDto findMember(Long userId) {
Member member = memberRepository.findById(userId)
.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND));
return MemberResponseDto.from(member);
}
}
31 changes: 26 additions & 5 deletions src/main/java/com/wedit/weditapp/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

import java.util.Arrays;

import com.wedit.weditapp.global.security.jwt.JwtAuthenticationFilter;
import com.wedit.weditapp.global.security.jwt.JwtProvider;
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.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -22,13 +28,19 @@
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)

// 세션 관련 정책 추가 : 세션 방식 사용 X (오직 JWT만 사용)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

.headers(
headersConfigurer ->
headersConfigurer
Expand All @@ -39,26 +51,35 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**").permitAll() // swagger-ui 접근 허용
.anyRequest().authenticated()
.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/api/members/login", "/api/members/signup").permitAll() // swagger-ui 접근 허용 + 로그인과 회원가입까지만 허용
.anyRequest().authenticated() // 그외에는 Access Token을 가진 유저만 접근 가능
);

// JWT 인증 필터 등록
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(Arrays.asList());
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("Authorization, Authorization_refresh", "accept"));
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "Authorization_refresh", "accept"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

// 회원가입 시 사용
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

}
Loading

0 comments on commit f92dd8c

Please sign in to comment.