Skip to content

Commit

Permalink
Merge pull request #59 from kakao-tech-campus-2nd-step3/Weekly
Browse files Browse the repository at this point in the history
6μ£Όμ°¨ μ‚°μΆœλ¬Ό(Weekly -> Develop)
  • Loading branch information
zzoe2346 authored Oct 11, 2024
2 parents fe56814 + 164f38a commit f84bbda
Show file tree
Hide file tree
Showing 84 changed files with 2,750 additions and 247 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Deploy

on:
push:
branches:
- Weekly

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up SSH
uses: webfactory/ssh-agent@v0.5.3
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

- name: Run deployment script on server
run: |
ssh -o StrictHostKeyChecking=no ubuntu@${{ secrets.SERVER_IP }} 'bash /home/ubuntu/deploy.sh'
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ out/

### dev file ###
src/main/resources/application-dev.properties

### macOS DS_Store files ###
.DS_Store
src/.DS_Store
src/main/.DS_Store
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation group: 'com.twilio.sdk', name: 'twilio', version: '10.5.0'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.example.sinitto.auth.controller;

import com.example.sinitto.auth.exception.JWTExpirationException;
import com.example.sinitto.auth.exception.KakaoRefreshTokenExpirationException;
import com.example.sinitto.auth.exception.TokenNotFoundException;
import com.example.sinitto.auth.exception.UnauthorizedException;
import com.example.sinitto.auth.exception.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
Expand All @@ -15,11 +12,17 @@
@RestControllerAdvice(basePackages = "com.example.sinitto.auth")
public class AuthControllerAdvice {

private static final String UNAUTHORIZED_ACCESS_URI = "/errors/unauthorized-access";
private static final String JWT_EXPIRATION_URI = "/errors/unauthorized-access-by-jwt-expiration";
private static final String TOKEN_NOT_FOUND_URI = "/errors/token-not-found";
private static final String KAKAO_REFRESH_TOKEN_EXPIRATION_URI = "/errors/unauthorized-access-by-kakao-refresh-token-expiration";
private static final String KAKAO_EMAIL_NOT_FOUND_URI = "/errors/kakao-email-not-found";

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ProblemDetail> handleUnauthorizedException(UnauthorizedException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
ex.getMessage());
problemDetail.setType(URI.create("/errors/unauthorized-access"));
problemDetail.setType(URI.create(UNAUTHORIZED_ACCESS_URI));
problemDetail.setTitle("Unauthorized Access");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail);
}
Expand All @@ -28,7 +31,7 @@ public ResponseEntity<ProblemDetail> handleUnauthorizedException(UnauthorizedExc
public ResponseEntity<ProblemDetail> handleJWTExpirationException(JWTExpirationException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
ex.getMessage());
problemDetail.setType(URI.create("/errors/unauthorized-access-by-jwt-expiration"));
problemDetail.setType(URI.create(JWT_EXPIRATION_URI));
problemDetail.setTitle("Unauthorized Access By JWT Expiration");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail);
}
Expand All @@ -37,7 +40,7 @@ public ResponseEntity<ProblemDetail> handleJWTExpirationException(JWTExpirationE
public ResponseEntity<ProblemDetail> handleTokenNotFoundException(TokenNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
ex.getMessage());
problemDetail.setType(URI.create("/errors/token-not-found"));
problemDetail.setType(URI.create(TOKEN_NOT_FOUND_URI));
problemDetail.setTitle("Token Not Found");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail);
}
Expand All @@ -46,8 +49,17 @@ public ResponseEntity<ProblemDetail> handleTokenNotFoundException(TokenNotFoundE
public ResponseEntity<ProblemDetail> handleKakaoRefreshTokenExpirationException(KakaoRefreshTokenExpirationException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
ex.getMessage());
problemDetail.setType(URI.create("/errors/unauthorized-access-by-kakao-refresh-token-expiration"));
problemDetail.setType(URI.create(KAKAO_REFRESH_TOKEN_EXPIRATION_URI));
problemDetail.setTitle("Unauthorized Access By Kakao Refresh Token Expiration");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail);
}

@ExceptionHandler(KakaoEmailNotFoundException.class)
public ResponseEntity<ProblemDetail> handleKakaoEmailNotFoundException(KakaoEmailNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
ex.getMessage());
problemDetail.setType(URI.create(KAKAO_EMAIL_NOT_FOUND_URI));
problemDetail.setTitle("Kakao Email Not Found");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
public record KakaoTokenResponse(
String accessToken,
String refreshToken,
Integer expiresIn,
Integer refreshTokenExpiresIn
int expiresIn,
int refreshTokenExpiresIn

) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public record LoginResponse(
String refreshToken,
String redirectUrl,
String email,
boolean isSinitto
boolean isSinitto,
boolean isMember
) {
}
20 changes: 10 additions & 10 deletions src/main/java/com/example/sinitto/auth/entity/KakaoToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,28 @@ public class KakaoToken {
@NotNull
private String refreshToken;
@NotNull
private int expires_in;
private int expiresIn;
@NotNull
private int refresh_token_expires_in;
private int refreshTokenExpiresIn;

public KakaoToken(String memberEmail, String accessToken, String refreshToken,
int expires_in, int refresh_token_expires_in) {
int expiresIn, int refreshTokenExpiresIn) {
this.memberEmail = memberEmail;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expires_in = expires_in;
this.refresh_token_expires_in = refresh_token_expires_in;
this.expiresIn = expiresIn;
this.refreshTokenExpiresIn = refreshTokenExpiresIn;
}

protected KakaoToken() {
}

public boolean isAccessTokenExpired() {
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(expires_in));
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(expiresIn));
}

