Skip to content

Commit

Permalink
infra: 1차 배포 (#17)
Browse files Browse the repository at this point in the history
* feat: 소속 단위 선거 기본 정보 추가 기능 구현

s3 업로드 기능 global로 수정 필요

* feat: 소속 단위 선거 기본 정보 등록용 엔티티 추가

* 온프레미스 환경 회원가입/로그인 코드 추가 (#9)

* feat: refreshtoken 저장 엔터티 추가

* feat: 패스워드 암호화 빈 추가

* feat: 회원 가입 로직 구현

* feat: 로그인 필터 추가

* feat: 온프레미스 서버 환경 로그인 기능 추가 (#10)

* feat: refreshtoken 저장 엔터티 추가

* feat: 패스워드 암호화 빈 추가

* feat: 회원 가입 로직 구현

* feat: 로그인 필터 추가

* feat: UserDetailsService, dto 구현

* feat: 서울 기준 시 설정

* feat: JWT 발급 코드 추가

* feat: JWTFilter 추가

* feat: cors 설정 추가

* feat: userdetailsservice 예외처리 추가

* fix: url length 추가

length가 255이상 인 경우 오류 가능 수정

* fix: dto추가

requestparam dto로 수정

* feat:선거기본정보 에러코드 추가

* fix: validation, lombok, dto 추가

validation, lombok, dto 추가

* docs: 회원가입 api 스웨거 작성 (#12)

* feat: refreshtoken 저장 엔터티 추가

* feat: 패스워드 암호화 빈 추가

* feat: 회원 가입 로직 구현

* feat: 로그인 필터 추가

* feat: UserDetailsService, dto 구현

* feat: 서울 기준 시 설정

* feat: JWT 발급 코드 추가

* feat: JWTFilter 추가

* feat: cors 설정 추가

* feat: userdetailsservice 예외처리 추가

* docs: 회원가입 api 스웨거 작성

* feat: 필터기반 로그인 -> 수동 인증 로직으로 수정 (#13)

* feat: refreshtoken 저장 엔터티 추가

* feat: 패스워드 암호화 빈 추가

* feat: 회원 가입 로직 구현

* feat: 로그인 필터 추가

* feat: UserDetailsService, dto 구현

* feat: 서울 기준 시 설정

* feat: JWT 발급 코드 추가

* feat: JWTFilter 추가

* feat: cors 설정 추가

* feat: userdetailsservice 예외처리 추가

* docs: 회원가입 api 스웨거 작성

* feat: 필터기반 로그인 -> 수동 인증 로직으로 수정

* feat: refreshToken 저장 추가 (#14)

* feat: refreshtoken 저장 엔터티 추가

* feat: 패스워드 암호화 빈 추가

* feat: 회원 가입 로직 구현

* feat: 로그인 필터 추가

* feat: UserDetailsService, dto 구현

* feat: 서울 기준 시 설정

* feat: JWT 발급 코드 추가

* feat: JWTFilter 추가

* feat: cors 설정 추가

* feat: userdetailsservice 예외처리 추가

* docs: 회원가입 api 스웨거 작성

* feat: 필터기반 로그인 -> 수동 인증 로직으로 수정

* feat: refreshToken 저장 추가

* feat: 회원가입 요청한 유저의 목록 조회 기능 구현 (#16)

* feat:s3 bean추가

* fix:import수정

* infra: cicd 연결 테스트

---------

Co-authored-by: ssm00 <[email protected]>
Co-authored-by: ssm00 <[email protected]>
Co-authored-by: ssm00 <[email protected]>
  • Loading branch information
4 people authored Dec 18, 2024
1 parent f87fd05 commit a06f15f
Show file tree
Hide file tree
Showing 35 changed files with 1,052 additions and 4 deletions.
82 changes: 82 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Java CI with Gradle

# 동작 조건 설정 : main 브랜치에 push 혹은 pull request가 발생할 경우 동작한다.
on:
push:
branches: [ "main" ]

permissions:
contents: read

jobs:
# Spring Boot 애플리케이션을 빌드하여 도커허브에 푸시하는 과정
build-docker-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# 1. Java 21 세팅
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'

## create application-database.yaml
- name: create application.properties file
run: |
mkdir ./src/main/resources
touch ./src/main/resources/application.yml
echo "${{ secrets.APPLICATION_YML }}" >> src/main/resources/application.yml
## create firebase-service-key.json
- name: create firebase_service_key.json file
run: |
mkdir ./src/main/resources/firebase
touch ./src/main/resources/firebase/firebase_service_key.json
echo "${{ secrets.FIREBASE_SERVICE_KEY }}" > src/main/resources/firebase/firebase_service_key.json
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash

# 2. Spring Boot 애플리케이션 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: clean bootJar

# 3. Docker 이미지 빌드
- name: docker image build
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/studentvote .

# 4. DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

# 5. Docker Hub 이미지 푸시
- name: docker Hub push
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/studentvote

run-docker-image-on-ec2:
needs: build-docker-image
runs-on: self-hosted

steps:
# 1. 최신 이미지를 풀받습니다
- name: docker pull
run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/studentvote

# 2. 기존의 컨테이너를 중지시킵니다
- name: docker stop container
run: sudo docker stop $(sudo docker ps -q) 2>/dev/null || true

# 3. 최신 이미지를 컨테이너화하여 실행시킵니다
- name: docker run new container
run: sudo docker run --name studentvote --rm -d -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/studentvote

# 4. 미사용 이미지를 정리합니다
- name: delete old docker image
run: sudo docker system prune -f
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM openjdk:21

ARG JAR_FILE=build/libs/*.jar

COPY ${JAR_FILE} app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app.jar"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.studentvote.domain.admin.application;

import com.studentvote.domain.admin.dto.response.AccountsWaitingForApprovalResponse;
import com.studentvote.domain.auth.dto.response.CustomUserDetails;
import com.studentvote.domain.user.domain.ApprovalStatus;
import com.studentvote.domain.user.domain.User;
import com.studentvote.domain.user.domain.repository.UserRepository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AdminService {

private static final String MASTER_USERNAME = "ADMIN";

private final UserRepository userRepository;

public List<AccountsWaitingForApprovalResponse> getAccountsWaitingForApproval(CustomUserDetails userDetails) {

System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
if (!userDetails.getUsername().equals(MASTER_USERNAME)) {
throw new IllegalArgumentException("접근 권한이 없습니다.");
}

Optional<List<User>> allByApprovalStatus = userRepository.findAllByApprovalStatus(ApprovalStatus.PENDING);

if (!allByApprovalStatus.isPresent()) {
return new ArrayList<>();
}

List<AccountsWaitingForApprovalResponse> accountsWaitingForApprovalResponses = allByApprovalStatus.get()
.stream()
.map(user -> new AccountsWaitingForApprovalResponse(
user.getId(),
user.getUsername(),
"",
user.getCreatedAt()
))
.collect(Collectors.toList());

return accountsWaitingForApprovalResponses;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.studentvote.domain.admin.dto.response;

import java.time.LocalDateTime;

public record AccountsWaitingForApprovalResponse(
Long userId,
String username,
String department,
LocalDateTime requestDate
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.studentvote.domain.admin.presentation;

import com.studentvote.domain.admin.application.AdminService;
import com.studentvote.domain.auth.dto.response.CustomUserDetails;
import com.studentvote.global.payload.ResponseCustom;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Admin", description = "Admin API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/admin")
public class AdminController {

private final AdminService adminService;

@Operation(summary = "가입 승인 대기 계정 조회", description = "가입 승인 대기 중인 계정 목록을 마스터 계정이 조회합니다.")
@GetMapping
public ResponseCustom<?> getAccountsWaitingForApproval(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
return ResponseCustom.OK(adminService.getAccountsWaitingForApproval(userDetails));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.studentvote.domain.auth.application;

import com.studentvote.domain.auth.domain.Token;
import com.studentvote.domain.auth.domain.repository.TokenRepository;
import com.studentvote.domain.auth.dto.request.SignInRequest;
import com.studentvote.domain.auth.dto.request.SignUpRequest;
import com.studentvote.domain.auth.dto.response.SignInResponse;
import com.studentvote.domain.auth.exception.AlreadyExistIdException;
import com.studentvote.domain.auth.exception.EmailNotFoundException;
import com.studentvote.domain.auth.exception.InvalidPasswordException;
import com.studentvote.domain.user.domain.Role;
import com.studentvote.domain.user.domain.User;
import com.studentvote.domain.user.domain.repository.UserRepository;
import com.studentvote.global.config.security.jwt.JWTUtil;
import com.studentvote.global.payload.Message;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class AuthService {

private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final BCryptPasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final TokenRepository tokenRepository;

private final JWTUtil jwtUtil;

@Transactional
public Message signUp(SignUpRequest signUpRequest) {
String email = signUpRequest.email();
String password = signUpRequest.password();
String name = signUpRequest.name();

Boolean isExist = userRepository.existsByEmail(email);

if (isExist) {
throw new AlreadyExistIdException();
}

User user = User.of(email, bCryptPasswordEncoder.encode(password), name);

userRepository.save(user);

return Message
.builder()
.message("회원가입이 완료되었습니다.")
.build();
}

@Transactional
public SignInResponse signIn(SignInRequest signInRequest) {
String email = signInRequest.email();
String password = signInRequest.password();

User user = userRepository.findByEmail(email)
.orElseThrow(() -> new EmailNotFoundException());

if (!passwordEncoder.matches(password, user.getPassword())) {
throw new InvalidPasswordException();
}

UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
email, password, null);

Authentication authenticate = authenticationManager.authenticate(authToken);

SecurityContextHolder.getContext().setAuthentication(authenticate);

Role role = user.getRole();

String accessToken = jwtUtil.createJwt(email, role.toString(), 36000000000L);
String refreshToken = jwtUtil.createJwt(email, role.toString(), 360000000000L);

tokenRepository.save(Token.builder().email(email).refreshToken(refreshToken).build());

SignInResponse signInResponse = SignInResponse
.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.role(role)
.build();

return signInResponse;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.studentvote.domain.auth.application;

import com.studentvote.domain.auth.dto.response.CustomUserDetails;
import com.studentvote.domain.auth.exception.EmailNotFoundException;
import com.studentvote.domain.user.domain.User;
import com.studentvote.domain.user.domain.repository.UserRepository;
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;

@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new EmailNotFoundException());


if (user != null) {
return new CustomUserDetails(user);
}

throw new UsernameNotFoundException(username);
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/studentvote/domain/auth/domain/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.studentvote.domain.auth.domain;

import com.studentvote.domain.common.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "token")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Token extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

@Column(name = "email", nullable = false)
private String email;

@Column(name = "refresh_token", nullable = false)
private String refreshToken;

public Token updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}

@Builder
public Token(String email, String refreshToken) {
this.email = email;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.studentvote.domain.auth.domain.repository;

import com.studentvote.domain.auth.domain.Token;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TokenRepository extends JpaRepository<Token, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.studentvote.domain.auth.dto.request;

public record SignInRequest(
String email,
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.studentvote.domain.auth.dto.request;

public record SignUpRequest(
String email,
String password,
String name
) {
}
Loading

0 comments on commit a06f15f

Please sign in to comment.