Skip to content

Commit 1a62d32

Browse files
authored
feat : 로그인 기능 및 api key 등록기능추가 (#9)
* feat : ApiKey 엔티티 추가 #6 * feat : Domain 추가 #6 * fix : 토큰 관련 위치 변경 #6 * feat : 약관 동의 항목 1개 추가 #6 * feat : BaseTimeEntity 추가 #6 * feat : 생일 파싱 기능 추가 #6 * fix : 유저 인증 기능 수정 #6 * fix : 파일 위치 변경#6 * feat : 로그아웃, 회원탈퇴, 회원 정보 수정 기능 추가 #6 * feat : API 키 클래스 명 변경 #6 * feat : 회원정보 조회 기능 추가 #6 * feat : 일반 로그인 기능 구현 #6 * feat : 토큰 재발급 기능 추가 #6 * feat : 아이디 찾기 기능 구현 #6 * feat : 비밀번호 변경 구현 #6 * feat : APIKEY 등록,삭제,조회 기능 추가 #6 * feat : swagger에 토큰 검증 추가 #6 * feat : 레디스 설정 추가 #6 * feat : 로그인 용 security 뚫어두기 #6 * feat : 이메일 인증 기능 추가 #6 * feat : 이메일 인증을 위한 gradle 수정 #6 * feat : 회원가입 단계에 이메일 otp 인증 부분 추가 #6 * fix : API key관련 문제 생기는 부분 수정 #6
1 parent 55c3b4a commit 1a62d32

File tree

57 files changed

+1567
-204
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1567
-204
lines changed

build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ dependencies {
4040
implementation 'org.slf4j:slf4j-api:2.0.9'
4141
implementation 'org.springframework.boot:spring-boot-starter-logging'
4242

43+
//레디스
44+
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
4345

46+
//메일 전송
47+
implementation 'org.springframework.boot:spring-boot-starter-validation'
48+
implementation 'org.springframework.security:spring-security-crypto'
49+
implementation 'com.fasterxml.jackson.core:jackson-databind'
50+
51+
implementation "org.springframework.boot:spring-boot-starter-mail" // JavaMailSender
52+
implementation "org.springframework.boot:spring-boot-starter-validation"
4453
testImplementation 'io.projectreactor:reactor-test'
4554
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
4655

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.backend.crame.domain.apikey.controller;
2+
3+
import java.util.List;
4+
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
7+
import org.springframework.web.bind.annotation.DeleteMapping;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
import com.backend.crame.domain.apikey.dto.ApiKeyDeleteResponse;
16+
import com.backend.crame.domain.apikey.dto.ApiKeyRequest;
17+
import com.backend.crame.domain.apikey.dto.ApiKeyResponse;
18+
import com.backend.crame.domain.apikey.service.ApiKeyService;
19+
import com.backend.crame.domain.token.dto.TokenResponse;
20+
import com.backend.crame.domain.token.entity.CustomPrincipal;
21+
import com.backend.crame.domain.user.dto.SignUpRequest;
22+
import com.backend.crame.global.response.BaseResponse;
23+
import com.backend.crame.global.response.dto.ResponseDto;
24+
import com.backend.crame.global.response.enums.SuccessCode;
25+
26+
import io.swagger.v3.oas.annotations.Operation;
27+
import io.swagger.v3.oas.annotations.tags.Tag;
28+
import lombok.RequiredArgsConstructor;
29+
import reactor.core.publisher.Mono;
30+
31+
@RestController
32+
@RequestMapping("/api/v1/apikey")
33+
@Tag(name = "APIKEY 관련 API")
34+
@RequiredArgsConstructor
35+
public class ApiKeyController {
36+
37+
private final ApiKeyService apiKeyService;
38+
39+
//API KEY 추가
40+
@PostMapping()
41+
@Operation(summary = "APIKEY를 추가 API")
42+
public Mono<ResponseEntity<ResponseDto<ApiKeyResponse>>> makeKey(@AuthenticationPrincipal CustomPrincipal customPrincipal, @RequestBody
43+
ApiKeyRequest request) {
44+
return apiKeyService.postNewKey(customPrincipal,request)
45+
.map(data -> BaseResponse.success(SuccessCode.API_KEY_MAKE_SUCCESS, data));
46+
}
47+
48+
//API KEY 삭제
49+
@DeleteMapping()
50+
@Operation(summary = "APIKEY를 삭제 API")
51+
public Mono<ResponseEntity<ResponseDto<ApiKeyDeleteResponse>>> deleteKey(@AuthenticationPrincipal CustomPrincipal customPrincipal,@RequestParam String publicKey) {
52+
return apiKeyService.deleteApiKey(customPrincipal,publicKey)
53+
.map(data -> BaseResponse.success(SuccessCode.API_KEY_DELETE_SUCCESS, data));
54+
}
55+
56+
57+
//API KEY 가져오기
58+
@GetMapping()
59+
@Operation(summary = "가지고 있는 APIKEY 가져오는 API -> pageable 필요 없을 것 같아서 그냥 list로 보냄")
60+
public Mono<ResponseEntity<ResponseDto<List<ApiKeyResponse>>>> getKeys(@AuthenticationPrincipal CustomPrincipal customPrincipal) {
61+
return apiKeyService.getKeysByUser(customPrincipal)
62+
.map(data -> BaseResponse.success(SuccessCode.API_KEY_GET_SUCCESS, data));
63+
}
64+
65+
66+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.backend.crame.domain.apikey.dto;
2+
3+
public record ApiKeyDeleteResponse(String message) {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.backend.crame.domain.apikey.dto;
2+
3+
public record ApiKeyRequest(String publicKey, String secretKey, String nickName) {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.backend.crame.domain.apikey.dto;
2+
3+
public record ApiKeyResponse(String nickName, String publicKey, String secretKey) {
4+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.backend.crame.domain.apikey.entitiy;
2+
3+
import org.springframework.data.annotation.Id;
4+
import org.springframework.data.mongodb.core.mapping.Document;
5+
import org.springframework.data.mongodb.core.mapping.Field;
6+
7+
import com.backend.crame.global.utils.BaseTimeEntity;
8+
9+
import lombok.AccessLevel;
10+
import lombok.AllArgsConstructor;
11+
import lombok.Builder;
12+
import lombok.Getter;
13+
import lombok.NoArgsConstructor;
14+
import lombok.Setter;
15+
16+
@Document(collection = "apikey")
17+
@Getter
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
19+
public class ApiKey extends BaseTimeEntity {
20+
@Id
21+
private String uuid;
22+
23+
private String userId;
24+
25+
@Setter
26+
private String nickname;
27+
28+
@Field("public_key")
29+
private String publicKey;
30+
31+
@Setter
32+
private String keyVersion;
33+
34+
private String secretKey;
35+
36+
@Builder
37+
private ApiKey(String key_uuid, String user_uuid, String nickname, String publicKey, String secretKey,String keyVersion){
38+
this.uuid = key_uuid;
39+
this.userId = user_uuid;
40+
this.nickname = nickname;
41+
this.publicKey = publicKey;
42+
this.secretKey = secretKey;
43+
this.keyVersion = keyVersion;
44+
}
45+
46+
47+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.backend.crame.domain.apikey.repository;
2+
3+
4+
5+
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
6+
7+
import com.backend.crame.domain.apikey.entitiy.ApiKey;
8+
9+
import reactor.core.publisher.Flux;
10+
import reactor.core.publisher.Mono;
11+
12+
public interface ApiKeyRepository extends ReactiveMongoRepository<ApiKey,String> {
13+
14+
Mono<Long> deleteByUserIdAndPublicKey(String userId,String publicKey);
15+
Flux<ApiKey> findAllByUserId(String userId);
16+
Mono<Boolean> existsByUserIdAndPublicKey(String userUuid, String publicKey);
17+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.backend.crame.domain.apikey.service;
2+
3+
import java.util.List;
4+
import java.util.UUID;
5+
6+
import org.springframework.stereotype.Service;
7+
8+
import com.backend.crame.domain.apikey.dto.ApiKeyDeleteResponse;
9+
import com.backend.crame.domain.apikey.dto.ApiKeyRequest;
10+
import com.backend.crame.domain.apikey.dto.ApiKeyResponse;
11+
import com.backend.crame.domain.apikey.entitiy.ApiKey;
12+
import com.backend.crame.domain.apikey.repository.ApiKeyRepository;
13+
import com.backend.crame.domain.token.entity.CustomPrincipal;
14+
import com.backend.crame.domain.user.repository.UserRepository;
15+
import com.backend.crame.global.exception.BaseException;
16+
import com.backend.crame.global.exception.ErrorCode;
17+
import com.backend.crame.global.utils.AesGcmCrypto;
18+
19+
import lombok.RequiredArgsConstructor;
20+
import reactor.core.publisher.Mono;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
public class ApiKeyService {
25+
26+
private final ApiKeyRepository apiKeyRepository;
27+
private final UserRepository userRepository;
28+
private final AesGcmCrypto aesGcmCrypto;
29+
30+
public Mono<ApiKeyResponse> postNewKey(CustomPrincipal principal, ApiKeyRequest request) {
31+
final String uuid = UUID.randomUUID().toString();
32+
final String userId = principal.getUserId();
33+
final String publicKey = request.publicKey();
34+
35+
return userRepository.findById(userId)
36+
.switchIfEmpty(Mono.error(new BaseException(ErrorCode.USER_NOT)))
37+
.then(apiKeyRepository.existsByUserIdAndPublicKey(userId, publicKey))
38+
.flatMap(exists -> {
39+
if (exists) {
40+
return Mono.error(new BaseException(ErrorCode.API_KEY_ALREADY_EXISTS));
41+
}
42+
String enc = aesGcmCrypto.encrypt(request.secretKey());
43+
ApiKey entity = ApiKey.builder()
44+
.key_uuid(uuid)
45+
.nickname(request.nickName())
46+
.publicKey(publicKey)
47+
.keyVersion(aesGcmCrypto.getKeyVersion())
48+
.secretKey(enc)
49+
.user_uuid(userId)
50+
.build();
51+
return apiKeyRepository.save(entity);
52+
})
53+
.onErrorMap(
54+
ex -> ex instanceof org.springframework.dao.DuplicateKeyException,
55+
ex -> new BaseException(ErrorCode.API_KEY_ALREADY_EXISTS)
56+
)
57+
.map(key -> {
58+
String decrypted = aesGcmCrypto.decrypt(key.getSecretKey());
59+
String masked = maskSecret(decrypted);
60+
return new ApiKeyResponse(key.getNickname(), key.getPublicKey(), masked);
61+
});
62+
}
63+
64+
65+
66+
public Mono<ApiKeyDeleteResponse> deleteApiKey(CustomPrincipal principal, String keyPublicKey) {
67+
final String userId = principal.getUserId();
68+
69+
return userRepository.findById(userId)
70+
.switchIfEmpty(Mono.error(new BaseException(ErrorCode.USER_NOT)))
71+
.flatMap(u -> apiKeyRepository.deleteByUserIdAndPublicKey(userId, keyPublicKey))
72+
.flatMap(deleted -> {
73+
if (deleted > 0) {
74+
return Mono.just(new ApiKeyDeleteResponse("API KEY가 삭제되었습니다."));
75+
} else {
76+
return Mono.error(new BaseException(ErrorCode.API_KEY_NOT_FOUND));
77+
}
78+
});
79+
}
80+
81+
82+
public Mono<List<ApiKeyResponse>> getKeysByUser(CustomPrincipal customPrincipal) {
83+
return apiKeyRepository.findAllByUserId(customPrincipal.getUserId())
84+
.map(key -> {
85+
String decrypted = aesGcmCrypto.decrypt(key.getSecretKey());
86+
String masked = maskSecret(decrypted);
87+
return new ApiKeyResponse(
88+
key.getNickname(),
89+
key.getPublicKey(),
90+
masked
91+
);
92+
})
93+
.collectList();
94+
}
95+
96+
private String maskSecret(String secret) {
97+
if (secret == null || secret.isEmpty()) {
98+
return "";
99+
}
100+
int visible = Math.min(2, secret.length()); // 앞 2자리만 노출
101+
return secret.substring(0, visible) + "*".repeat(secret.length() - visible);
102+
}
103+
104+
}

src/main/java/com/backend/crame/domain/google/controller/GoogleController.java

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,32 @@
33
import java.util.Map;
44

55
import org.springframework.http.ResponseEntity;
6-
import org.springframework.web.bind.annotation.DeleteMapping;
7-
import org.springframework.web.bind.annotation.GetMapping;
86
import org.springframework.web.bind.annotation.PostMapping;
9-
import org.springframework.web.bind.annotation.PutMapping;
10-
import org.springframework.web.bind.annotation.RequestBody;
117
import org.springframework.web.bind.annotation.RequestMapping;
128
import org.springframework.web.bind.annotation.RequestParam;
13-
import org.springframework.web.bind.annotation.ResponseStatus;
149
import org.springframework.web.bind.annotation.RestController;
1510

16-
import com.backend.crame.domain.google.dto.SignUpRequest;
1711
import com.backend.crame.domain.google.service.GoogleOAuthService;
18-
import com.backend.crame.global.exception.BaseException;
19-
import com.backend.crame.global.exception.ErrorCode;
20-
import com.backend.crame.global.response.BaseResponse;
21-
import com.backend.crame.global.response.enums.SuccessCode;
2212

2313
import io.swagger.v3.oas.annotations.tags.Tag;
2414
import lombok.RequiredArgsConstructor;
2515
import reactor.core.publisher.Mono;
2616

2717
@RestController
28-
@RequestMapping("/api/login/google")
18+
@RequestMapping("/api/v1/google")
2919
@Tag(name = "구글 로그인 API", description = "구글 로그인 관련 API입니다.")
3020
@RequiredArgsConstructor
3121
public class GoogleController {
3222

3323
private final GoogleOAuthService googleOAuthService;
3424

35-
@PostMapping("")
25+
@PostMapping("/login")
3626
public Mono<ResponseEntity<Map<String, Object>>> loginWithGoogle(@RequestParam("code") String code) {
3727
return googleOAuthService.loginWithGoogle(code)
3828
.map(response -> ResponseEntity.ok().body(response))
3929
.onErrorMap(e -> new IllegalArgumentException(e.getMessage()));
4030
}
4131

42-
@PutMapping("/signup")
43-
public Mono<ResponseEntity<?>> completeSignup(@RequestBody SignUpRequest request) {
44-
return googleOAuthService.completeSignup(request)
45-
.map(data -> BaseResponse.success(SuccessCode.SIGNUP_SUCCESS, data));
46-
}
47-
48-
49-
@DeleteMapping("/logout")
50-
public Mono<ResponseEntity<?>> logout(@RequestParam("userId") String userId){
51-
return googleOAuthService.logOut(userId)
52-
.map(data->BaseResponse.success(SuccessCode.LOGOUT_SUCCESS,data));
53-
}
5432

5533

5634
}

src/main/java/com/backend/crame/domain/google/dto/SignUpRequest.java

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)