Skip to content

Commit

Permalink
Merge branch 'main' into feat/#127
Browse files Browse the repository at this point in the history
  • Loading branch information
Cyma-s authored Aug 9, 2023
2 parents 7bd679a + f71c43b commit ef58544
Show file tree
Hide file tree
Showing 183 changed files with 6,170 additions and 1,297 deletions.
22 changes: 10 additions & 12 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@ name: Backend CI
# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정
on:
pull_request:
branches: [ main ] # main branch로 pull request될 때 실행됩니다.
branches: [main] # main branch로 pull request될 때 실행됩니다.
types: [opened, synchronize, reopened]
paths:
- "backend/**"


defaults:
run:
working-directory: backend

# workflow는 한개 이상의 job을 가지며, 각 job은 여러 step에 따라 단계를 나눌 수 있습니다.
jobs:
build:
build:
name: CI
# 해당 jobs에서 아래의 steps들이 어떠한 환경에서 실행될 것인지를 지정합니다.(우리가 확인하고 싶은 것은 build가 되는 것과 test를 통과하는 여부를 알고 싶음)
runs-on: ubuntu-latest

steps:
# 작업에서 액세스할 수 있도록 $GITHUB_WORKSPACE에서 저장소를 체크아웃합니다.
- name: Pull Repository
uses: actions/checkout@v3
# 작업에서 액세스할 수 있도록 $GITHUB_WORKSPACE에서 저장소를 체크아웃합니다.
- name: Pull Repository
uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v2 # 자바를 설치하는 명렁어
with:
Expand All @@ -34,12 +32,12 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash

- name: Build with Gradle
run: ./gradlew build
shell: bash
- name: Comment on test result

- name: Comment on test result
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/backend-dev-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Backend Develop Deploy (CD)

on:
workflow_dispatch:
inputs:
branch:
description: 'Branch Name'
required: true

jobs:
build:
name: Backend Deploy
runs-on: shook-runner

steps:
- name: Log pwd
shell: bash
run: pwd
- name: Log Branch Name
shell: bash
run: echo "${{ github.event.inputs.branch }}"
- name: Deploy
shell: bash
run: bash /home/ubuntu/deploy.sh ${{ github.event.inputs.branch }}
61 changes: 61 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Frontend CI

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]

jobs:
test:
runs-on: ubuntu-latest
env:
working-directory: ./frontend
node-version: '18.16.1'

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Use Node.js ${{ env.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ env.node-version }}

- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
working-directory: ${{ env.working-directory }}

- name: ESLint
if: always()
run: npm run lint
working-directory: ${{ env.working-directory }}

- name: Test
if: always()
run: npm test
working-directory: ${{ env.working-directory }}

- name: Build
if: always()
run: npm run build
working-directory: ${{ env.working-directory }}

- name: Notify slack on CI fail
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: 프론트엔드 테스트 실패 알림
fields: repo, message, commit, author, action, eventName, ref, workflow, job, took
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
7 changes: 7 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// JWT Dependency
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

// WebClient Dependency
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package shook.shook.auth.jwt.application;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import shook.shook.auth.jwt.exception.TokenException;

@Component
public class TokenProvider {

private final long accessTokenValidTime;
private final long refreshTokenValidTime;
private final Key secretKey;

public TokenProvider(
@Value("${jwt.access-token-valid-time}") final long accessTokenValidTime,
@Value("${jwt.refresh-token-valid-time}") final long refreshTokenValidTime,
@Value("${jwt.secret-code}") final String secretCode
) {
this.accessTokenValidTime = accessTokenValidTime;
this.refreshTokenValidTime = refreshTokenValidTime;
this.secretKey = generateSecretKey(secretCode);
}

private Key generateSecretKey(final String secretCode) {
final String encodedSecretCode = Base64.getEncoder().encodeToString(secretCode.getBytes());
return Keys.hmacShaKeyFor(encodedSecretCode.getBytes());
}

public String createAccessToken(final long memberId) {
return createToken(memberId, accessTokenValidTime);
}

public String createRefreshToken(final long memberId) {
return createToken(memberId, refreshTokenValidTime);
}

private String createToken(final long memberId, final long validTime) {
final Claims claims = Jwts.claims().setSubject("user");
claims.put("memberId", memberId);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + validTime))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

