Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop To Prod #182

Merged
merged 15 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 4 additions & 1 deletion .github/workflows/showpot-dev-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jobs:
files: |
./app/src/main/resources/application-dev.yml,
./app/src/main/resources/application-cloud-dev.yml,
./app/domain/common-domain/src/main/resources/application-domain-dev.yml
./app/domain/common-domain/src/main/resources/application-domain-dev.yml,
./app/infrastructure/spotify/src/main/resources/application-spotify-dev.yml
env:
token.secret-key: ${{ secrets.TOKEN_SECRET_KEY }}
cloud.aws.credentials.accessKey: ${{ secrets.AWS_ACCESS_KEY }}
Expand All @@ -40,6 +41,8 @@ jobs:
spring.datasource.url: ${{ secrets.APPLICATION_DATASOURCE_URL_DEV }}
spring.datasource.username: ${{ secrets.APPLICATION_DATASOURCE_USERNAME }}
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}
spotify.client-id: ${{ secrets.SPOTIFY_CLIENT_ID }}
spotify.client-secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=dev
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/showpot-dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ jobs:
files:
./app/src/main/resources/application-dev.yml,
./app/src/main/resources/application-cloud-dev.yml,
./app/domain/common-domain/src/main/resources/application-domain-dev.yml
./app/domain/common-domain/src/main/resources/application-domain-dev.yml,
./app/infrastructure/spotify/src/main/resources/application-spotify-dev.yml
env:
token.secret-key: ${{ secrets.TOKEN_SECRET_KEY }}
cloud.aws.credentials.accessKey: ${{ secrets.AWS_ACCESS_KEY }}
Expand All @@ -39,6 +40,8 @@ jobs:
spring.datasource.url: ${{ secrets.APPLICATION_DATASOURCE_URL_DEV }}
spring.datasource.username: ${{ secrets.APPLICATION_DATASOURCE_USERNAME }}
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}
spotify.client-id: ${{ secrets.SPOTIFY_CLIENT_ID }}
spotify.client-secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=dev
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/showpot-prod-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
./app/src/main/resources/application-prod.yml,
./app/src/main/resources/application-cloud-prod.yml,
./app/domain/common-domain/src/main/resources/application-domain-prod.yml
./app/infrastructure/spotify/src/main/resources/application-spotify-prod.yml
env:
token.secret-key: ${{ secrets.TOKEN_SECRET_KEY }}
cloud.aws.credentials.accessKey: ${{ secrets.AWS_ACCESS_KEY }}
Expand All @@ -45,6 +46,8 @@ jobs:
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}
spring.data.redis.host: ${{ secrets.REDIS_HOST_PROD }}
spring.data.redis.port: ${{ secrets.REDIS_PORT_PROD }}
spotify.client-id: ${{ secrets.SPOTIFY_CLIENT_ID }}
spotify.client-secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=prod
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/showpot-prod-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
./app/src/main/resources/application-prod.yml,
./app/src/main/resources/application-cloud-prod.yml,
./app/domain/common-domain/src/main/resources/application-domain-prod.yml
./app/infrastructure/spotify/src/main/resources/application-spotify-prod.yml
env:
token.secret-key: ${{ secrets.TOKEN_SECRET_KEY }}
cloud.aws.credentials.accessKey: ${{ secrets.AWS_ACCESS_KEY }}
Expand All @@ -39,6 +40,8 @@ jobs:
spring.datasource.url: ${{ secrets.APPLICATION_DATASOURCE_URL_PROD }}
spring.datasource.username: ${{ secrets.APPLICATION_DATASOURCE_USERNAME }}
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}
spotify.client-id: ${{ secrets.SPOTIFY_CLIENT_ID }}
spotify.client-secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=prod
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ gradle-app.setting
### QClass ###
**/src/main/generated/

### application-cloud-local.yml
### yml ###
app/src/main/resources/application-cloud-local.yml
app/infrastructure/spotify/src/main/resources/application-spotify-local.yml

# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,macos,gradle
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# 24th-App-Team-3-BE
# Showpot