public boolean isRefreshTokenExpired() {
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(refresh_token_expires_in));
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(refreshTokenExpiresIn));
}

public String getAccessToken() {
Expand All @@ -65,11 +65,11 @@ public String getMemberEmail() {
}

public void updateKakaoToken(String accessToken, String refreshToken,
int expires_in, int refresh_token_expires_in) {
int expiresIn, int refreshTokenExpiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expires_in = expires_in;
this.refresh_token_expires_in = refresh_token_expires_in;
this.expiresIn = expiresIn;
this.refreshTokenExpiresIn = refreshTokenExpiresIn;
this.issuedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.sinitto.auth.exception;

public class KakaoEmailNotFoundException extends RuntimeException {
public KakaoEmailNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.example.sinitto.auth.dto.KakaoTokenResponse;
import com.example.sinitto.auth.dto.KakaoUserResponse;
import com.example.sinitto.auth.exception.KakaoEmailNotFoundException;
import com.example.sinitto.common.properties.KakaoProperties;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
Expand All @@ -14,6 +15,8 @@
@Service
public class KakaoApiService {

private static final String KAKAO_AUTH_BASE_URL = "https://kauth.kakao.com/oauth";
private static final String KAKAO_API_BASE_URL = "https://kapi.kakao.com/v2/user";
private final RestTemplate restTemplate;
private final KakaoProperties kakaoProperties;

Expand All @@ -23,14 +26,15 @@ public KakaoApiService(RestTemplate restTemplate, KakaoProperties kakaoPropertie
}

public String getAuthorizationUrl() {
return "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id="
return KAKAO_AUTH_BASE_URL + "/authorize?response_type=code&client_id="
+ kakaoProperties.clientId() + "&redirect_uri=" + kakaoProperties.redirectUri();
}

public KakaoTokenResponse getAccessToken(String authorizationCode) {
String url = "https://kauth.kakao.com/oauth/token";
String url = KAKAO_AUTH_BASE_URL + "/token";
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);

LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", kakaoProperties.clientId());
Expand All @@ -47,8 +51,7 @@ public KakaoTokenResponse getAccessToken(String authorizationCode) {
}

public KakaoTokenResponse refreshAccessToken(String refreshToken) {
String url = "https://kauth.kakao.com/oauth/token";

String url = KAKAO_AUTH_BASE_URL + "/token";
String body = "grant_type=refresh_token&client_id=" + kakaoProperties.clientId()
+ "&refresh_token=" + refreshToken;

Expand All @@ -64,7 +67,7 @@ public KakaoTokenResponse refreshAccessToken(String refreshToken) {
}

public KakaoUserResponse getUserInfo(String accessToken) {
String url = "https://kapi.kakao.com/v2/user/me";
String url = KAKAO_API_BASE_URL + "/me";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBearerAuth(accessToken);
Expand All @@ -77,7 +80,10 @@ public KakaoUserResponse getUserInfo(String accessToken) {
ResponseEntity<KakaoUserResponse> response = restTemplate.exchange(
url, HttpMethod.POST, request, KakaoUserResponse.class);

if (response.getBody().kakaoAccount().email() == null) {
throw new KakaoEmailNotFoundException("카카였 κ³„μ •μœΌλ‘œλΆ€ν„° 전달받은 이메일이 μ—†μŠ΅λ‹ˆλ‹€.");
}

return response.getBody();
}

}
66 changes: 32 additions & 34 deletions src/main/java/com/example/sinitto/auth/service/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
public class TokenService {
Expand All @@ -20,10 +22,12 @@ public class TokenService {
private static final long REFRESH_SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7;

private final Key secretKey;
private final RedisTemplate<String, String> redisTemplate;

public TokenService(@Value("${jwt.secret}") String secretKey) {
public TokenService(@Value("${jwt.secret}") String secretKey, RedisTemplate<String, String> redisTemplate) {
byte[] decodedKey = Base64.getDecoder().decode(secretKey);
this.secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256");
this.redisTemplate = redisTemplate;
}

public String generateAccessToken(String email) {
Expand All @@ -36,51 +40,45 @@ public String generateAccessToken(String email) {
}

public String generateRefreshToken(String email) {
return Jwts.builder()
String refreshToken = Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_SEVEN_DAYS))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();

redisTemplate.opsForValue().set(email, refreshToken, REFRESH_SEVEN_DAYS, TimeUnit.MILLISECONDS);
return refreshToken;
}


public String extractEmail(String token) {
try {
var claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();

if (claims.getExpiration().before(new Date())) {
throw new JWTExpirationException("μ—‘μ„ΈμŠ€ 토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
}

return claims.getSubject();
} catch (Exception e) {
throw new UnauthorizedException("μœ νš¨ν•˜μ§€ μ•Šμ€ μ—‘μ„ΈμŠ€ ν† ν°μž…λ‹ˆλ‹€.");
var claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();

if (claims.getExpiration().before(new Date())) {
throw new JWTExpirationException("토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. 재둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.");
}

return claims.getSubject();
}

public TokenResponse refreshAccessToken(String refreshToken) {
try {
var claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(refreshToken)
.getBody();

if (claims.getExpiration().before(new Date())) {
throw new JWTExpirationException("λ¦¬ν”„λ ˆμ‰¬ 토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
}

String newAccessToken = generateAccessToken(claims.getSubject());
String newRefreshToken = generateRefreshToken(claims.getSubject());

return new TokenResponse(newAccessToken, newRefreshToken);
} catch (Exception e) {
throw new UnauthorizedException("μœ νš¨ν•˜μ§€ μ•Šμ€ λ¦¬ν”„λ ˆμ‰¬ ν† ν°μž…λ‹ˆλ‹€.");
String email = extractEmail(refreshToken);

String storedRefreshToken = redisTemplate.opsForValue().get(email);
if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
throw new UnauthorizedException("λ§Œλ£Œλ˜κ±°λ‚˜ 이미 ν•œλ²ˆ μ‚¬μš©λœ λ¦¬ν”„λ ˆμ‰¬ ν† ν°μž…λ‹ˆλ‹€. 재둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.");
}
}

redisTemplate.delete(email);

String newAccessToken = generateAccessToken(email);
String newRefreshToken = generateRefreshToken(email);

return new TokenResponse(newAccessToken, newRefreshToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ public ResponseEntity<Page<CallbackResponse>> getCallbackList(@MemberId Long mem
return ResponseEntity.ok(callbackService.getCallbacks(memberId, pageable));
}

@Operation(summary = "콜백 μ „ν™” μ™„λ£Œ", description = "μ‹œλ‹ˆλ˜μ™€ μ‹œλ‹ˆμ–΄μ˜ 연락이 끝났을 λ•Œ μ‹œλ‹ˆμ–΄μ˜ μš”μ²­μ‚¬ν•­μ„ μˆ˜ν–‰ μ—¬λΆ€λ₯Ό μ„ νƒν•˜μ—¬ μ²˜λ¦¬ν•©λ‹ˆλ‹€.")
@Operation(summary = "진행 μƒνƒœμΈ μ½œλ°±μ„ μ™„λ£Œ λŒ€κΈ° μƒνƒœλ‘œ μ „ν™˜(μ‹œλ‹ˆλ˜κ°€)", description = "μ‹œλ‹ˆλ˜κ°€ μˆ˜λ½ν•œ 콜백 μˆ˜ν–‰μ„ μ™„λ£Œν–ˆμ„λ•Œ 이 api ν˜ΈμΆœν•˜λ©΄ μ™„λ£Œ λŒ€κΈ° μƒνƒœλ‘œ λ³€ν•©λ‹ˆλ‹€.")
@PutMapping("/pendingComplete/{callbackId}")
public ResponseEntity<Void> pendingCompleteCallback(@MemberId Long memberId,
@PathVariable Long callbackId) {

callbackService.pendingComplete(memberId, callbackId);
return ResponseEntity.ok().build();
}

@Operation(summary = "μ™„λ£Œ λŒ€κΈ° μƒνƒœμΈ μ½œλ°±μ„ μ™„λ£Œ μƒνƒœλ‘œ μ „ν™˜(λ³΄ν˜Έμžκ°€)", description = "λ³΄ν˜Έμžκ°€ μ™„λ£Œ λŒ€κΈ° μƒνƒœμΈ μ½œλ°±μ„ μ™„λ£Œ ν™•μ • μ‹œν‚΅λ‹ˆλ‹€.")
@PutMapping("/complete/{callbackId}")
public ResponseEntity<Void> completeCallback(@MemberId Long memberId,
@PathVariable Long callbackId) {
Expand Down Expand Up @@ -63,4 +72,11 @@ public ResponseEntity<String> addCallCheck(@RequestParam("From") String fromNumb

return ResponseEntity.ok(callbackService.add(fromNumber));
}

@Operation(summary = "μ‹œλ‹ˆλ˜μ—κ²Œ ν˜„μž¬ ν• λ‹Ήλœ 콜백 쑰회", description = "ν˜„μž¬ μ‹œλ‹ˆλ˜ λ³ΈμΈμ—κ²Œ ν• λ‹Ήλœ μ½œλ°±μ„ μ‘°νšŒν•©λ‹ˆλ‹€.")
@GetMapping("/sinitto/accepted")
public ResponseEntity<CallbackResponse> getAcceptedCallback(@MemberId Long memberId) {

return ResponseEntity.ok(callbackService.getAcceptedCallback(memberId));
}
}
Loading

0 comments on commit f84bbda

Please sign in to comment.