From 6c96deb7ae2a86b2f4109d75ffcec768b5712858 Mon Sep 17 00:00:00 2001
From: mingmingmon <96719969+mingmingmon@users.noreply.github.com>
Date: Wed, 6 Nov 2024 15:41:19 +0900
Subject: [PATCH 01/10] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=ED=8C=90?=
=?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=9E=AC=EB=B6=84?=
=?UTF-8?q?=EB=A5=98=20=EC=99=84=EB=A3=8C=20(#608)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../page/clab/api/domain/community/board/domain/Board.java | 7 -------
.../api/domain/community/board/domain/BoardCategory.java | 4 ++--
2 files changed, 2 insertions(+), 9 deletions(-)
diff --git a/src/main/java/page/clab/api/domain/community/board/domain/Board.java b/src/main/java/page/clab/api/domain/community/board/domain/Board.java
index 9fb9970fc..c7ab80c84 100644
--- a/src/main/java/page/clab/api/domain/community/board/domain/Board.java
+++ b/src/main/java/page/clab/api/domain/community/board/domain/Board.java
@@ -50,10 +50,6 @@ public boolean isNotice() {
return this.category.equals(BoardCategory.NOTICE);
}
- public boolean isGraduated() {
- return this.category.equals(BoardCategory.GRADUATED);
- }
-
public boolean shouldNotifyForNewBoard(MemberDetailedInfoDto memberInfo) {
return memberInfo.isAdminRole() && this.category.equals(BoardCategory.NOTICE); // Assuming 2 is Admin role level
}
@@ -72,8 +68,5 @@ public void validateAccessPermissionForCreation(MemberDetailedInfoDto currentMem
if (this.isNotice() && !currentMemberInfo.isAdminRole()) {
throw new PermissionDeniedException("공지사항은 관리자만 작성할 수 있습니다.");
}
- if (this.isGraduated() && !currentMemberInfo.isGraduated()) {
- throw new PermissionDeniedException("졸업생 게시판은 졸업생만 작성할 수 있습니다.");
- }
}
}
diff --git a/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java b/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java
index 2783017ba..831bad896 100644
--- a/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java
+++ b/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java
@@ -9,8 +9,8 @@ public enum BoardCategory {
NOTICE("notice", "공지사항"),
FREE("free", "자유 게시판"),
- QNA("qna", "질문 게시판"),
- GRADUATED("graduated", "졸업생 게시판"),
+ DEVELOPMENT_QNA("development_qna", "개발 질문 게시판"),
+ INFORMATION_REVIEWS("information_reviews", "정보 및 후기 게시판"),
ORGANIZATION("organization", "동아리 소식");
private final String key;
From a15b0c65311a8b3c396b5e4b2e2ab16546df3449 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 Nov 2024 18:46:47 +0900
Subject: [PATCH 02/10] chore(deps): bump org.mapstruct:mapstruct-processor
from 1.6.2 to 1.6.3 (#612)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index 859bea3f6..a19aa7f55 100644
--- a/build.gradle
+++ b/build.gradle
@@ -76,7 +76,7 @@ dependencies {
// MapStruct
implementation 'org.mapstruct:mapstruct:1.6.2'
- annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.2'
+ annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
// IPInfo
From d2cbe66ff901195011b798ac16dc682bb26a25e7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 Nov 2024 18:47:21 +0900
Subject: [PATCH 03/10] chore(deps): bump org.mapstruct:mapstruct from 1.6.2 to
1.6.3 (#613)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 한관희 <85067003+limehee@users.noreply.github.com>
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index a19aa7f55..ce60c823a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -75,7 +75,7 @@ dependencies {
implementation 'org.apache.commons:commons-lang3:3.17.0' // Apache Commons Lang
// MapStruct
- implementation 'org.mapstruct:mapstruct:1.6.2'
+ implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
From 6826bada9fe7c0aab3933b9c393b9f10b79b5398 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:29:30 +0900
Subject: [PATCH 04/10] chore(deps): bump commons-io:commons-io from 2.17.0 to
2.18.0 (#622)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
build.gradle | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/build.gradle b/build.gradle
index ce60c823a..e98760a2f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -67,7 +67,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok' // 롬복
implementation 'com.google.code.gson:gson:2.11.0' // JSON 라이브러리
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' // Swagger
- implementation 'commons-io:commons-io:2.17.0' // Apache Commons IO
+ implementation 'commons-io:commons-io:2.18.0' // Apache Commons IO
implementation 'com.google.guava:guava:33.3.1-jre' // Google Core Libraries For Java
implementation 'org.springframework.boot:spring-boot-starter-mail' // Spring Mail
implementation 'com.google.zxing:core:3.4.1' // QR 코드
@@ -94,7 +94,7 @@ dependencies {
implementation 'org.apache.commons:commons-text:1.11.0' // Apache Commons Text
// Image
- implementation 'commons-io:commons-io:2.17.0'
+ implementation 'commons-io:commons-io:2.18.0'
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
implementation 'org.imgscalr:imgscalr-lib:4.2'
From 027f662187bd5107bf9cad02e935b215ce37f2a3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:30:13 +0900
Subject: [PATCH 05/10] chore(deps): bump com.slack.api:slack-app-backend from
1.44.1 to 1.44.2 (#620)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index e98760a2f..a7fbfdf72 100644
--- a/build.gradle
+++ b/build.gradle
@@ -86,7 +86,7 @@ dependencies {
// Slack
implementation 'com.slack.api:slack-api-model:1.44.1'
implementation 'com.slack.api:slack-api-client:1.44.1'
- implementation 'com.slack.api:slack-app-backend:1.44.1'
+ implementation 'com.slack.api:slack-app-backend:1.44.2'
// XSS Filter
implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1' // Lucy XSS Servlet Filter
From e1fa8bb056cd9e51e864699364cdc97418faff7f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:32:58 +0900
Subject: [PATCH 06/10] chore(deps): bump
org.springdoc:springdoc-openapi-starter-webmvc-ui from 2.6.0 to 2.7.0 (#621)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index a7fbfdf72..fb13d4b92 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,7 +66,7 @@ dependencies {
compileOnly 'org.projectlombok:lombok' // 롬복
annotationProcessor 'org.projectlombok:lombok' // 롬복
implementation 'com.google.code.gson:gson:2.11.0' // JSON 라이브러리
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' // Swagger
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' // Swagger
implementation 'commons-io:commons-io:2.18.0' // Apache Commons IO
implementation 'com.google.guava:guava:33.3.1-jre' // Google Core Libraries For Java
implementation 'org.springframework.boot:spring-boot-starter-mail' // Spring Mail
From cb487fc70cfaa194bb76e1c4f1b8789999235102 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:35:29 +0900
Subject: [PATCH 07/10] chore(deps): bump com.slack.api:slack-api-model, client
from 1.44.1 to 1.44.2 (#619)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 한관희 <85067003+limehee@users.noreply.github.com>
---
build.gradle | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/build.gradle b/build.gradle
index fb13d4b92..c336fa5e0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -84,8 +84,8 @@ dependencies {
implementation 'io.ipinfo:ipinfo-api:3.0.0' // IPInfo API
// Slack
- implementation 'com.slack.api:slack-api-model:1.44.1'
- implementation 'com.slack.api:slack-api-client:1.44.1'
+ implementation 'com.slack.api:slack-api-model:1.44.2'
+ implementation 'com.slack.api:slack-api-client:1.44.2'
implementation 'com.slack.api:slack-app-backend:1.44.2'
// XSS Filter
From 5b55cf4f2890b1766c7a9bd31c40edf2b50c802c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:35:41 +0900
Subject: [PATCH 08/10] chore(deps): bump org.springframework.boot from 3.3.5
to 3.4.0 (#618)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index c336fa5e0..7e692b19d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '3.3.5'
+ id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.6'
}
From 3ca00d5397b60fc6f65433fa0667ebd01bd73f14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?=
<85067003+limehee@users.noreply.github.com>
Date: Wed, 27 Nov 2024 23:37:34 +0900
Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20CD=20=ED=8C=8C=EC=9D=B4?=
=?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20=EB=B9=8C=EB=93=9C=20?=
=?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=EC=9D=84=20=EC=9C=84?=
=?UTF-8?q?=ED=95=9C=20Docker=20Multi-stage=20Build,=20Spring=20Layered=20?=
=?UTF-8?q?JAR=20=EB=B0=8F=20BuildKit=20=EB=8F=84=EC=9E=85=20=EC=99=84?=
=?UTF-8?q?=EB=A3=8C=20(#617)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.dockerignore | 31 ++
build.gradle | 4 +
gradle/wrapper/gradle-wrapper.properties | 2 +-
jenkins/prod/Dockerfile | 38 +-
jenkins/prod/Jenkinsfile | 507 ++++++++++-------------
jenkins/stage/Dockerfile | 38 +-
jenkins/stage/Jenkinsfile | 448 +++++++++-----------
7 files changed, 510 insertions(+), 558 deletions(-)
create mode 100644 .dockerignore
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..b1bc83f2f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,31 @@
+# Ignore Git-related files
+.git
+.gitignore
+
+# Ignore local IDE files
+.idea/
+*.iml
+*.log
+
+# Ignore build directories
+build/
+out/
+target/
+
+# Ignore Docker-related files
+Dockerfile
+docker-compose.yml
+
+# Ignore other unnecessary files/directories
+*.md
+*.tmp
+*.bak
+
+# Ignore specific directories
+cloud/
+config/
+images/
+infra/
+jenkins/
+monitoring/
+nginx/
diff --git a/build.gradle b/build.gradle
index 7e692b19d..d641c37ad 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,6 +15,10 @@ bootJar {
archivesBaseName = "clab"
archiveFileName = "clab.jar"
archiveVersion = "1.0.0"
+
+ layered {
+ enabled = true
+ }
}
java {
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 3499ded5c..4eaec4670 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/jenkins/prod/Dockerfile b/jenkins/prod/Dockerfile
index 054f01fbf..aaa3a9dde 100644
--- a/jenkins/prod/Dockerfile
+++ b/jenkins/prod/Dockerfile
@@ -1,12 +1,32 @@
-# Use the official OpenJDK 21 image from the Docker Hub
-FROM openjdk:21-jdk
+# 1. Build Stage
+FROM gradle:8.11.1-jdk21 AS build
+WORKDIR /app
-# Expose port 8080 to the outside world
-EXPOSE 8080
+# Copy Gradle files and install dependencies
+COPY build.gradle settings.gradle /app/
+RUN gradle dependencies --stacktrace
-# Copy the JAR file into the container
-COPY build/libs/clab.jar /clab.jar
+# Copy source code and build
+COPY src /app/src
+RUN gradle bootJar --no-daemon --stacktrace
-# Set the default active profile to 'stage'. Modify the 'spring.profiles.active' property to match your environment.
-# For example, use '-Dspring.profiles.active=production' for production environment.
-ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/clab.jar"]
+# Extract layers from JAR file
+RUN java -Djarmode=layertools -jar build/libs/*.jar extract \
+ && ls -l /app \
+ && ls -l /app/dependencies \
+ && ls -l /app/spring-boot-loader \
+ && ls -l /app/snapshot-dependencies \
+ && ls -l /app/application
+
+# 2. Runtime Stage
+FROM eclipse-temurin:21-jre AS runtime
+WORKDIR /app
+
+# Copy each layer
+COPY --from=build /app/dependencies/ ./
+COPY --from=build /app/spring-boot-loader/ ./
+COPY --from=build /app/snapshot-dependencies/ ./
+COPY --from=build /app/application/ ./
+
+# Run the application
+ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "org.springframework.boot.loader.launch.JarLauncher"]
diff --git a/jenkins/prod/Jenkinsfile b/jenkins/prod/Jenkinsfile
index 49ae770ab..9f0513946 100644
--- a/jenkins/prod/Jenkinsfile
+++ b/jenkins/prod/Jenkinsfile
@@ -18,7 +18,6 @@
*/
def FAILED_STAGE = ""
-def BACKUP_FILE = ""
pipeline {
agent any
@@ -35,110 +34,48 @@ pipeline {
}
withCredentials([file(credentialsId: 'members_prod_config_yml', variable: 'CONFIG_FILE')]) {
script {
- def config = readYaml(file: env.CONFIG_FILE)
-
- env.JENKINS_DOMAIN = config.'jenkins-domain'
- env.SLACK_WEBHOOK_URL = config.slack.'webhook-url'
- env.SLACK_COLOR_SUCCESS = config.slack.'color-success'
- env.SLACK_COLOR_FAILURE = config.slack.'color-failure'
-
- env.PG_USER = config.postgresql.user
- env.PG_PASSWORD = config.postgresql.password
- env.BACKUP_DIR = config.postgresql.'backup-dir'
-
- env.STAGING_USER = config.staging.'user'
- env.STAGING_HOST = config.staging.'host'
- env.STAGING_BACKUP_DIR_PATH = config.staging.'backup-dir-path'
- env.STAGING_RESTORE_BACKUP_SCRIPT_PATH = config.staging.'restore-backup-script-path'
- env.STAGING_SSH_PORT = config.staging.'ssh-port'
- env.STAGING_PG_USER = config.staging.'postgresql-user'
-
- env.DOCKER_HUB_REPO = config.dockerhub.repo
- env.DOCKER_HUB_USER = config.dockerhub.user
- env.DOCKER_HUB_PASSWORD = config.dockerhub.password
-
- env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path'
- env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path'
- env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path'
-
- env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path'
- env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path'
- env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path'
-
- env.BLUE_CONTAINER = config.containers.blue
- env.GREEN_CONTAINER = config.containers.green
- env.BLUE_URL = config.containers.'blue-url'
- env.GREEN_URL = config.containers.'green-url'
- env.IMAGE_NAME = config.containers.'image-name'
-
- env.APPLICATION_NETWORK = config.networks.application
- env.MONITORING_NETWORK = config.networks.monitoring
-
- env.PROFILE = config.spring.profile
- env.PORT_A = config.spring.'port-a'.toString()
- env.PORT_B = config.spring.'port-b'.toString()
-
- env.WHITELIST_ADMIN_USERNAME = config.admin.username
- env.WHITELIST_ADMIN_PASSWORD = config.admin.password
-
- env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}"
- env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name'
- env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name'
+ loadEnvironmentVariables(env.CONFIG_FILE)
}
}
}
}
- stage('Check Java Version') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- }
- sh 'java -version'
- }
- }
-
- stage('Get Git Change Log') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- env.GIT_CHANGELOG = getChangeLog()
- }
- }
- }
-
- stage('PostgreSQL Backup') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- BACKUP_FILE = backupPostgres()
+ stage('Concurrent Pre-Build Steps') {
+ parallel {
+ stage('Get Git Change Log') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ env.GIT_CHANGELOG = getChangeLog()
+ }
+ }
}
- }
- }
- stage('Docker Hub Login') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- dockerLogin()
+ stage('PostgreSQL Backup') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ backupPostgres()
+ }
+ }
}
- }
- }
- stage('Determine Containers') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- determineContainers()
+ stage('Docker Hub Login') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ dockerLogin()
+ }
+ }
}
- }
- }
- stage('Build Application') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- buildApplication()
+ stage('Determine Containers') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ determineContainers()
+ }
+ }
}
}
}
@@ -170,29 +107,33 @@ pipeline {
}
}
- stage('Transfer Backup to Staging') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- transferBackupToStaging(BACKUP_FILE)
+ stage('Backup Transfer and Deployment') {
+ parallel {
+ stage('Transfer Backup to Staging') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ transferBackupToStaging(BACKUP_FILE)
+ }
+ }
}
- }
- }
- stage('Restore Backup on Staging') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- restoreBackupOnStaging(BACKUP_FILE)
+ stage('Restore Backup on Staging') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ restoreBackupOnStaging(BACKUP_FILE)
+ }
+ }
}
- }
- }
- stage('Switch Traffic and Cleanup') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- switchTrafficAndCleanup()
+ stage('Switch Traffic and Cleanup') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ switchTrafficAndCleanup()
+ }
+ }
}
}
}
@@ -217,7 +158,14 @@ def sendSlackBuildNotification(String message, String color) {
def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}"
def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console"
- def payload = [
+ def payload = createSlackPayload(message, color, jobUrl, consoleOutputUrl)
+ def payloadJson = groovy.json.JsonOutput.toJson(payload)
+
+ sendHttpPostRequest(env.SLACK_WEBHOOK_URL, payloadJson)
+}
+
+def createSlackPayload(String message, String color, String jobUrl, String consoleOutputUrl) {
+ return [
blocks: [
[
type: "section",
@@ -263,17 +211,74 @@ def sendSlackBuildNotification(String message, String color) {
]
]
]
- ].findAll { it != null }
+ ]
]
]
]
+}
- withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) {
- def payloadJson = groovy.json.JsonOutput.toJson(payload)
- sh """
- curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL}
- """
- }
+def sendHttpPostRequest(String url, String payload) {
+ def CONTENT_TYPE_JSON = 'application/json'
+ def HTTP_POST = 'POST'
+
+ sh """
+ curl -X ${HTTP_POST} \\
+ -H 'Content-type: ${CONTENT_TYPE_JSON}' \\
+ --data '${payload}' \\
+ ${url}
+ """
+}
+
+def loadEnvironmentVariables(String configFile) {
+ def config = readYaml(file: configFile)
+
+ env.JENKINS_DOMAIN = config.'jenkins-domain'
+ env.SLACK_WEBHOOK_URL = config.slack.'webhook-url'
+ env.SLACK_COLOR_SUCCESS = config.slack.'color-success'
+ env.SLACK_COLOR_FAILURE = config.slack.'color-failure'
+
+ env.PG_USER = config.postgresql.user
+ env.PG_PASSWORD = config.postgresql.password
+ env.BACKUP_DIR = config.postgresql.'backup-dir'
+
+ env.STAGING_USER = config.staging.'user'
+ env.STAGING_HOST = config.staging.'host'
+ env.STAGING_BACKUP_DIR_PATH = config.staging.'backup-dir-path'
+ env.STAGING_RESTORE_BACKUP_SCRIPT_PATH = config.staging.'restore-backup-script-path'
+ env.STAGING_SSH_PORT = config.staging.'ssh-port'
+ env.STAGING_PG_USER = config.staging.'postgresql-user'
+
+ env.DOCKER_HUB_REPO = config.dockerhub.repo
+ env.DOCKER_HUB_USER = config.dockerhub.user
+ env.DOCKER_HUB_PASSWORD = config.dockerhub.password
+
+ env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path'
+ env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path'
+ env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path'
+
+ env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path'
+ env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path'
+ env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path'
+
+ env.BLUE_CONTAINER = config.containers.blue
+ env.GREEN_CONTAINER = config.containers.green
+ env.BLUE_URL = config.containers.'blue-url'
+ env.GREEN_URL = config.containers.'green-url'
+ env.IMAGE_NAME = config.containers.'image-name'
+
+ env.APPLICATION_NETWORK = config.networks.application
+ env.MONITORING_NETWORK = config.networks.monitoring
+
+ env.PROFILE = config.spring.profile
+ env.PORT_A = config.spring.'port-a'.toString()
+ env.PORT_B = config.spring.'port-b'.toString()
+
+ env.WHITELIST_ADMIN_USERNAME = config.admin.username
+ env.WHITELIST_ADMIN_PASSWORD = config.admin.password
+
+ env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}"
+ env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name'
+ env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name'
}
def getChangeLog() {
@@ -295,163 +300,106 @@ def getChangeLog() {
def backupPostgres() {
def BACKUP_FILE = "postgres_backup_${new Date().format('yyyy-MM-dd_HH-mm-ss')}.sql"
- withEnv([
- "BACKUP_DIR=${env.BACKUP_DIR}",
- "POSTGRESQL_CONTAINER_NAME=${env.POSTGRESQL_CONTAINER_NAME}",
- "PG_PASSWORD=${env.PG_PASSWORD}",
- "PG_USER=${env.PG_USER}"
- ]) {
- sh """
- echo "Backing up PostgreSQL database to ${BACKUP_DIR}/${BACKUP_FILE}..."
- echo "Executing as user: \$(whoami)"
- docker exec -e PGPASSWORD=${PG_PASSWORD} ${POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${PG_USER} > ${BACKUP_DIR}/${BACKUP_FILE}'
- """
- }
- return BACKUP_FILE
+ sh """
+ echo "Backing up PostgreSQL database to ${env.BACKUP_DIR}/${BACKUP_FILE}..."
+ docker exec -e PGPASSWORD=${env.PG_PASSWORD} ${env.POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${env.PG_USER} > ${env.BACKUP_DIR}/${BACKUP_FILE}'
+ """
}
def dockerLogin() {
- withEnv(["DOCKER_HUB_PASSWORD=${env.DOCKER_HUB_PASSWORD}", "DOCKER_HUB_USER=${env.DOCKER_HUB_USER}"]) {
- sh """
- echo "Logging in to Docker Hub..."
- echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USER} --password-stdin
- """
- }
+ sh """
+ echo "Logging in to Docker Hub..."
+ echo "${env.DOCKER_HUB_PASSWORD}" | docker login -u "${env.DOCKER_HUB_USER}" --password-stdin
+ """
}
def determineContainers() {
script {
- withEnv([
- "BLUE_CONTAINER=${env.BLUE_CONTAINER}",
- "GREEN_CONTAINER=${env.GREEN_CONTAINER}",
- "BLUE_URL=${env.BLUE_URL}",
- "GREEN_URL=${env.GREEN_URL}",
- "PORT_A=${env.PORT_A}",
- "PORT_B=${env.PORT_B}"
- ]) {
- def blueRunning = sh(script: "docker ps --filter 'name=${BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${BLUE_CONTAINER}'", returnStatus: true) == 0
- if (blueRunning) {
- env.CURRENT_CONTAINER = BLUE_CONTAINER
- env.DEPLOY_CONTAINER = GREEN_CONTAINER
- env.NEW_TARGET = GREEN_URL
- env.NEW_PORT = PORT_B
- env.OLD_PORT = PORT_A
- } else {
- env.CURRENT_CONTAINER = GREEN_CONTAINER
- env.DEPLOY_CONTAINER = BLUE_CONTAINER
- env.NEW_TARGET = BLUE_URL
- env.NEW_PORT = PORT_A
- env.OLD_PORT = PORT_B
- }
- echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}."
+ def blueRunning = sh(script: "docker ps --filter 'name=${env.BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${env.BLUE_CONTAINER}'", returnStatus: true) == 0
+ if (blueRunning) {
+ env.CURRENT_CONTAINER = env.BLUE_CONTAINER
+ env.DEPLOY_CONTAINER = env.GREEN_CONTAINER
+ env.NEW_TARGET = env.GREEN_URL
+ env.NEW_PORT = env.PORT_B
+ env.OLD_PORT = env.PORT_A
+ } else {
+ env.CURRENT_CONTAINER = env.GREEN_CONTAINER
+ env.DEPLOY_CONTAINER = env.BLUE_CONTAINER
+ env.NEW_TARGET = env.BLUE_URL
+ env.NEW_PORT = env.PORT_A
+ env.OLD_PORT = env.PORT_B
}
- }
-}
-
-def buildApplication() {
- withEnv([
- "PROFILE=${env.PROFILE}"
- ]) {
- sh """
- echo "Building application with profile ${PROFILE}..."
- ./gradlew clean build -Penv=${PROFILE} --stacktrace --info
- """
+ echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}."
}
}
def buildAndPushDockerImage() {
- withEnv([
- "DOCKER_HUB_REPO=${env.DOCKER_HUB_REPO}",
- "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}",
- "DOCKERFILE_PATH=${env.DOCKERFILE_PATH}",
- "IMAGE_NAME=${env.IMAGE_NAME}"
- ]) {
- sh """
- docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${DEPLOY_CONTAINER} .
- docker tag ${IMAGE_NAME}:${DEPLOY_CONTAINER} ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER}
- docker push ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER}
- """
- }
+ sh """
+ DOCKER_BUILDKIT=1 docker build -f ${env.DOCKERFILE_PATH} -t ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} .
+ docker tag ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER}
+ docker push ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER}
+ docker logout
+ """
}
def deployNewInstance() {
- withEnv([
- "PROFILE=${env.PROFILE}",
- "NEW_PORT=${env.NEW_PORT}",
- "APPLICATION_NETWORK=${env.APPLICATION_NETWORK}",
- "MONITORING_NETWORK=${env.MONITORING_NETWORK}",
- "EXTERNAL_SERVER_CONFIG_PATH=${env.EXTERNAL_SERVER_CONFIG_PATH}",
- "EXTERNAL_SERVER_CLOUD_PATH=${env.EXTERNAL_SERVER_CLOUD_PATH}",
- "EXTERNAL_SERVER_LOGS_PATH=${env.EXTERNAL_SERVER_LOGS_PATH}",
- "INTERNAL_SERVER_CONFIG_PATH=${env.INTERNAL_SERVER_CONFIG_PATH}",
- "INTERNAL_SERVER_CLOUD_PATH=${env.INTERNAL_SERVER_CLOUD_PATH}",
- "INTERNAL_SERVER_LOGS_PATH=${env.INTERNAL_SERVER_LOGS_PATH}",
- "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}",
- "IMAGE_NAME=${env.IMAGE_NAME}"
- ]) {
- sh """
- echo "Stopping and removing existing container if it exists"
- if docker ps | grep -q ${DEPLOY_CONTAINER}; then
- docker stop ${DEPLOY_CONTAINER}
- docker rm ${DEPLOY_CONTAINER}
- fi
-
- echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}"
- docker run -d --name ${DEPLOY_CONTAINER} \\
- -p ${NEW_PORT}:8080 \\
- --network ${APPLICATION_NETWORK} \\
- -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\
- -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\
- -v ${EXTERNAL_SERVER_LOGS_PATH}:${INTERNAL_SERVER_LOGS_PATH} \\
- -e LOG_PATH=${INTERNAL_SERVER_LOGS_PATH} \\
- -e SPRING_PROFILES_ACTIVE=${PROFILE} \\
- ${IMAGE_NAME}:${DEPLOY_CONTAINER}
-
- echo "Checking if monitoring network ${MONITORING_NETWORK} exists"
- if docker network ls --format '{{.Name}}' | grep -q '^${MONITORING_NETWORK}\$'; then
- echo "Connecting to monitoring network ${MONITORING_NETWORK}"
- docker network connect ${MONITORING_NETWORK} ${DEPLOY_CONTAINER}
- else
- echo "Monitoring network ${MONITORING_NETWORK} does not exist. Skipping connection."
- fi
-
- echo "Listing all containers"
- docker ps -a
- """
- }
+ sh """
+ echo "Stopping and removing existing container if it exists"
+ if docker ps | grep -q ${env.DEPLOY_CONTAINER}; then
+ docker stop ${env.DEPLOY_CONTAINER}
+ docker rm ${env.DEPLOY_CONTAINER}
+ fi
+
+ echo "Running new container ${env.DEPLOY_CONTAINER} with image ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER}"
+ docker run -d --name ${env.DEPLOY_CONTAINER} \\
+ -p ${env.NEW_PORT}:8080 \\
+ --network ${env.APPLICATION_NETWORK} \\
+ -v ${env.EXTERNAL_SERVER_CONFIG_PATH}:${env.INTERNAL_SERVER_CONFIG_PATH} \\
+ -v ${env.EXTERNAL_SERVER_CLOUD_PATH}:${env.INTERNAL_SERVER_CLOUD_PATH} \\
+ -v ${env.EXTERNAL_SERVER_LOGS_PATH}:${env.INTERNAL_SERVER_LOGS_PATH} \\
+ -e LOG_PATH=${env.INTERNAL_SERVER_LOGS_PATH} \\
+ -e SPRING_PROFILES_ACTIVE=${env.PROFILE} \\
+ ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER}
+
+ echo "Checking if monitoring network ${env.MONITORING_NETWORK} exists"
+ if docker network ls --format '{{.Name}}' | grep -q '^${env.MONITORING_NETWORK}\$'; then
+ echo "Connecting to monitoring network ${env.MONITORING_NETWORK}"
+ docker network connect ${env.MONITORING_NETWORK} ${env.DEPLOY_CONTAINER}
+ else
+ echo "Monitoring network ${env.MONITORING_NETWORK} does not exist. Skipping connection."
+ fi
+
+ echo "Listing all containers"
+ docker ps -a
+ """
}
def performHealthCheck() {
- withEnv([
- "WHITELIST_ADMIN_USERNAME=${env.WHITELIST_ADMIN_USERNAME}",
- "WHITELIST_ADMIN_PASSWORD=${env.WHITELIST_ADMIN_PASSWORD}"
- ]) {
- def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim()
- echo "Public IP address: ${PUBLIC_IP}"
-
- def start_time = System.currentTimeMillis()
- def timeout = start_time + 150000 // 2.5 minutes
-
- while (System.currentTimeMillis() < timeout) {
- def elapsed = (System.currentTimeMillis() - start_time) / 1000
- echo "Checking health... ${elapsed} seconds elapsed."
- def status = sh(
- script: """curl -s -u ${WHITELIST_ADMIN_USERNAME}:${WHITELIST_ADMIN_PASSWORD} \
- http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""",
- returnStatus: true
- )
- if (status == 0) {
- echo "New application started successfully after ${elapsed} seconds."
- return
- }
- sleep 5
+ def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim()
+ echo "Public IP address: ${PUBLIC_IP}"
+
+ def start_time = System.currentTimeMillis()
+ def timeout = start_time + 150000 // 2.5 minutes
+
+ while (System.currentTimeMillis() < timeout) {
+ def elapsed = (System.currentTimeMillis() - start_time) / 1000
+ echo "Checking health... ${elapsed} seconds elapsed."
+ def status = sh(
+ script: """curl -s -u ${env.WHITELIST_ADMIN_USERNAME}:${env.WHITELIST_ADMIN_PASSWORD} \
+ http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""",
+ returnStatus: true
+ )
+ if (status == 0) {
+ echo "New application started successfully after ${elapsed} seconds."
+ return
}
+ sleep 5
+ }
- if (System.currentTimeMillis() >= timeout) {
- sh "docker stop ${env.DEPLOY_CONTAINER}"
- sh "docker rm ${env.DEPLOY_CONTAINER}"
- error "Health check failed"
- }
+ if (System.currentTimeMillis() >= timeout) {
+ sh "docker stop ${env.DEPLOY_CONTAINER}"
+ sh "docker rm ${env.DEPLOY_CONTAINER}"
+ error "Health check failed"
}
}
@@ -471,30 +419,21 @@ def restoreBackupOnStaging(String BACKUP_FILE) {
}
def switchTrafficAndCleanup() {
- withEnv([
- "NEW_PORT=${env.NEW_PORT}",
- "OLD_PORT=${env.OLD_PORT}",
- "NEW_TARGET=${env.NEW_TARGET}",
- "CURRENT_CONTAINER=${env.CURRENT_CONTAINER}",
- "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}",
- "NGINX_CONTAINER_NAME=${env.NGINX_CONTAINER_NAME}"
- ]) {
- sh """
- echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}."
- docker exec ${NGINX_CONTAINER_NAME} bash -c '
- export BACKEND_URL=${NEW_TARGET}
- envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
- '
- docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/default.conf
- docker exec ${NGINX_CONTAINER_NAME} nginx -t
- docker exec ${NGINX_CONTAINER_NAME} nginx -s reload
-
- echo "Checking if current container ${CURRENT_CONTAINER} is running..."
- if docker ps | grep -q ${CURRENT_CONTAINER}; then
- docker stop ${CURRENT_CONTAINER}
- docker rm ${CURRENT_CONTAINER}
- echo "Removed old container ${CURRENT_CONTAINER}."
- fi
- """
- }
+ sh """
+ echo "Switching traffic to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}."
+ docker exec ${env.NGINX_CONTAINER_NAME} bash -c '
+ export BACKEND_URL=${env.NEW_TARGET}
+ envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/members.conf.template > /etc/nginx/conf.d/members.conf
+ '
+ docker exec ${env.NGINX_CONTAINER_NAME} sed -i 's/${env.OLD_PORT}/${env.NEW_PORT}/' /etc/nginx/conf.d/members.conf
+ docker exec ${env.NGINX_CONTAINER_NAME} nginx -t
+ docker exec ${env.NGINX_CONTAINER_NAME} nginx -s reload
+
+ echo "Checking if current container ${env.CURRENT_CONTAINER} is running..."
+ if docker ps | grep -q ${env.CURRENT_CONTAINER}; then
+ docker stop ${env.CURRENT_CONTAINER}
+ docker rm ${env.CURRENT_CONTAINER}
+ echo "Removed old container ${env.CURRENT_CONTAINER}."
+ fi
+ """
}
diff --git a/jenkins/stage/Dockerfile b/jenkins/stage/Dockerfile
index bf0236339..b7f76d4a6 100644
--- a/jenkins/stage/Dockerfile
+++ b/jenkins/stage/Dockerfile
@@ -1,12 +1,32 @@
-# Use the official OpenJDK 21 image from the Docker Hub
-FROM openjdk:21-jdk
+# 1. Build Stage
+FROM gradle:8.11.1-jdk21 AS build
+WORKDIR /app
-# Expose port 8080 to the outside world
-EXPOSE 8080
+# Copy Gradle files and install dependencies
+COPY build.gradle settings.gradle /app/
+RUN gradle dependencies --stacktrace
-# Copy the JAR file into the container
-COPY build/libs/clab.jar /clab.jar
+# Copy source code and build
+COPY src /app/src
+RUN gradle bootJar --no-daemon --build-cache --stacktrace
-# Set the default active profile to 'stage'. Modify the 'spring.profiles.active' property to match your environment.
-# For example, use '-Dspring.profiles.active=production' for production environment.
-ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=stage", "/clab.jar"]
+# Extract layers from JAR file
+RUN java -Djarmode=layertools -jar build/libs/*.jar extract \
+ && ls -l /app \
+ && ls -l /app/dependencies \
+ && ls -l /app/spring-boot-loader \
+ && ls -l /app/snapshot-dependencies \
+ && ls -l /app/application
+
+# 2. Runtime Stage
+FROM eclipse-temurin:21-jre AS runtime
+WORKDIR /app
+
+# Copy each layer
+COPY --from=build /app/dependencies/ ./
+COPY --from=build /app/spring-boot-loader/ ./
+COPY --from=build /app/snapshot-dependencies/ ./
+COPY --from=build /app/application/ ./
+
+# Run the application
+ENTRYPOINT ["java", "-Dspring.profiles.active=stage", "org.springframework.boot.loader.launch.JarLauncher"]
diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile
index 00d96cce1..d22b26d02 100644
--- a/jenkins/stage/Jenkinsfile
+++ b/jenkins/stage/Jenkinsfile
@@ -34,103 +34,48 @@ pipeline {
}
withCredentials([file(credentialsId: 'members_stage_config_yml', variable: 'CONFIG_FILE')]) {
script {
- def config = readYaml(file: env.CONFIG_FILE)
-
- env.JENKINS_DOMAIN = config.'jenkins-domain'
- env.SLACK_WEBHOOK_URL = config.slack.'webhook-url'
- env.SLACK_COLOR_SUCCESS = config.slack.'color-success'
- env.SLACK_COLOR_FAILURE = config.slack.'color-failure'
-
- env.PG_USER = config.postgresql.user
- env.PG_PASSWORD = config.postgresql.password
- env.BACKUP_DIR = config.postgresql.'backup-dir'
-
- env.DOCKER_HUB_REPO = config.dockerhub.repo
- env.DOCKER_HUB_USER = config.dockerhub.user
- env.DOCKER_HUB_PASSWORD = config.dockerhub.password
-
- env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path'
- env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path'
- env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path'
-
- env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path'
- env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path'
- env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path'
-
- env.BLUE_CONTAINER = config.containers.blue
- env.GREEN_CONTAINER = config.containers.green
- env.BLUE_URL = config.containers.'blue-url'
- env.GREEN_URL = config.containers.'green-url'
- env.IMAGE_NAME = config.containers.'image-name'
-
- env.APPLICATION_NETWORK = config.networks.application
- env.MONITORING_NETWORK = config.networks.monitoring
-
- env.PROFILE = config.spring.profile
- env.PORT_A = config.spring.'port-a'.toString()
- env.PORT_B = config.spring.'port-b'.toString()
-
- env.WHITELIST_ADMIN_USERNAME = config.admin.username
- env.WHITELIST_ADMIN_PASSWORD = config.admin.password
-
- env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}"
- env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name'
- env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name'
+ loadEnvironmentVariables(env.CONFIG_FILE)
}
}
}
}
- stage('Check Java Version') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- }
- sh 'java -version'
- }
- }
-
- stage('Get Git Change Log') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- env.GIT_CHANGELOG = getChangeLog()
- }
- }
- }
-
- stage('PostgreSQL Backup') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- backupPostgres()
+ stage('Concurrent Pre-Build Steps') {
+ parallel {
+ stage('Get Git Change Log') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ env.GIT_CHANGELOG = getChangeLog()
+ }
+ }
}
- }
- }
- stage('Docker Hub Login') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- dockerLogin()
+ stage('PostgreSQL Backup') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ backupPostgres()
+ }
+ }
}
- }
- }
- stage('Determine Containers') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- determineContainers()
+ stage('Docker Hub Login') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ dockerLogin()
+ }
+ }
}
- }
- }
- stage('Build Application') {
- steps {
- script {
- FAILED_STAGE = env.STAGE_NAME
- buildApplication()
+ stage('Determine Containers') {
+ steps {
+ script {
+ FAILED_STAGE = env.STAGE_NAME
+ determineContainers()
+ }
+ }
}
}
}
@@ -191,7 +136,14 @@ def sendSlackBuildNotification(String message, String color) {
def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}"
def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console"
- def payload = [
+ def payload = createSlackPayload(message, color, jobUrl, consoleOutputUrl)
+ def payloadJson = groovy.json.JsonOutput.toJson(payload)
+
+ sendHttpPostRequest(env.SLACK_WEBHOOK_URL, payloadJson)
+}
+
+def createSlackPayload(String message, String color, String jobUrl, String consoleOutputUrl) {
+ return [
blocks: [
[
type: "section",
@@ -237,17 +189,67 @@ def sendSlackBuildNotification(String message, String color) {
]
]
]
- ].findAll { it != null }
+ ]
]
]
]
+}
- withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) {
- def payloadJson = groovy.json.JsonOutput.toJson(payload)
- sh """
- curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL}
- """
- }
+def sendHttpPostRequest(String url, String payload) {
+ def CONTENT_TYPE_JSON = 'application/json'
+ def HTTP_POST = 'POST'
+
+ sh """
+ curl -X ${HTTP_POST} \\
+ -H 'Content-type: ${CONTENT_TYPE_JSON}' \\
+ --data '${payload}' \\
+ ${url}
+ """
+}
+
+def loadEnvironmentVariables(String configFile) {
+ def config = readYaml(file: configFile)
+
+ env.JENKINS_DOMAIN = config.'jenkins-domain'
+ env.SLACK_WEBHOOK_URL = config.slack.'webhook-url'
+ env.SLACK_COLOR_SUCCESS = config.slack.'color-success'
+ env.SLACK_COLOR_FAILURE = config.slack.'color-failure'
+
+ env.PG_USER = config.postgresql.user
+ env.PG_PASSWORD = config.postgresql.password
+ env.BACKUP_DIR = config.postgresql.'backup-dir'
+
+ env.DOCKER_HUB_REPO = config.dockerhub.repo
+ env.DOCKER_HUB_USER = config.dockerhub.user
+ env.DOCKER_HUB_PASSWORD = config.dockerhub.password
+
+ env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path'
+ env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path'
+ env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path'
+
+ env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path'
+ env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path'
+ env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path'
+
+ env.BLUE_CONTAINER = config.containers.blue
+ env.GREEN_CONTAINER = config.containers.green
+ env.BLUE_URL = config.containers.'blue-url'
+ env.GREEN_URL = config.containers.'green-url'
+ env.IMAGE_NAME = config.containers.'image-name'
+
+ env.APPLICATION_NETWORK = config.networks.application
+ env.MONITORING_NETWORK = config.networks.monitoring
+
+ env.PROFILE = config.spring.profile
+ env.PORT_A = config.spring.'port-a'.toString()
+ env.PORT_B = config.spring.'port-b'.toString()
+
+ env.WHITELIST_ADMIN_USERNAME = config.admin.username
+ env.WHITELIST_ADMIN_PASSWORD = config.admin.password
+
+ env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}"
+ env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name'
+ env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name'
}
def getChangeLog() {
@@ -269,189 +271,125 @@ def getChangeLog() {
def backupPostgres() {
def BACKUP_FILE = "postgres_backup_${new Date().format('yyyy-MM-dd_HH-mm-ss')}.sql"
- withEnv([
- "BACKUP_DIR=${env.BACKUP_DIR}",
- "POSTGRESQL_CONTAINER_NAME=${env.POSTGRESQL_CONTAINER_NAME}",
- "PG_PASSWORD=${env.PG_PASSWORD}",
- "PG_USER=${env.PG_USER}"
- ]) {
- sh """
- echo "Backing up PostgreSQL database to ${BACKUP_DIR}/${BACKUP_FILE}..."
- docker exec -e PGPASSWORD=${PG_PASSWORD} ${POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${PG_USER} > ${BACKUP_DIR}/${BACKUP_FILE}'
- """
- }
+ sh """
+ echo "Backing up PostgreSQL database to ${env.BACKUP_DIR}/${BACKUP_FILE}..."
+ docker exec -e PGPASSWORD=${env.PG_PASSWORD} ${env.POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${env.PG_USER} > ${env.BACKUP_DIR}/${BACKUP_FILE}'
+ """
}
def dockerLogin() {
- withEnv(["DOCKER_HUB_PASSWORD=${env.DOCKER_HUB_PASSWORD}", "DOCKER_HUB_USER=${env.DOCKER_HUB_USER}"]) {
- sh """
- echo "Logging in to Docker Hub..."
- echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USER} --password-stdin
- """
- }
+ sh """
+ echo "Logging in to Docker Hub..."
+ echo "${env.DOCKER_HUB_PASSWORD}" | docker login -u "${env.DOCKER_HUB_USER}" --password-stdin
+ """
}
def determineContainers() {
script {
- withEnv([
- "BLUE_CONTAINER=${env.BLUE_CONTAINER}",
- "GREEN_CONTAINER=${env.GREEN_CONTAINER}",
- "BLUE_URL=${env.BLUE_URL}",
- "GREEN_URL=${env.GREEN_URL}",
- "PORT_A=${env.PORT_A}",
- "PORT_B=${env.PORT_B}"
- ]) {
- def blueRunning = sh(script: "docker ps --filter 'name=${BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${BLUE_CONTAINER}'", returnStatus: true) == 0
- if (blueRunning) {
- env.CURRENT_CONTAINER = BLUE_CONTAINER
- env.DEPLOY_CONTAINER = GREEN_CONTAINER
- env.NEW_TARGET = GREEN_URL
- env.NEW_PORT = PORT_B
- env.OLD_PORT = PORT_A
- } else {
- env.CURRENT_CONTAINER = GREEN_CONTAINER
- env.DEPLOY_CONTAINER = BLUE_CONTAINER
- env.NEW_TARGET = BLUE_URL
- env.NEW_PORT = PORT_A
- env.OLD_PORT = PORT_B
- }
- echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}."
+ def blueRunning = sh(script: "docker ps --filter 'name=${env.BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${env.BLUE_CONTAINER}'", returnStatus: true) == 0
+ if (blueRunning) {
+ env.CURRENT_CONTAINER = env.BLUE_CONTAINER
+ env.DEPLOY_CONTAINER = env.GREEN_CONTAINER
+ env.NEW_TARGET = env.GREEN_URL
+ env.NEW_PORT = env.PORT_B
+ env.OLD_PORT = env.PORT_A
+ } else {
+ env.CURRENT_CONTAINER = env.GREEN_CONTAINER
+ env.DEPLOY_CONTAINER = env.BLUE_CONTAINER
+ env.NEW_TARGET = env.BLUE_URL
+ env.NEW_PORT = env.PORT_A
+ env.OLD_PORT = env.PORT_B
}
- }
-}
-
-def buildApplication() {
- withEnv([
- "PROFILE=${env.PROFILE}"
- ]) {
- sh """
- echo "Building application with profile ${PROFILE}..."
- ./gradlew clean build -Penv=${PROFILE} --stacktrace --info
- """
+ echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}."
}
}
def buildAndPushDockerImage() {
- withEnv([
- "DOCKER_HUB_REPO=${env.DOCKER_HUB_REPO}",
- "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}",
- "DOCKERFILE_PATH=${env.DOCKERFILE_PATH}",
- "IMAGE_NAME=${env.IMAGE_NAME}"
- ]) {
- sh """
- docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${DEPLOY_CONTAINER} .
- docker tag ${IMAGE_NAME}:${DEPLOY_CONTAINER} ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER}
- docker push ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER}
- """
- }
+ sh """
+ DOCKER_BUILDKIT=1 docker build -f ${env.DOCKERFILE_PATH} -t ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} .
+ docker tag ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER}
+ docker push ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER}
+ docker logout
+ """
}
def deployNewInstance() {
- withEnv([
- "PROFILE=${env.PROFILE}",
- "NEW_PORT=${env.NEW_PORT}",
- "APPLICATION_NETWORK=${env.APPLICATION_NETWORK}",
- "MONITORING_NETWORK=${env.MONITORING_NETWORK}",
- "EXTERNAL_SERVER_CONFIG_PATH=${env.EXTERNAL_SERVER_CONFIG_PATH}",
- "EXTERNAL_SERVER_CLOUD_PATH=${env.EXTERNAL_SERVER_CLOUD_PATH}",
- "EXTERNAL_SERVER_LOGS_PATH=${env.EXTERNAL_SERVER_LOGS_PATH}",
- "INTERNAL_SERVER_CONFIG_PATH=${env.INTERNAL_SERVER_CONFIG_PATH}",
- "INTERNAL_SERVER_CLOUD_PATH=${env.INTERNAL_SERVER_CLOUD_PATH}",
- "INTERNAL_SERVER_LOGS_PATH=${env.INTERNAL_SERVER_LOGS_PATH}",
- "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}",
- "IMAGE_NAME=${env.IMAGE_NAME}"
- ]) {
- sh """
- echo "Stopping and removing existing container if it exists"
- if docker ps | grep -q ${DEPLOY_CONTAINER}; then
- docker stop ${DEPLOY_CONTAINER}
- docker rm ${DEPLOY_CONTAINER}
- fi
-
- echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}"
- docker run -d --name ${DEPLOY_CONTAINER} \\
- -p ${NEW_PORT}:8080 \\
- --network ${APPLICATION_NETWORK} \\
- -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\
- -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\
- -v ${EXTERNAL_SERVER_LOGS_PATH}:${INTERNAL_SERVER_LOGS_PATH} \\
- -e LOG_PATH=${INTERNAL_SERVER_LOGS_PATH} \\
- -e SPRING_PROFILES_ACTIVE=${PROFILE} \\
- ${IMAGE_NAME}:${DEPLOY_CONTAINER}
-
- echo "Checking if monitoring network ${MONITORING_NETWORK} exists"
- if docker network ls --format '{{.Name}}' | grep -q '^${MONITORING_NETWORK}\$'; then
- echo "Connecting to monitoring network ${MONITORING_NETWORK}"
- docker network connect ${MONITORING_NETWORK} ${DEPLOY_CONTAINER}
- else
- echo "Monitoring network ${MONITORING_NETWORK} does not exist. Skipping connection."
- fi
-
- echo "Listing all containers"
- docker ps -a
- """
- }
+ sh """
+ echo "Stopping and removing existing container if it exists"
+ if docker ps | grep -q ${env.DEPLOY_CONTAINER}; then
+ docker stop ${env.DEPLOY_CONTAINER}
+ docker rm ${env.DEPLOY_CONTAINER}
+ fi
+
+ echo "Running new container ${env.DEPLOY_CONTAINER} with image ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER}"
+ docker run -d --name ${env.DEPLOY_CONTAINER} \\
+ -p ${env.NEW_PORT}:8080 \\
+ --network ${env.APPLICATION_NETWORK} \\
+ -v ${env.EXTERNAL_SERVER_CONFIG_PATH}:${env.INTERNAL_SERVER_CONFIG_PATH} \\
+ -v ${env.EXTERNAL_SERVER_CLOUD_PATH}:${env.INTERNAL_SERVER_CLOUD_PATH} \\
+ -v ${env.EXTERNAL_SERVER_LOGS_PATH}:${env.INTERNAL_SERVER_LOGS_PATH} \\
+ -e LOG_PATH=${env.INTERNAL_SERVER_LOGS_PATH} \\
+ -e SPRING_PROFILES_ACTIVE=${env.PROFILE} \\
+ ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER}
+
+ echo "Checking if monitoring network ${env.MONITORING_NETWORK} exists"
+ if docker network ls --format '{{.Name}}' | grep -q '^${env.MONITORING_NETWORK}\$'; then
+ echo "Connecting to monitoring network ${env.MONITORING_NETWORK}"
+ docker network connect ${env.MONITORING_NETWORK} ${env.DEPLOY_CONTAINER}
+ else
+ echo "Monitoring network ${env.MONITORING_NETWORK} does not exist. Skipping connection."
+ fi
+
+ echo "Listing all containers"
+ docker ps -a
+ """
}
def performHealthCheck() {
- withEnv([
- "WHITELIST_ADMIN_USERNAME=${env.WHITELIST_ADMIN_USERNAME}",
- "WHITELIST_ADMIN_PASSWORD=${env.WHITELIST_ADMIN_PASSWORD}"
- ]) {
- def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim()
- echo "Public IP address: ${PUBLIC_IP}"
-
- def start_time = System.currentTimeMillis()
- def timeout = start_time + 150000 // 2.5 minutes
-
- while (System.currentTimeMillis() < timeout) {
- def elapsed = (System.currentTimeMillis() - start_time) / 1000
- echo "Checking health... ${elapsed} seconds elapsed."
- def status = sh(
- script: """curl -s -u ${WHITELIST_ADMIN_USERNAME}:${WHITELIST_ADMIN_PASSWORD} \
- http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""",
- returnStatus: true
- )
- if (status == 0) {
- echo "New application started successfully after ${elapsed} seconds."
- return
- }
- sleep 5
+ def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim()
+ echo "Public IP address: ${PUBLIC_IP}"
+
+ def start_time = System.currentTimeMillis()
+ def timeout = start_time + 150000 // 2.5 minutes
+
+ while (System.currentTimeMillis() < timeout) {
+ def elapsed = (System.currentTimeMillis() - start_time) / 1000
+ echo "Checking health... ${elapsed} seconds elapsed."
+ def status = sh(
+ script: """curl -s -u ${env.WHITELIST_ADMIN_USERNAME}:${env.WHITELIST_ADMIN_PASSWORD} \
+ http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""",
+ returnStatus: true
+ )
+ if (status == 0) {
+ echo "New application started successfully after ${elapsed} seconds."
+ return
}
+ sleep 5
+ }
- if (System.currentTimeMillis() >= timeout) {
- sh "docker stop ${env.DEPLOY_CONTAINER}"
- sh "docker rm ${env.DEPLOY_CONTAINER}"
- error "Health check failed"
- }
+ if (System.currentTimeMillis() >= timeout) {
+ sh "docker stop ${env.DEPLOY_CONTAINER}"
+ sh "docker rm ${env.DEPLOY_CONTAINER}"
+ error "Health check failed"
}
}
def switchTrafficAndCleanup() {
- withEnv([
- "NEW_PORT=${env.NEW_PORT}",
- "OLD_PORT=${env.OLD_PORT}",
- "NEW_TARGET=${env.NEW_TARGET}",
- "CURRENT_CONTAINER=${env.CURRENT_CONTAINER}",
- "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}",
- "NGINX_CONTAINER_NAME=${env.NGINX_CONTAINER_NAME}"
- ]) {
- sh """
- echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}."
- docker exec ${NGINX_CONTAINER_NAME} bash -c '
- export BACKEND_URL=${NEW_TARGET}
- envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
- '
- docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/default.conf
- docker exec ${NGINX_CONTAINER_NAME} nginx -t
- docker exec ${NGINX_CONTAINER_NAME} nginx -s reload
-
- echo "Checking if current container ${CURRENT_CONTAINER} is running..."
- if docker ps | grep -q ${CURRENT_CONTAINER}; then
- docker stop ${CURRENT_CONTAINER}
- docker rm ${CURRENT_CONTAINER}
- echo "Removed old container ${CURRENT_CONTAINER}."
- fi
- """
- }
+ sh """
+ echo "Switching traffic to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}."
+ docker exec ${env.NGINX_CONTAINER_NAME} bash -c '
+ export BACKEND_URL=${env.NEW_TARGET}
+ envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/members.conf.template > /etc/nginx/conf.d/members.conf
+ '
+ docker exec ${env.NGINX_CONTAINER_NAME} sed -i 's/${env.OLD_PORT}/${env.NEW_PORT}/' /etc/nginx/conf.d/members.conf
+ docker exec ${env.NGINX_CONTAINER_NAME} nginx -t
+ docker exec ${env.NGINX_CONTAINER_NAME} nginx -s reload
+
+ echo "Checking if current container ${env.CURRENT_CONTAINER} is running..."
+ if docker ps | grep -q ${env.CURRENT_CONTAINER}; then
+ docker stop ${env.CURRENT_CONTAINER}
+ docker rm ${env.CURRENT_CONTAINER}
+ echo "Removed old container ${env.CURRENT_CONTAINER}."
+ fi
+ """
}
From a9ad4e546a0ffb07c8a270bfcdb9fd8cd216b109 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?=
<85067003+limehee@users.noreply.github.com>
Date: Wed, 27 Nov 2024 23:53:01 +0900
Subject: [PATCH 10/10] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?=
=?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?=
=?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?=
=?UTF-8?q?=EB=B0=8F=20=EB=A9=80=ED=8B=B0=ED=94=8C=EB=9E=AB=ED=8F=BC=20?=
=?UTF-8?q?=EC=A7=80=EC=9B=90=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20?=
=?UTF-8?q?(#615)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
build.gradle | 2 +-
.../application/service/MemberBanService.java | 13 +-
.../service/MemberUnbanService.java | 13 +-
.../service/BlacklistIpRegisterService.java | 19 +-
.../service/BlacklistIpRemoveService.java | 16 +-
.../service/BlacklistIpResetService.java | 17 +-
.../TwoFactorAuthenticationService.java | 28 +-
.../AbnormalAccessIpRemoveService.java | 12 +-
.../AbnormalAccessIpsClearService.java | 17 +-
.../service/BoardRegisterService.java | 21 +-
.../service/ApplicationApplyService.java | 13 +-
.../service/BookLoanRequestService.java | 25 +-
.../service/MemberRoleManagementService.java | 20 +-
.../service/MembershipFeeRegisterService.java | 14 +-
.../ExternalAccountLockManagementService.java | 24 +-
...xternalIpAccessMonitorRegisterService.java | 12 +-
.../CustomBasicAuthenticationFilter.java | 54 +--
.../filter/InvalidEndpointAccessFilter.java | 25 +-
.../auth/filter/JwtAuthenticationFilter.java | 27 +-
...NotificationSettingRetrieveController.java | 30 ++
.../NotificationSettingToggleController.java | 33 ++
...NotificationSettingPersistenceAdapter.java | 34 ++
.../NotificationSettingRepository.java | 9 +-
.../out/webhook/AbstractWebhookClient.java | 17 +
.../webhook/DiscordNotificationSender.java | 25 ++
.../out/webhook/DiscordWebhookClient.java | 362 +++++++++++++++++
.../out/webhook/SlackNotificationSender.java | 25 ++
.../out/webhook/SlackWebhookClient.java | 338 ++++++++++++++++
.../mapper/NotificationSettingDtoMapper.java} | 8 +-
.../notification/BoardNotificationInfo.java} | 11 +-
.../BookLoanRecordNotificationInfo.java} | 8 +-
.../MembershipFeeNotificationInfo.java} | 8 +-
.../NotificationSettingToggleRequestDto.java} | 4 +-
.../NotificationSettingResponseDto.java | 2 +-
.../event/ApplicationStartupListener.java | 23 ++
.../application}/event/NotificationEvent.java | 8 +-
.../event/NotificationListener.java | 95 +++++
.../exception/AlertTypeNotFoundException.java | 2 +-
.../in/ManageNotificationSettingUseCase.java | 11 +
.../RetrieveNotificationSettingUseCase.java | 9 +
.../port/out/NotificationSender.java | 10 +
.../out/RetrieveNotificationSettingPort.java | 13 +
.../out/UpdateNotificationSettingPort.java | 8 +
.../application/port/out/WebhookClient.java | 23 ++
.../ManageNotificationSettingService.java | 52 +++
.../RetrieveNotificationSettingService.java | 36 ++
.../service/WebhookCommonService.java | 87 +++++
.../config/NotificationConfig.java | 13 +
.../config/NotificationConfigProperties.java | 45 +++
.../domain/AlertCategory.java | 8 +
.../notificationSetting/domain/AlertType.java | 10 +
.../domain/AlertTypeConverter.java | 5 +-
.../domain/AlertTypeResolver.java | 4 +-
.../domain/ExecutivesAlertType.java | 11 +-
.../domain/GeneralAlertType.java | 17 +
.../domain/NotificationSetting.java | 2 +-
.../domain/PlatformType.java | 14 +
.../domain/SecurityAlertType.java | 29 +-
.../api/NotificationSettingController.java | 45 ---
.../NotificationSettingService.java | 61 ---
.../slack/application/SlackService.java | 82 ----
.../slack/application/SlackServiceHelper.java | 368 ------------------
.../global/common/slack/domain/AlertType.java | 8 -
.../common/slack/domain/GeneralAlertType.java | 16 -
.../slack/listener/NotificationListener.java | 28 --
.../api/global/config/SecurityConfig.java | 22 +-
.../clab/api/global/config/SlackConfig.java | 26 --
.../handler/GlobalExceptionHandler.java | 18 +-
src/main/resources/application.yml | 50 ++-
69 files changed, 1689 insertions(+), 856 deletions(-)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java
rename src/main/java/page/clab/api/global/common/{slack/dao => notificationSetting/adapter/out/persistence}/NotificationSettingRepository.java (52%)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackNotificationSender.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackWebhookClient.java
rename src/main/java/page/clab/api/global/common/{slack/dto/mapper/SlackDtoMapper.java => notificationSetting/application/dto/mapper/NotificationSettingDtoMapper.java} (51%)
rename src/main/java/page/clab/api/global/common/{slack/domain/SlackBoardInfo.java => notificationSetting/application/dto/notification/BoardNotificationInfo.java} (58%)
rename src/main/java/page/clab/api/global/common/{slack/domain/SlackBookLoanRecordInfo.java => notificationSetting/application/dto/notification/BookLoanRecordNotificationInfo.java} (69%)
rename src/main/java/page/clab/api/global/common/{slack/domain/SlackMembershipFeeInfo.java => notificationSetting/application/dto/notification/MembershipFeeNotificationInfo.java} (69%)
rename src/main/java/page/clab/api/global/common/{slack/dto/request/NotificationSettingUpdateRequestDto.java => notificationSetting/application/dto/request/NotificationSettingToggleRequestDto.java} (78%)
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting/application}/dto/response/NotificationSettingResponseDto.java (67%)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/event/ApplicationStartupListener.java
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting/application}/event/NotificationEvent.java (59%)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationListener.java
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting/application}/exception/AlertTypeNotFoundException.java (71%)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/ManageNotificationSettingUseCase.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/RetrieveNotificationSettingUseCase.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/NotificationSender.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/RetrieveNotificationSettingPort.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/UpdateNotificationSettingPort.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/WebhookClient.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/service/ManageNotificationSettingService.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/service/RetrieveNotificationSettingService.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/application/service/WebhookCommonService.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfig.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfigProperties.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertCategory.java
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertType.java
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting}/domain/AlertTypeConverter.java (88%)
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting}/domain/AlertTypeResolver.java (77%)
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting}/domain/ExecutivesAlertType.java (54%)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/domain/GeneralAlertType.java
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting}/domain/NotificationSetting.java (94%)
create mode 100644 src/main/java/page/clab/api/global/common/notificationSetting/domain/PlatformType.java
rename src/main/java/page/clab/api/global/common/{slack => notificationSetting}/domain/SecurityAlertType.java (58%)
delete mode 100644 src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java
delete mode 100644 src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java
delete mode 100644 src/main/java/page/clab/api/global/common/slack/application/SlackService.java
delete mode 100644 src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java
delete mode 100644 src/main/java/page/clab/api/global/common/slack/domain/AlertType.java
delete mode 100644 src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java
delete mode 100644 src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java
delete mode 100644 src/main/java/page/clab/api/global/config/SlackConfig.java
diff --git a/build.gradle b/build.gradle
index d641c37ad..1ee05e906 100644
--- a/build.gradle
+++ b/build.gradle
@@ -117,7 +117,7 @@ tasks.named('test') {
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
sourceSets {
- main.java.srcDirs += [ querydslDir ]
+ main.java.srcDirs += [querydslDir]
}
tasks.withType(JavaCompile).configureEach {
diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java
index d83793ba0..a7ca3a616 100644
--- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java
+++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.in.BanMemberUseCase;
@@ -11,8 +12,8 @@
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -22,7 +23,7 @@ public class MemberBanService implements BanMemberUseCase {
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 멤버를 영구적으로 차단합니다.
@@ -30,7 +31,7 @@ public class MemberBanService implements BanMemberUseCase {
* 해당 멤버의 계정 잠금 정보를 조회하고, 없으면 새로 생성합니다.
* Redis에 저장된 해당 멤버의 인증 토큰을 삭제하며, Slack에 밴 알림을 전송합니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param memberId 차단할 멤버의 ID
* @return 저장된 계정 잠금 정보의 ID
*/
@@ -58,6 +59,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) {
private void sendSlackBanNotification(HttpServletRequest request, String memberId) {
String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_BANNED, "ID: " + memberId + ", Name: " + memberName);
+ String memberBannedMessage = "ID: " + memberId + ", Name: " + memberName;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.MEMBER_BANNED, request, memberBannedMessage));
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java
index 391628d8e..1aaf67e4d 100644
--- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java
+++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.in.UnbanMemberUseCase;
@@ -10,8 +11,8 @@
import page.clab.api.domain.auth.accountLockInfo.domain.AccountLockInfo;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -20,7 +21,7 @@ public class MemberUnbanService implements UnbanMemberUseCase {
private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort;
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 차단된 멤버를 해제합니다.
@@ -28,7 +29,7 @@ public class MemberUnbanService implements UnbanMemberUseCase {
* 해당 멤버의 계정 잠금 정보를 조회하고 해제합니다.
* 해제된 정보는 저장되며, Slack에 해제 알림이 전송됩니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param memberId 해제할 멤버의 ID
* @return 업데이트된 계정 잠금 정보의 ID
*/
@@ -55,6 +56,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) {
private void sendSlackUnbanNotification(HttpServletRequest request, String memberId) {
String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_UNBANNED, "ID: " + memberId + ", Name: " + memberName);
+ String memberUnbannedMessage = "ID: " + memberId + ", Name: " + memberName;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.MEMBER_UNBANNED, request, memberUnbannedMessage));
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java
index 6694afdec..42bc014d8 100644
--- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java
+++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.dto.mapper.BlacklistIpDtoMapper;
@@ -10,8 +11,8 @@
import page.clab.api.domain.auth.blacklistIp.application.port.out.RegisterBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,17 +20,16 @@ public class BlacklistIpRegisterService implements RegisterBlacklistIpUseCase {
private final RegisterBlacklistIpPort registerBlacklistIpPort;
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final BlacklistIpDtoMapper mapper;
/**
* 지정된 IP 주소를 블랙리스트에 등록합니다.
*
* 해당 IP 주소가 이미 블랙리스트에 존재하는지 확인하고,
- * 존재하지 않을 경우 새롭게 등록합니다.
- * 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.
+ * 존재하지 않을 경우 새롭게 등록합니다. 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param requestDto 블랙리스트에 추가할 IP 주소 정보를 담은 DTO
* @return 기존에 존재하거나 새로 추가된 블랙리스트 IP 주소
*/
@@ -42,7 +42,12 @@ public String registerBlacklistIp(HttpServletRequest request, BlacklistIpRequest
.orElseGet(() -> {
BlacklistIp blacklistIp = mapper.fromDto(requestDto);
registerBlacklistIpPort.save(blacklistIp);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Added IP: " + ipAddress);
+
+ String blacklistAddedMessage = "Added IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request,
+ blacklistAddedMessage));
+
return ipAddress;
});
}
diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java
index 5de09b640..26b558adc 100644
--- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java
+++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java
@@ -2,14 +2,15 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.port.in.RemoveBlacklistIpUseCase;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -17,7 +18,7 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final RemoveBlacklistIpPort removeBlacklistIpPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 지정된 IP 주소를 블랙리스트에서 제거합니다.
@@ -25,7 +26,7 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
* 블랙리스트에 등록된 IP 주소 정보를 조회하고 해당 정보를 삭제합니다.
* 삭제가 완료되면 Slack을 통해 보안 알림이 전송됩니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param ipAddress 제거할 블랙리스트 IP 주소
* @return 삭제된 블랙리스트 IP 주소
*/
@@ -34,7 +35,12 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
public String removeBlacklistIp(HttpServletRequest request, String ipAddress) {
BlacklistIp blacklistIp = retrieveBlacklistIpPort.getByIpAddress(ipAddress);
removeBlacklistIpPort.delete(blacklistIp);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: " + ipAddress);
+
+ String blacklistRemovedMessage = "Deleted IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request,
+ blacklistRemovedMessage));
+
return blacklistIp.getIpAddress();
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java
index 342a2561f..cdcf60021 100644
--- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java
+++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java
@@ -1,17 +1,17 @@
package page.clab.api.domain.auth.blacklistIp.application.service;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.port.in.ResetBlacklistIpsUseCase;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
-
-import java.util.List;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,7 +19,7 @@ public class BlacklistIpResetService implements ResetBlacklistIpsUseCase {
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final RemoveBlacklistIpPort removeBlacklistIpPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 블랙리스트에 등록된 모든 IP 주소를 초기화합니다.
@@ -38,7 +38,12 @@ public List resetBlacklistIps(HttpServletRequest request) {
.map(BlacklistIp::getIpAddress)
.toList();
removeBlacklistIpPort.deleteAll();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: ALL");
+
+ String blacklistRemovedMessage = "Deleted IP: ALL";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request,
+ blacklistRemovedMessage));
+
return blacklistedIps;
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java b/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java
index 655a51a06..402cf4ee7 100644
--- a/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java
+++ b/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java
@@ -1,8 +1,10 @@
package page.clab.api.domain.auth.login.application.service;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountAccessLog.domain.AccountAccessResult;
@@ -21,11 +23,10 @@
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.global.auth.jwt.JwtTokenProvider;
-import page.clab.api.global.common.slack.application.SlackService;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType;
import page.clab.api.global.util.HttpReqResUtil;
-import java.util.List;
-
@Service
@RequiredArgsConstructor
@Qualifier("twoFactorAuthenticationService")
@@ -36,12 +37,14 @@ public class TwoFactorAuthenticationService implements ManageLoginUseCase {
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalRegisterAccountAccessLogUseCase externalRegisterAccountAccessLogUseCase;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
@Override
- public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto) throws LoginFailedException, MemberLockedException {
+ public LoginResult authenticate(HttpServletRequest request,
+ TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto)
+ throws LoginFailedException, MemberLockedException {
String memberId = twoFactorAuthenticationRequestDto.getMemberId();
MemberLoginInfoDto loginMember = externalRetrieveMemberUseCase.getMemberLoginInfoById(memberId);
String totp = twoFactorAuthenticationRequestDto.getTotp();
@@ -55,9 +58,11 @@ public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticat
return LoginResult.create(header, true);
}
- private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request) throws MemberLockedException, LoginFailedException {
+ private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request)
+ throws MemberLockedException, LoginFailedException {
if (!manageAuthenticatorUseCase.isAuthenticatorValid(memberId, totp)) {
- externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId, AccountAccessResult.FAILURE);
+ externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId,
+ AccountAccessResult.FAILURE);
externalManageAccountLockUseCase.handleLoginFailure(request, memberId);
throw new LoginFailedException("잘못된 인증번호입니다.");
}
@@ -67,18 +72,21 @@ private void verifyTwoFactorAuthentication(String memberId, String totp, HttpSer
private TokenInfo generateAndSaveToken(MemberLoginInfoDto memberInfo) {
TokenInfo tokenInfo = jwtTokenProvider.generateToken(memberInfo.getMemberId(), memberInfo.getRole());
String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist();
- externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo, clientIpAddress);
+ externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo,
+ clientIpAddress);
return tokenInfo;
}
private void sendAdminLoginNotification(HttpServletRequest request, MemberLoginInfoDto loginMember) {
if (loginMember.isSuperAdminRole()) {
- slackService.sendAdminLoginNotification(request, loginMember);
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, GeneralAlertType.ADMIN_LOGIN, request, loginMember));
}
}
@Override
- public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto) throws LoginFailedException, MemberLockedException {
+ public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto)
+ throws LoginFailedException, MemberLockedException {
throw new UnsupportedOperationException("Method not implemented");
}
diff --git a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java
index 7789853cb..a31e7ee8a 100644
--- a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java
+++ b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java
@@ -2,25 +2,29 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.RemoveAbnormalAccessIpUseCase;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RemoveIpAccessMonitorPort;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
public class AbnormalAccessIpRemoveService implements RemoveAbnormalAccessIpUseCase {
private final RemoveIpAccessMonitorPort removeIpAccessMonitorPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
public String removeAbnormalAccessIp(HttpServletRequest request, String ipAddress) {
removeIpAccessMonitorPort.deleteById(ipAddress);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: " + ipAddress);
+ String abnormalAccessIpDeletedMessage = "Deleted IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request,
+ abnormalAccessIpDeletedMessage));
return ipAddress;
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java
index 7a5c7dbcb..f347f2644 100644
--- a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java
+++ b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java
@@ -1,17 +1,17 @@
package page.clab.api.domain.auth.redisIpAccessMonitor.application.service;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.ClearAbnormalAccessIpsUseCase;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.ClearIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RetrieveIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.domain.RedisIpAccessMonitor;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
-
-import java.util.List;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,14 +19,19 @@ public class AbnormalAccessIpsClearService implements ClearAbnormalAccessIpsUseC
private final ClearIpAccessMonitorPort clearIpAccessMonitorPort;
private final RetrieveIpAccessMonitorPort retrieveIpAccessMonitorPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
public List clearAbnormalAccessIps(HttpServletRequest request) {
List ipAccessMonitors = retrieveIpAccessMonitorPort.findAll();
clearIpAccessMonitorPort.deleteAll();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: ALL");
+
+ String abnormalAccessIpClearedMessage = "Deleted IP: ALL";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request,
+ abnormalAccessIpClearedMessage));
+
return ipAccessMonitors;
}
}
diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java
index f2e487749..cdcd2dee8 100644
--- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java
+++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java
@@ -1,6 +1,8 @@
package page.clab.api.domain.community.board.application.service;
+import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper;
@@ -8,17 +10,16 @@
import page.clab.api.domain.community.board.application.port.in.RegisterBoardUseCase;
import page.clab.api.domain.community.board.application.port.out.RegisterBoardPort;
import page.clab.api.domain.community.board.domain.Board;
-import page.clab.api.global.common.slack.domain.SlackBoardInfo;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
import page.clab.api.global.common.file.application.UploadedFileService;
import page.clab.api.global.common.file.domain.UploadedFile;
-import page.clab.api.global.common.slack.application.SlackService;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
import page.clab.api.global.exception.PermissionDeniedException;
-import java.util.List;
-
@Service
@RequiredArgsConstructor
public class BoardRegisterService implements RegisterBoardUseCase {
@@ -27,7 +28,7 @@ public class BoardRegisterService implements RegisterBoardUseCase {
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
private final UploadedFileService uploadedFileService;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final BoardDtoMapper mapper;
/**
@@ -48,10 +49,14 @@ public String registerBoard(BoardRequestDto requestDto) throws PermissionDeniedE
Board board = mapper.fromDto(requestDto, currentMemberInfo.getMemberId(), uploadedFiles);
board.validateAccessPermissionForCreation(currentMemberInfo);
if (board.shouldNotifyForNewBoard(currentMemberInfo)) {
- externalSendNotificationUseCase.sendNotificationToMember(currentMemberInfo.getMemberId(), "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다.");
+ externalSendNotificationUseCase.sendNotificationToMember(currentMemberInfo.getMemberId(),
+ "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다.");
}
- SlackBoardInfo boardInfo = SlackBoardInfo.create(board, currentMemberInfo);
- slackService.sendNewBoardNotification(boardInfo);
+
+ BoardNotificationInfo boardInfo = BoardNotificationInfo.create(board, currentMemberInfo);
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_BOARD, null,
+ boardInfo));
+
return registerBoardPort.save(board).getCategory().getKey();
}
}
diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java
index 11c69305c..357a59cde 100644
--- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java
+++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java
@@ -1,6 +1,7 @@
package page.clab.api.domain.hiring.application.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.hiring.application.application.dto.mapper.ApplicationDtoMapper;
@@ -10,7 +11,8 @@
import page.clab.api.domain.hiring.application.domain.Application;
import page.clab.api.external.hiring.application.application.port.ExternalRetrieveRecruitmentUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
@Service
@RequiredArgsConstructor
@@ -19,9 +21,9 @@ public class ApplicationApplyService implements ApplyForApplicationUseCase {
private final RegisterApplicationPort registerApplicationPort;
private final ExternalRetrieveRecruitmentUseCase externalRetrieveRecruitmentUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final ApplicationDtoMapper mapper;
-
+
@Transactional
@Override
public String applyForClub(ApplicationRequestDto requestDto) {
@@ -30,7 +32,10 @@ public String applyForClub(ApplicationRequestDto requestDto) {
String applicationType = application.getApplicationTypeForNotificationPrefix();
externalSendNotificationUseCase.sendNotificationToAdmins(applicationType + requestDto.getStudentId() + " " +
requestDto.getName() + "님이 지원하였습니다.");
- slackService.sendNewApplicationNotification(requestDto);
+
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_APPLICATION, null,
+ requestDto));
+
return registerApplicationPort.save(application).getStudentId();
}
}
diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java
index 1e759332e..9f32d5adb 100644
--- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java
+++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java
@@ -1,6 +1,7 @@
package page.clab.api.domain.library.bookLoanRecord.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -17,8 +18,9 @@
import page.clab.api.external.library.book.application.port.ExternalRetrieveBookUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SlackBookLoanRecordInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
import page.clab.api.global.exception.CustomOptimisticLockingFailureException;
@Service
@@ -30,21 +32,19 @@ public class BookLoanRequestService implements RequestBookLoanUseCase {
private final ExternalRetrieveBookUseCase externalRetrieveBookUseCase;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 도서 대출 신청을 처리합니다.
*
* 현재 로그인한 멤버의 대출 상태와 한도를 검증한 후,
- * 도서의 대출 신청이 이미 존재하는지 확인합니다.
- * 대출 신청이 성공적으로 완료되면 멤버와 Slack에 알림을 전송하고,
- * 대출 기록을 저장한 후 그 ID를 반환합니다.
+ * 도서의 대출 신청이 이미 존재하는지 확인합니다. 대출 신청이 성공적으로 완료되면 멤버와 Slack에 알림을 전송하고, 대출 기록을 저장한 후 그 ID를 반환합니다.
*
* @param requestDto 도서 대출 신청 요청 정보 DTO
* @return 저장된 대출 기록의 ID
* @throws CustomOptimisticLockingFailureException 동시에 다른 사용자가 대출을 신청하여 충돌이 발생한 경우 예외 발생
- * @throws MaxBorrowLimitExceededException 대출 한도를 초과한 경우 예외 발생
- * @throws BookAlreadyAppliedForLoanException 이미 신청된 도서일 경우 예외 발생
+ * @throws MaxBorrowLimitExceededException 대출 한도를 초과한 경우 예외 발생
+ * @throws BookAlreadyAppliedForLoanException 이미 신청된 도서일 경우 예외 발생
*/
@Transactional
@Override
@@ -60,10 +60,13 @@ public Long requestBookLoan(BookLoanRecordRequestDto requestDto) throws CustomOp
BookLoanRecord bookLoanRecord = BookLoanRecord.create(book.getId(), borrowerInfo);
- externalSendNotificationUseCase.sendNotificationToMember(borrowerInfo.getMemberId(), "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다.");
+ externalSendNotificationUseCase.sendNotificationToMember(borrowerInfo.getMemberId(),
+ "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다.");
- SlackBookLoanRecordInfo bookLoanRecordInfo = SlackBookLoanRecordInfo.create(book, borrowerInfo);
- slackService.sendNewBookLoanRequestNotification(bookLoanRecordInfo);
+ BookLoanRecordNotificationInfo bookLoanRecordInfo = BookLoanRecordNotificationInfo.create(book,
+ borrowerInfo);
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_BOOK_LOAN_REQUEST, null,
+ bookLoanRecordInfo));
return registerBookLoanRecordPort.save(bookLoanRecord).getId();
} catch (ObjectOptimisticLockingFailureException e) {
diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java
index db2ced3c0..ceed5fb73 100644
--- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java
+++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.memberManagement.member.application.dto.request.ChangeMemberRoleRequest;
@@ -11,8 +12,8 @@
import page.clab.api.domain.memberManagement.member.application.port.out.UpdateMemberPort;
import page.clab.api.domain.memberManagement.member.domain.Member;
import page.clab.api.domain.memberManagement.member.domain.Role;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -20,11 +21,12 @@ public class MemberRoleManagementService implements ManageMemberRoleUseCase {
private final RetrieveMemberPort retrieveMemberPort;
private final UpdateMemberPort updateMemberPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Transactional
@Override
- public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId, ChangeMemberRoleRequest request) {
+ public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId,
+ ChangeMemberRoleRequest request) {
Member member = retrieveMemberPort.getById(memberId);
Role oldRole = member.getRole();
@@ -34,9 +36,13 @@ public String changeMemberRole(HttpServletRequest httpServletRequest, String mem
member.changeRole(newRole);
updateMemberPort.update(member);
- slackService.sendSecurityAlertNotification(httpServletRequest, SecurityAlertType.MEMBER_ROLE_CHANGED,
- String.format("[%s] %s: %s -> %s",
- member.getId(), member.getName(), oldRole, newRole));
+
+ String memberRoleChangedMessage = String.format("[%s] %s: %s -> %s", member.getId(), member.getName(), oldRole,
+ newRole);
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.MEMBER_ROLE_CHANGED, httpServletRequest,
+ memberRoleChangedMessage));
+
return memberId;
}
diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java
index 3d50a19e3..533b7d879 100644
--- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java
+++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java
@@ -1,6 +1,7 @@
package page.clab.api.domain.members.membershipFee.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
@@ -11,8 +12,9 @@
import page.clab.api.domain.members.membershipFee.domain.MembershipFee;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SlackMembershipFeeInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
@Service
@RequiredArgsConstructor
@@ -21,7 +23,7 @@ public class MembershipFeeRegisterService implements RegisterMembershipFeeUseCas
private final RegisterMembershipFeePort registerMembershipFeePort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final MembershipFeeDtoMapper mapper;
@Transactional
@@ -30,8 +32,10 @@ public Long registerMembershipFee(MembershipFeeRequestDto requestDto) {
MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo();
MembershipFee membershipFee = mapper.fromDto(requestDto, memberInfo.getMemberId());
externalSendNotificationUseCase.sendNotificationToAdmins("새로운 회비 내역이 등록되었습니다.");
- SlackMembershipFeeInfo membershipFeeInfo = SlackMembershipFeeInfo.create(membershipFee, memberInfo);
- slackService.sendNewMembershipFeeNotification(membershipFeeInfo);
+ MembershipFeeNotificationInfo membershipFeeInfo = MembershipFeeNotificationInfo.create(membershipFee,
+ memberInfo);
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_MEMBERSHIP_FEE, null,
+ membershipFeeInfo));
return registerMembershipFeePort.save(membershipFee).getId();
}
}
diff --git a/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java b/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java
index 4a9eb6dde..436c68313 100644
--- a/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java
+++ b/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java
@@ -3,6 +3,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.out.RegisterAccountLockInfoPort;
@@ -13,8 +14,8 @@
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto;
import page.clab.api.external.auth.accountLockInfo.application.ExternalManageAccountLockUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -23,7 +24,7 @@ public class ExternalAccountLockManagementService implements ExternalManageAccou
private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort;
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Value("${security.login-attempt.max-failures}")
private int maxLoginFailures;
@@ -39,7 +40,7 @@ public class ExternalAccountLockManagementService implements ExternalManageAccou
*
* @param memberId 잠금 해제하려는 멤버의 ID
* @throws MemberLockedException 계정이 현재 잠겨 있을 경우 예외 발생
- * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
+ * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
*/
@Transactional
@Override
@@ -55,17 +56,17 @@ public void handleAccountLockInfo(String memberId) throws MemberLockedException,
* 로그인 실패를 처리하고 계정 잠금을 관리합니다.
*
* 로그인 실패 시 멤버의 존재 여부와 계정 잠금 상태를 확인합니다.
- * 로그인 실패 횟수를 증가시키며, 설정된 최대 실패 횟수에 도달하면 계정을 잠그고
- * Slack에 보안 알림을 전송합니다.
+ * 로그인 실패 횟수를 증가시키며, 설정된 최대 실패 횟수에 도달하면 계정을 잠그고 Slack에 보안 알림을 전송합니다.
*
- * @param request 현재 HTTP 요청 객체
+ * @param request 현재 HTTP 요청 객체
* @param memberId 로그인 실패를 기록할 멤버의 ID
* @throws MemberLockedException 계정이 현재 잠겨 있을 경우 예외 발생
- * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
+ * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
*/
@Transactional
@Override
- public void handleLoginFailure(HttpServletRequest request, String memberId) throws MemberLockedException, LoginFailedException {
+ public void handleLoginFailure(HttpServletRequest request, String memberId)
+ throws MemberLockedException, LoginFailedException {
ensureMemberExists(memberId);
AccountLockInfo accountLockInfo = ensureAccountLockInfo(memberId);
validateAccountLockStatus(accountLockInfo);
@@ -99,7 +100,10 @@ private void sendSlackLoginFailureNotification(HttpServletRequest request, Strin
String memberName = memberInfo.getMemberName();
if (memberInfo.isAdminRole()) {
request.setAttribute("member", memberId + " " + memberName);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.REPEATED_LOGIN_FAILURES, "로그인 실패 횟수 초과로 계정이 잠겼습니다.");
+ String repeatedLoginFailuresMessage = "로그인 실패 횟수 초과로 계정이 잠겼습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.REPEATED_LOGIN_FAILURES, request,
+ repeatedLoginFailuresMessage));
}
}
}
diff --git a/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java b/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java
index a600c8b3d..61ac9a980 100644
--- a/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java
+++ b/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java
@@ -4,14 +4,15 @@
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RegisterIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RetrieveIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.domain.RedisIpAccessMonitor;
import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalRegisterIpAccessMonitorUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,7 +20,7 @@ public class ExternalIpAccessMonitorRegisterService implements ExternalRegisterI
private final RegisterIpAccessMonitorPort registerIpAccessMonitorPort;
private final RetrieveIpAccessMonitorPort retrieveIpAccessMonitorPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Value("${security.ip-attempt.max-attempts}")
private int maxAttempts;
@@ -29,7 +30,10 @@ public class ExternalIpAccessMonitorRegisterService implements ExternalRegisterI
public void registerIpAccessMonitor(HttpServletRequest request, String ipAddress) {
RedisIpAccessMonitor redisIpAccessMonitor = getOrCreateRedisIpAccessMonitor(ipAddress);
if (redisIpAccessMonitor.isBlocked()) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_BLOCKED, "Blocked IP: " + ipAddress);
+ String abnormalAccessIpBlockedMessage = "Blocked IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_BLOCKED, request,
+ abnormalAccessIpBlockedMessage));
}
registerIpAccessMonitorPort.save(redisIpAccessMonitor);
}
diff --git a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java
index 954d80f94..ed2287c8e 100644
--- a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java
+++ b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java
@@ -4,8 +4,11 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -15,21 +18,17 @@
import page.clab.api.external.auth.blacklistIp.application.port.ExternalRetrieveBlacklistIpUseCase;
import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalCheckIpBlockedUseCase;
import page.clab.api.global.auth.util.IpWhitelistValidator;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
import page.clab.api.global.util.HttpReqResUtil;
import page.clab.api.global.util.ResponseUtil;
import page.clab.api.global.util.WhitelistPathMatcher;
-import java.io.IOException;
-import java.util.Base64;
-
/**
* {@code CustomBasicAuthenticationFilter}는 기본 인증 필터를 확장하여 추가적인 보안 기능을 제공합니다.
*
* IP 주소 기반 접근 제한, 화이트리스트 경로 검증, 사용자 인증 정보를 바탕으로
- * Slack 보안 알림을 전송하는 기능을 포함합니다. 또한 Swagger 또는 Actuator에 대한
- * 접근이 성공하거나 실패할 경우 이를 Slack에 알립니다.
+ * Slack 보안 알림을 전송하는 기능을 포함합니다. 또한 Swagger 또는 Actuator에 대한 접근이 성공하거나 실패할 경우 이를 Slack에 알립니다.
*
* 이 필터는 다음과 같은 추가 검증을 수행합니다:
*
@@ -47,22 +46,29 @@
public class CustomBasicAuthenticationFilter extends BasicAuthenticationFilter {
private final IpWhitelistValidator ipWhitelistValidator;
- private final SlackService slackService;
private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase;
private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase;
+ private final ApplicationEventPublisher eventPublisher;
public CustomBasicAuthenticationFilter(
AuthenticationManager authenticationManager,
IpWhitelistValidator ipWhitelistValidator,
- SlackService slackService,
ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase,
- ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase
+ ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase,
+ ApplicationEventPublisher eventPublisher
) {
super(authenticationManager);
this.externalCheckIpBlockedUseCase = externalCheckIpBlockedUseCase;
this.externalRetrieveBlacklistIpUseCase = externalRetrieveBlacklistIpUseCase;
this.ipWhitelistValidator = ipWhitelistValidator;
- this.slackService = slackService;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @NotNull
+ private static String[] decodeCredentials(String authorizationHeader) {
+ String base64Credentials = authorizationHeader.substring("Basic ".length());
+ String credentials = new String(Base64.getDecoder().decode(base64Credentials));
+ return credentials.split(":", 2);
}
@Override
@@ -82,7 +88,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
super.doFilterInternal(request, response, chain);
}
- private boolean authenticateUserCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ private boolean authenticateUserCredentials(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Basic ")) {
response.setHeader("WWW-Authenticate", "Basic realm=\"Please enter your username and password\"");
@@ -122,28 +129,29 @@ private boolean verifyIpAddressAccess(HttpServletResponse response) throws IOExc
return true;
}
- @NotNull
- private static String[] decodeCredentials(String authorizationHeader) {
- String base64Credentials = authorizationHeader.substring("Basic ".length());
- String credentials = new String(Base64.getDecoder().decode(base64Credentials));
- return credentials.split(":", 2);
- }
-
private void sendAuthenticationSuccessAlertSlackMessage(HttpServletRequest request) {
String path = request.getRequestURI();
if (WhitelistPathMatcher.isSwaggerIndexEndpoint(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 허가되었습니다.");
+ String apiDocsAccessMessage = "API 문서에 대한 접근이 허가되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.API_DOCS_ACCESS, request, apiDocsAccessMessage));
} else if (WhitelistPathMatcher.isActuatorRequest(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 허가되었습니다.");
+ String actuatorAccessMessage = "Actuator에 대한 접근이 허가되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ACTUATOR_ACCESS, request, actuatorAccessMessage));
}
}
private void sendAuthenticationFailureAlertSlackMessage(HttpServletRequest request) {
String path = request.getRequestURI();
if (WhitelistPathMatcher.isSwaggerIndexEndpoint(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 거부되었습니다.");
+ String apiDocsAccessMessage = "API 문서에 대한 접근이 거부되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.API_DOCS_ACCESS, request, apiDocsAccessMessage));
} else if (WhitelistPathMatcher.isActuatorRequest(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 거부되었습니다.");
+ String actuatorAccessMessage = "Actuator에 대한 접근이 거부되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ACTUATOR_ACCESS, request, actuatorAccessMessage));
}
}
}
diff --git a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java
index b8314f9bf..5f7548fd1 100644
--- a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java
+++ b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java
@@ -6,25 +6,24 @@
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
import page.clab.api.external.auth.blacklistIp.application.port.ExternalRegisterBlacklistIpUseCase;
import page.clab.api.external.auth.blacklistIp.application.port.ExternalRetrieveBlacklistIpUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
import page.clab.api.global.util.HttpReqResUtil;
import page.clab.api.global.util.ResponseUtil;
import page.clab.api.global.util.SecurityPatternChecker;
-import java.io.IOException;
-
/**
- * {@code InvalidEndpointAccessFilter}는 서버 내부 파일 및 디렉토리에 대한 비정상적인 접근을 차단하고
- * 보안 경고를 전송하는 필터입니다.
+ * {@code InvalidEndpointAccessFilter}는 서버 내부 파일 및 디렉토리에 대한 비정상적인 접근을 차단하고 보안 경고를 전송하는 필터입니다.
*
* 특정 패턴을 통해 비정상적인 접근 시도를 탐지하며, 비정상적인 경로로 접근을 시도한 IP를
* 블랙리스트에 등록하고, Slack을 통해 보안 경고 메시지를 전송합니다.
@@ -44,13 +43,14 @@
@Slf4j
public class InvalidEndpointAccessFilter extends GenericFilterBean {
- private final SlackService slackService;
private final String fileURL;
private final ExternalRegisterBlacklistIpUseCase externalRegisterBlacklistIpUseCase;
private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase;
+ private final ApplicationEventPublisher eventPublisher;
@Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
boolean isUploadedFileAccess = path.startsWith(fileURL);
@@ -75,7 +75,8 @@ private void handleSuspiciousAccess(HttpServletRequest request, HttpServletRespo
private void logSuspiciousAccess(HttpServletRequest request, String clientIpAddress) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- String id = (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName();
+ String id =
+ (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName();
String requestUrl = request.getRequestURI();
String httpMethod = request.getMethod();
int statusCode = HttpServletResponse.SC_FORBIDDEN;
@@ -97,7 +98,9 @@ private void sendSecurityAlerts(HttpServletRequest request, String clientIpAddre
String abnormalAccessMessage = "서버 내부 파일 및 디렉토리에 대한 접근이 감지되었습니다.";
String blacklistAddedMessage = "Added IP: " + clientIpAddress;
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS, abnormalAccessMessage);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, blacklistAddedMessage);
+ eventPublisher.publishEvent(new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS, request,
+ abnormalAccessMessage));
+ eventPublisher.publishEvent(new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request,
+ blacklistAddedMessage));
}
}
diff --git a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java
index 12bda3af8..fba999b53 100644
--- a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java
+++ b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java
@@ -6,8 +6,10 @@
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
@@ -16,14 +18,12 @@
import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalCheckIpBlockedUseCase;
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.global.auth.jwt.JwtTokenProvider;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
import page.clab.api.global.util.HttpReqResUtil;
import page.clab.api.global.util.ResponseUtil;
import page.clab.api.global.util.WhitelistPathMatcher;
-import java.io.IOException;
-
/**
* {@code JwtAuthenticationFilter}는 JWT 토큰을 검증하고, IP 주소 기반 접근 제한을 수행하는 필터입니다.
*
@@ -45,14 +45,15 @@
@Slf4j
public class JwtAuthenticationFilter extends GenericFilterBean {
- private final SlackService slackService;
private final JwtTokenProvider jwtTokenProvider;
+ private final ApplicationEventPublisher eventPublisher;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase;
private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase;
@Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String path = httpServletRequest.getRequestURI();
@@ -71,7 +72,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
private boolean verifyIpAddressAccess(HttpServletResponse response, String clientIpAddress) throws IOException {
- if (externalRetrieveBlacklistIpUseCase.existsByIpAddress(clientIpAddress) || externalCheckIpBlockedUseCase.isIpBlocked(clientIpAddress)) {
+ if (externalRetrieveBlacklistIpUseCase.existsByIpAddress(clientIpAddress)
+ || externalCheckIpBlockedUseCase.isIpBlocked(clientIpAddress)) {
log.info("[{}] : 서비스 이용이 제한된 IP입니다.", clientIpAddress);
ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED);
return false;
@@ -79,12 +81,15 @@ private boolean verifyIpAddressAccess(HttpServletResponse response, String clien
return true;
}
- private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response, String clientIpAddress) throws IOException {
+ private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response, String clientIpAddress)
+ throws IOException {
String token = jwtTokenProvider.resolveToken(request);
// 토큰이 존재하고 유효한 경우
if (token != null && jwtTokenProvider.validateToken(token)) {
- RedisToken redisToken = jwtTokenProvider.isRefreshToken(token) ? externalManageRedisTokenUseCase.findByRefreshToken(token) : externalManageRedisTokenUseCase.findByAccessToken(token);
+ RedisToken redisToken =
+ jwtTokenProvider.isRefreshToken(token) ? externalManageRedisTokenUseCase.findByRefreshToken(token)
+ : externalManageRedisTokenUseCase.findByAccessToken(token);
if (redisToken == null) {
log.warn("존재하지 않는 토큰입니다.");
ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED);
@@ -108,7 +113,9 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons
private void sendSecurityAlertSlackMessage(HttpServletRequest request, RedisToken redisToken) {
if (redisToken.isAdminToken()) {
request.setAttribute("member", redisToken.getId());
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.DUPLICATE_LOGIN, "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다.");
+ String duplicateLoginMessage = "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.DUPLICATE_LOGIN, request, duplicateLoginMessage));
}
}
}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java
new file mode 100644
index 000000000..00117991b
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java
@@ -0,0 +1,30 @@
+package page.clab.api.global.common.notificationSetting.adapter.in.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import page.clab.api.global.common.dto.ApiResponse;
+import page.clab.api.global.common.notificationSetting.application.dto.response.NotificationSettingResponseDto;
+import page.clab.api.global.common.notificationSetting.application.port.in.RetrieveNotificationSettingUseCase;
+
+@RestController
+@RequestMapping("/api/v1/notification-settings")
+@RequiredArgsConstructor
+@Tag(name = "Notification Setting", description = "웹훅 알림 설정")
+public class NotificationSettingRetrieveController {
+
+ private final RetrieveNotificationSettingUseCase retrieveNotificationSettingUseCase;
+
+ @Operation(summary = "[S] 웹훅 알림 조회", description = "ROLE_SUPER 이상의 권한이 필요함")
+ @PreAuthorize("hasRole('SUPER')")
+ @GetMapping("")
+ public ApiResponse> getNotificationSettings() {
+ List notificationSettings = retrieveNotificationSettingUseCase.retrieveNotificationSettings();
+ return ApiResponse.success(notificationSettings);
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java
new file mode 100644
index 000000000..74023a6bc
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java
@@ -0,0 +1,33 @@
+package page.clab.api.global.common.notificationSetting.adapter.in.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import page.clab.api.global.common.dto.ApiResponse;
+import page.clab.api.global.common.notificationSetting.application.dto.request.NotificationSettingToggleRequestDto;
+import page.clab.api.global.common.notificationSetting.application.port.in.ManageNotificationSettingUseCase;
+
+@RestController
+@RequestMapping("/api/v1/notification-settings")
+@RequiredArgsConstructor
+@Tag(name = "Notification Setting", description = "웹훅 알림 설정")
+public class NotificationSettingToggleController {
+
+ private final ManageNotificationSettingUseCase manageNotificationSettingUseCase;
+
+ @Operation(summary = "[S] 웹훅 알림 설정 변경", description = "ROLE_SUPER 이상의 권한이 필요함")
+ @PreAuthorize("hasRole('SUPER')")
+ @PutMapping("")
+ public ApiResponse toggleNotificationSetting(
+ @Valid @RequestBody NotificationSettingToggleRequestDto requestDto
+ ) {
+ manageNotificationSettingUseCase.toggleNotificationSetting(requestDto.getAlertType(), requestDto.isEnabled());
+ return ApiResponse.success();
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java
new file mode 100644
index 000000000..4bae839c3
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java
@@ -0,0 +1,34 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.persistence;
+
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import page.clab.api.global.common.notificationSetting.application.port.out.RetrieveNotificationSettingPort;
+import page.clab.api.global.common.notificationSetting.application.port.out.UpdateNotificationSettingPort;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+import page.clab.api.global.common.notificationSetting.domain.NotificationSetting;
+
+@Component
+@RequiredArgsConstructor
+public class NotificationSettingPersistenceAdapter implements
+ RetrieveNotificationSettingPort,
+ UpdateNotificationSettingPort {
+
+ private final NotificationSettingRepository repository;
+
+ @Override
+ public List findAll() {
+ return repository.findAll();
+ }
+
+ @Override
+ public Optional findByAlertType(AlertType alertType) {
+ return repository.findByAlertType(alertType);
+ }
+
+ @Override
+ public NotificationSetting save(NotificationSetting setting) {
+ return repository.save(setting);
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java
similarity index 52%
rename from src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java
rename to src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java
index e4cab3c38..4be9efb7b 100644
--- a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java
@@ -1,10 +1,9 @@
-package page.clab.api.global.common.slack.dao;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import page.clab.api.global.common.slack.domain.AlertType;
-import page.clab.api.global.common.slack.domain.NotificationSetting;
+package page.clab.api.global.common.notificationSetting.adapter.out.persistence;
import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+import page.clab.api.global.common.notificationSetting.domain.NotificationSetting;
public interface NotificationSettingRepository extends JpaRepository {
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java
new file mode 100644
index 000000000..d67f0a17f
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java
@@ -0,0 +1,17 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.webhook;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.concurrent.CompletableFuture;
+import page.clab.api.global.common.notificationSetting.application.port.out.WebhookClient;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+
+/**
+ * {@code AbstractWebhookClient}는 Discord 및 Slack Webhook 클라이언트의 공통 인터페이스를 정의하는 추상 클래스입니다.
+ */
+public abstract class AbstractWebhookClient implements WebhookClient {
+
+ @Override
+ public abstract CompletableFuture sendMessage(String webhookUrl, AlertType alertType,
+ HttpServletRequest request,
+ Object additionalData);
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java
new file mode 100644
index 000000000..b6bfedf51
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java
@@ -0,0 +1,25 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.webhook;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.application.port.out.NotificationSender;
+import page.clab.api.global.common.notificationSetting.domain.PlatformType;
+
+@Component
+@RequiredArgsConstructor
+public class DiscordNotificationSender implements NotificationSender {
+
+ private final DiscordWebhookClient discordWebhookClient;
+
+ @Override
+ public String getPlatformName() {
+ return PlatformType.DISCORD.getName();
+ }
+
+ @Override
+ public void sendNotification(NotificationEvent event, String webhookUrl) {
+ discordWebhookClient.sendMessage(webhookUrl, event.getAlertType(), event.getRequest(),
+ event.getAdditionalData());
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java
new file mode 100644
index 000000000..fb1c443d3
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java
@@ -0,0 +1,362 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.webhook;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.env.Environment;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto;
+import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.service.WebhookCommonService;
+import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
+import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
+import page.clab.api.global.util.HttpReqResUtil;
+
+/**
+ * {@code DiscordWebhookClient}는 다양한 알림 유형에 따라 Discord 메시지를 구성하고 전송하는 클래스입니다.
+ *
+ * 주요 기능:
+ *
+ * - {@link #sendMessage(String, AlertType, HttpServletRequest, Object)}: Discord에 알림 메시지를 비동기적으로 전송
+ * - {@link #createEmbeds(AlertType, HttpServletRequest, Object)}: 알림 유형에 따라 Discord 메시지 임베드 생성
+ * - 다양한 알림 유형에 맞는 메시지 형식을 생성하는 전용 메서드
+ *
+ *
+ * Discord Webhook API를 사용하여 웹훅 URL을 통해 메시지를 전송하며, 메시지 전송 실패 시 로그에 오류를 기록합니다.
+ *
+ * AlertType을 기반으로 여러 도메인에서 발생하는 이벤트를 Discord를 통해 모니터링할 수 있도록 지원하며,
+ * Discord 알림은 주로 서버 이벤트, 보안 경고, 신규 신청, 관리자 로그인 등의 이벤트를 다룹니다.
+ *
+ * @see HttpClient
+ * @see HttpRequest
+ * @see HttpResponse
+ */
+@Component
+@Slf4j
+public class DiscordWebhookClient extends AbstractWebhookClient {
+
+ private final HttpClient httpClient;
+ private final ObjectMapper objectMapper;
+ private final NotificationConfigProperties.CommonProperties commonProperties;
+ private final Environment environment;
+ private final WebhookCommonService webhookCommonService;
+
+ public DiscordWebhookClient(
+ NotificationConfigProperties notificationConfigProperties,
+ ObjectMapper objectMapper,
+ Environment environment,
+ WebhookCommonService webhookCommonService
+ ) {
+ this.httpClient = HttpClient.newHttpClient();
+ this.objectMapper = objectMapper;
+ this.commonProperties = notificationConfigProperties.getCommon();
+ this.environment = environment;
+ this.webhookCommonService = webhookCommonService;
+ }
+
+ /**
+ * Discord에 알림 메시지를 비동기적으로 전송합니다.
+ *
+ * @param webhookUrl 메시지를 보낼 Discord 웹훅 URL
+ * @param alertType 알림 유형을 나타내는 {@link AlertType}
+ * @param request HttpServletRequest 객체, 클라이언트 요청 정보
+ * @param additionalData 추가 데이터
+ * @return 메시지 전송 성공 여부를 나타내는 CompletableFuture
+ */
+ public CompletableFuture sendMessage(String webhookUrl, AlertType alertType,
+ HttpServletRequest request, Object additionalData) {
+ Map payload = createPayload(alertType, request, additionalData);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String jsonPayload = objectMapper.writeValueAsString(payload);
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(webhookUrl))
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
+ .build();
+
+ HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() == HttpStatus.NO_CONTENT.value()) {
+ return true;
+ } else {
+ log.error("Discord notification failed: {}", response.body());
+ return false;
+ }
+ } catch (IOException | InterruptedException e) {
+ log.error("Failed to send Discord message: {}", e.getMessage(), e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * 알림 유형과 요청 정보, 추가 데이터를 사용하여 Discord 메시지 페이로드를 생성합니다.
+ *
+ * @param alertType 알림 유형
+ * @param request 클라이언트 요청 정보
+ * @param additionalData 추가 데이터
+ * @return 생성된 페이로드 맵
+ */
+ public Map createPayload(AlertType alertType, HttpServletRequest request, Object additionalData) {
+ List