![poster](https://github.com/user-attachments/assets/8b2a3abe-dd1f-4e1d-90a5-f618029f506a)
![app](https://github.com/user-attachments/assets/32885e0a-5a3d-4b60-8320-bbd8c1dd1bd4)
![problem](https://github.com/user-attachments/assets/72213b3c-e3a6-433d-b5bb-efeaa0dda203)
![survey](https://github.com/user-attachments/assets/1593677f-2b50-4bec-8a3d-7521902c5691)

## Architecture
![image](https://github.com/user-attachments/assets/66b2897a-6ae7-4651-b71b-d373db1d84ec)

## Tech Stack

|Tech|Detail|
|:------|:---|
|Language|Java 17|
|Framework|Spring Boot 3.2.5|
|ORM|Spring Data JPA|
|DB|PostgreSQL|
|External|Docker, Redis|
|CI/CD|Github Action|
|API Docs|Swagger, Notion, Confluence|
|Others|Jira, Discord, Riido, Figma|

## Hexagonal Architecture
1 change: 1 addition & 0 deletions app/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ allprojects {
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'

implementation "org.springframework.boot:spring-boot-starter-web"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ private RequestMatcher getMatcherForUserAndAdmin() {
antMatcher(HttpMethod.GET, "/api/v1/shows/interests"),
antMatcher(HttpMethod.GET, "/api/v1/users/shows/interests/count"),
antMatcher(HttpMethod.POST, "/api/v1/shows/{showId}/interests"),
antMatcher(HttpMethod.POST, "/api/v1/shows/{showId}/uninterested"),
antMatcher(HttpMethod.POST, "/api/v1/shows/{showId}/alert"),
antMatcher(HttpMethod.GET, "/api/v1/shows/alerts"),
antMatcher(HttpMethod.GET, "/api/v1/shows/{showId}/alert/reservations"),
Expand All @@ -106,7 +107,9 @@ private RequestMatcher getMatcherForUserAndAdmin() {
antMatcher(HttpMethod.GET, "/api/v1/genres/unsubscriptions"),
antMatcher(HttpMethod.POST, "/api/v1/artists/subscribe"),
antMatcher(HttpMethod.POST, "/api/v1/artists/unsubscribe"),
antMatcher(HttpMethod.GET, "/api/v1/artists/subscriptions")
antMatcher(HttpMethod.GET, "/api/v1/artists/subscriptions"),
antMatcher(HttpMethod.GET, "/api/v1/users/notifications"),
antMatcher(HttpMethod.GET, "/api/v1/users/notifications/unread")
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.example.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record CursorApiResponse(

@Schema(description = "μ‘°νšŒν•œ λ°μ΄ν„°μ˜ Cursor Id")
Object id,

@Schema(description = "μ‘°νšŒν•œ λ°μ΄ν„°μ˜ Cursor Value")
Object value
) {

public static CursorApiResponse toCursorResponse(Object id, Object value) {
return new CursorApiResponse(id, value);
}

public static CursorApiResponse toCursorId(Object id) {
return new CursorApiResponse(id, null);
}

public static CursorApiResponse noneCursor() {
return new CursorApiResponse(null, null);
}

public static <T> T getLastElement(List<T> list) {
return list.isEmpty() ? null : list.get(list.size() - 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ public record PaginationApiResponse<T>(
@Schema(description = "λ‹€μŒ 쑰회 κ°€λŠ₯ μ—¬λΆ€")
boolean hasNext,
@Schema(description = "쑰회 데이터")
List<T> data
List<T> data,
@Schema(description = "쑰회 λ°μ΄ν„°μ˜ cursor")
CursorApiResponse cursor
) {

@Builder
public PaginationApiResponse(
List<T> data,
boolean hasNext
boolean hasNext,
CursorApiResponse cursor
) {
this(data.size(), hasNext, data);
this(data.size(), hasNext, data, cursor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.example.error;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ErrorHandler;

@Component
@Slf4j
public class RedisSubErrorHandler implements ErrorHandler {

@Override
public void handleError(Throwable t) {
log.error("Redis Sub error occurred - message : {}, cause : {} ", t.getMessage(), t.getCause());
}
}
37 changes: 28 additions & 9 deletions app/api/common-api/src/main/java/org/example/filter/JWTFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.example.security.dto.AuthenticatedUser;
import org.example.security.dto.AuthenticatedInfo;
import org.example.security.dto.UserParam;
import org.example.security.token.JWTHandler;
import org.example.security.token.TokenProcessor;
Expand All @@ -34,27 +34,46 @@ protected void doFilterInternal(
handleAccessToken(request);
}

if (request.getHeader("Refresh") != null) {
handleRefreshToken(request);
}

filterChain.doFilter(request, response);
}

private void handleAccessToken(HttpServletRequest request) {
String accessToken = jwtHandler.extractAccessToken(request);
UserParam userParam = jwtHandler.extractUserFrom(accessToken);
tokenProcessor.verifyAccessTokenBlacklist(userParam, accessToken);
saveOnSecurityContextHolder(userParam);
saveOnSecurityContextHolder(userParam, accessToken);
}

private void saveOnSecurityContextHolder(UserParam userParam, String accessToken) {
var authenticatedInfo = AuthenticatedInfo.getUserWithAccessToken(userParam, accessToken);

SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
authenticatedInfo,
null,
List.of(new SimpleGrantedAuthority(authenticatedInfo.role().getAuthority()))
)
);
}

private void handleRefreshToken(HttpServletRequest request) {
String refreshToken = jwtHandler.extractRefreshToken(request);
UserParam userParam = jwtHandler.extractUserFrom(refreshToken);
saveOnSecurityContextHolderWithRefreshToken(userParam, refreshToken);
}

private void saveOnSecurityContextHolder(UserParam userParam) {
AuthenticatedUser authenticatedUser = AuthenticatedUser.builder()
.userId(userParam.userId())
.role(userParam.role())
.build();
private void saveOnSecurityContextHolderWithRefreshToken(UserParam userParam, String refreshToken) {
var authenticatedInfo = AuthenticatedInfo.getUserWithRefreshToken(userParam, refreshToken);

SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
authenticatedUser,
authenticatedInfo,
null,
List.of(new SimpleGrantedAuthority(authenticatedUser.role().getAuthority()))
List.of(new SimpleGrantedAuthority(authenticatedInfo.role().getAuthority()))
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ public interface TokenRepository {

boolean existAccessTokenInBlacklist(UUID userId, String accessToken);

void deleteRefreshToken(UUID userId);
boolean deleteRefreshToken(UUID userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.example.security.dto;

import java.util.UUID;
import lombok.Builder;
import org.example.vo.UserRoleApiType;

@Builder
public record AuthenticatedInfo(
String accessToken,
String refreshToken,
UUID userId,
UserRoleApiType role
) {

public static AuthenticatedInfo getUserWithAccessToken(
UserParam userParam,
String accessToken
) {
return AuthenticatedInfo.builder()
.userId(userParam.userId())
.role(userParam.role())
.accessToken(accessToken)
.build();
}

public static AuthenticatedInfo getUserWithRefreshToken(
UserParam userParam,
String refreshToken
) {
return AuthenticatedInfo.builder()
.refreshToken(refreshToken)
.userId(userParam.userId())
.role(userParam.role())
.build();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ public void makeAccessTokenBlacklistAndDeleteRefreshToken(
UUID userId
) {
tokenRepository.saveBlacklistAccessToken(userId, accessToken);
tokenRepository.deleteRefreshToken(userId);

if (!tokenRepository.deleteRefreshToken(userId)) {
throw new BusinessException(TokenError.INVALID_TOKEN);
}
}

private String getExistRefreshToken(UserParam userParam) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.example.util;

import java.util.UUID;
import org.example.security.dto.AuthenticatedUser;
import org.example.security.dto.AuthenticatedInfo;

public final class ValidatorUser {

public static UUID getUserId(AuthenticatedUser user) {
if (user == null) {
public static UUID getUserId(AuthenticatedInfo info) {
if (info == null) {
return null;
}

return user.userId();
return info.userId();
}

}
4 changes: 4 additions & 0 deletions app/api/show-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ dependencies {
implementation project(":app:domain:user-domain")
implementation project(':app:api:common-api')

//redis sub
implementation 'org.springframework.data:spring-data-redis'
implementation 'io.lettuce:lettuce-core:6.3.0.RELEASE'

//testFixtures
testImplementation(testFixtures(project(":app:domain:show-domain")))
testImplementation(testFixtures(project(":app:domain:user-domain")))
Expand Down
Loading
Loading