public Claims parseClaims(final String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (MalformedJwtException e) {
throw new TokenException.NotIssuedTokenException();
} catch (ExpiredJwtException e) {
throw new TokenException.ExpiredTokenException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package shook.shook.auth.jwt.exception;

public class TokenException extends RuntimeException {

public static class NotIssuedTokenException extends TokenException {

public NotIssuedTokenException() {
super();
}
}

public static class ExpiredTokenException extends TokenException {

public ExpiredTokenException() {
super();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package shook.shook.auth.oauth.application;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import shook.shook.auth.oauth.application.dto.GoogleAccessTokenRequest;
import shook.shook.auth.oauth.application.dto.GoogleAccessTokenResponse;
import shook.shook.auth.oauth.application.dto.GoogleMemberInfoResponse;
import shook.shook.auth.oauth.exception.OAuthException;

@RequiredArgsConstructor
@Component
public class GoogleInfoProvider {

private static final String TOKEN_PREFIX = "Bearer ";
private static final String GRANT_TYPE = "authorization_code";
private static final String AUTHORIZATION_HEADER = "Authorization";

@Value("${oauth2.google.access-token-url}")
private String GOOGLE_ACCESS_TOKEN_URL;

@Value("${oauth2.google.member-info-url}")
private String GOOGLE_MEMBER_INFO_URL;

@Value("${oauth2.google.client-id}")
private String GOOGLE_CLIENT_ID;

@Value("${oauth2.google.client-secret}")
private String GOOGLE_CLIENT_SECRET;

@Value("${oauth2.google.redirect-uri}")
private String LOGIN_REDIRECT_URL;

private final RestTemplate restTemplate;

public GoogleMemberInfoResponse getMemberInfo(final String accessToken) {
try {
final HttpHeaders headers = new HttpHeaders();
headers.set(AUTHORIZATION_HEADER, TOKEN_PREFIX + accessToken);
final HttpEntity<Object> request = new HttpEntity<>(headers);

final GoogleMemberInfoResponse responseEntity = restTemplate.exchange(
GOOGLE_MEMBER_INFO_URL,
HttpMethod.GET,
request,
GoogleMemberInfoResponse.class).getBody();

if (!Objects.requireNonNull(responseEntity).isVerifiedEmail()) {
throw new OAuthException.InvalidEmailException();
}

return responseEntity;
} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAccessTokenException();
} catch (HttpServerErrorException e) {
throw new OAuthException.GoogleServerException();
}
}

public GoogleAccessTokenResponse getAccessToken(final String authorizationCode) {
try {
final GoogleAccessTokenRequest googleAccessTokenRequest = new GoogleAccessTokenRequest(
authorizationCode,
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
LOGIN_REDIRECT_URL,
GRANT_TYPE);
final HttpEntity<GoogleAccessTokenRequest> request = new HttpEntity<>(
googleAccessTokenRequest);

return Objects.requireNonNull(restTemplate.postForEntity(
GOOGLE_ACCESS_TOKEN_URL,
request,
GoogleAccessTokenResponse.class).getBody());

} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAuthorizationCodeException();
} catch (HttpServerErrorException e) {
throw new OAuthException.GoogleServerException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package shook.shook.auth.oauth.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shook.shook.auth.jwt.application.TokenProvider;
import shook.shook.auth.oauth.application.dto.GoogleAccessTokenResponse;
import shook.shook.auth.oauth.application.dto.GoogleMemberInfoResponse;
import shook.shook.auth.oauth.application.dto.LoginResponse;
import shook.shook.member.application.MemberService;
import shook.shook.member.domain.Email;
import shook.shook.member.domain.Member;

@RequiredArgsConstructor
@Service
public class OAuthService {

private final MemberService memberService;
private final GoogleInfoProvider googleInfoProvider;
private final TokenProvider tokenProvider;

public LoginResponse login(final String accessCode) {
final GoogleAccessTokenResponse accessTokenResponse =
googleInfoProvider.getAccessToken(accessCode);
final GoogleMemberInfoResponse memberInfo = googleInfoProvider
.getMemberInfo(accessTokenResponse.getAccessToken());

final String userEmail = memberInfo.getEmail();
final Member member = memberService.findByEmail(new Email(userEmail))
.orElseGet(() -> memberService.register(userEmail));

final long memberId = member.getId();
final String accessToken = tokenProvider.createAccessToken(memberId);
final String refreshToken = tokenProvider.createRefreshToken(memberId);
return new LoginResponse(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package shook.shook.auth.oauth.application.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
@Getter
public class GoogleAccessTokenRequest {

private String code;
private String clientId;
private String clientSecret;
private String redirectUri;
private String grantType;
}
Loading

0 comments on commit ef58544

Please sign in to comment.