diff --git a/.github/actions/ci-workflow.yml b/.github/actions/ci-workflow.yml deleted file mode 100644 index 69470fd..0000000 --- a/.github/actions/ci-workflow.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: SonarCloud -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened] -jobs: - build: - name: Build and analyze - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: 'zulu' # Alternative distribution options are available - - - name: grant permission to gradle - run: chmod +x gradlew - - - name: Cache SonarCloud packages - uses: actions/cache@v3 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: ${{ runner.os }}-gradle - - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonar --info diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 9c18e38..bf2d79a 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -5,6 +5,7 @@ on: - main pull_request: types: [ opened, synchronize, reopened ] + jobs: build: name: Build and analyze @@ -33,7 +34,6 @@ jobs: - name: touch application.yml run: | - mkdir ./src/main/resources touch ./src/main/resources/application.yml echo "${{ secrets.APPLICATION_DEV_YML }}" > ./src/main/resources/application.yml shell: bash diff --git a/.gitignore b/.gitignore index c2065bc..9b3fbb0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### Credentials ### +.env +src/main/resources/application.yaml + ### STS ### .apt_generated .classpath diff --git a/Dockerfile b/Dockerfile index 8067ea0..5e5acaf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Start with a base image containing Java runtime -FROM openjdk:17-jdk-alpine +# Use a multi-architecture base image that supports ARM64 +FROM openjdk:17 # Set the working directory in the container WORKDIR /app @@ -11,4 +11,4 @@ COPY build/libs/platform-core-*.jar app.jar EXPOSE 8080 # Command to run the executable jar file -ENTRYPOINT ["java", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"] diff --git a/build.gradle b/build.gradle index 1206801..9b3a314 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,10 @@ -import io.swagger.v3.oas.models.servers.Server import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI -import org.springframework.boot.gradle.tasks.bundling.BootJar - -buildscript { - ext { - restdocsApiSpecVersion = '0.19.2' - } -} plugins { id 'java' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' - id "org.sonarqube" version "4.4.1.3373" + id 'org.sonarqube' version '4.4.1.3373' id 'com.epages.restdocs-api-spec' version '0.19.2' id 'org.hidetake.swagger.generator' version '2.19.2' id 'jacoco' @@ -27,8 +19,8 @@ java { } } -jar { - enabled = false +repositories { + mavenCentral() } configurations { @@ -37,11 +29,8 @@ configurations { } } -repositories { - mavenCentral() -} - dependencies { + // Spring Boot implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security:2.3.3.RELEASE' @@ -49,98 +38,110 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.0") + + // AWS + implementation platform('io.awspring.cloud:spring-cloud-aws-dependencies:3.0.0') implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3' + // Database + runtimeOnly 'com.h2database:h2:2.2.224' + implementation 'mysql:mysql-connector-java:8.0.32' + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.1' + + // Lombok compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2:2.2.224' -// implementation 'mysql:mysql-connector-java:8.0.32' - implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.1") - - - // test dependencies + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' - testImplementation 'com.epages:restdocs-api-spec:0.19.2' - testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.19.2' + testImplementation "com.epages:restdocs-api-spec:0.19.2" + testImplementation "com.epages:restdocs-api-spec-mockmvc:0.19.2" testImplementation 'org.springframework.security:spring-security-test:6.3.1' + // Swagger UI swaggerUI 'org.webjars:swagger-ui:5.0.0' + + // Dev Tools + developmentOnly 'org.springframework.boot:spring-boot-devtools' } -openapi3 { - servers = [ - { url = 'http://localhost:8080' }, - { url = 'https://stage.gdsc-konkuk.dev' }, - { url = 'https://gdsc-konkuk.dev' }, - ] as List> - title = 'Post Service API' - description = 'Post Service API description' - version = '1.0.0' - format = 'yaml' +jar { + enabled = false } -swaggerSources { - sample { - setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml")) +bootJar { + dependsOn 'copySwaggerUI' + doFirst { + if (project.hasProperty('excludeSecrets') && project.property('excludeSecrets') == 'true') { + exclude 'application.yaml' + println 'Excluding application.yaml from the build' + } } } -tasks.named('test') { +resolveMainClassName { + dependsOn 'copySwaggerUI' +} + +test { useJUnitPlatform() finalizedBy 'jacocoTestReport' } jacoco { - toolVersion = "0.8.7" + toolVersion = '0.8.7' } jacocoTestReport { dependsOn 'copySwaggerUI' - reports { html.required = true xml.required = true } } -tasks.withType(GenerateSwaggerUI).configureEach { - dependsOn 'openapi3' +sonar { + properties { + property 'sonar.projectKey', 'gdsc-konkuk_platform-core' + property 'sonar.organization', 'gdsc-konkuk' + property 'sonar.host.url', 'https://sonarcloud.io' + property 'sonar.coverage.jacoco.xmlReportPaths', layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml") + } } -tasks.register('copySwaggerUI', Copy) { - dependsOn 'generateSwaggerUISample' - - def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get() - - from("${generateSwaggerUISampleTask.outputDir}") - into("${project.buildDir}/resources/main/static/docs") +openapi3 { + servers = [ + { url = 'https://gdsc-konkuk.dev' }, + { url = 'https://api.gdsc-konkuk.dev' }, + { url = 'https://stage.gdsc-konkuk.dev' }, + { url = 'http://localhost:8080' }, + ] + title = 'Post Service API' + description = 'Post Service API description' + version = '1.0.0' + format = 'yaml' } -tasks.resolveMainClassName { - dependsOn 'copySwaggerUI' +postman { + baseUrl = 'http://localhost:8080' } -tasks.withType(BootJar).configureEach { - dependsOn 'copySwaggerUI' +swaggerSources { + sample { + inputFile = file(layout.buildDirectory.file("api-spec/openapi3.yaml")) + } } -tasks.named('jar').configure { - dependsOn 'copySwaggerUI' +tasks.withType(GenerateSwaggerUI).configureEach { + dependsOn 'openapi3' } -// sonarqube plugins -sonar { - properties { - property "sonar.projectKey", "gdsc-konkuk_platform-core" - property "sonar.organization", "gdsc-konkuk" - property "sonar.host.url", "https://sonarcloud.io" - property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml' - } +tasks.register('copySwaggerUI', Copy) { + dependsOn 'generateSwaggerUISample' + from(tasks.named('generateSwaggerUISample', GenerateSwaggerUI).map { it.outputDir }) + into(layout.buildDirectory.dir("resources/main/static/docs")) } diff --git a/build.sh b/build.sh index 9c7adbc..f68d83f 100755 --- a/build.sh +++ b/build.sh @@ -1,13 +1,10 @@ #!/bin/bash # 프로젝트 클린 및 빌드 -./gradlew clean bootJar +./gradlew clean bootJar -PexcludeSecrets=true -# Docker 이미지 빌드 -docker build -t ekgns33/gdsc-spring:latest . - -# Docker 이미지 푸시 -docker push ekgns33/gdsc-spring:latest +# Docker 이미지 빌드 & 푸시 +docker buildx build --platform linux/amd64,linux/arm64 -t ekgns33/gdsc-spring:latest . --push # 완료 메시지 echo "Docker image pushed to ekgns33/gdsc-spring:latest successfully." diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..c143894 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,140 @@ +services: + mysql: + image: mysql:8.0 + restart: always + container_name: mysql + logging: + driver: awslogs + options: + awslogs-region: ${AWS_REGION} + awslogs-group: ${AWS_LOG_GROUP} + awslogs-stream: mysql + networks: + - backend + volumes: + - mysql-data:/var/lib/mysql:rw + - ./sql/:/docker-entrypoint-initdb.d/:ro + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --skip-character-set-client-handshake + healthcheck: + test: mysqladmin ping --host=mysql --user=${MYSQL_USER} --password=${MYSQL_PASSWORD} || exit 1 + interval: 30s + timeout: 5s + retries: 3 + start_period: 3m + start_interval: 5s + + spring: + image: goldentrash/gdsc-internal:latest + container_name: spring-app + restart: always + logging: + driver: awslogs + options: + awslogs-region: ${AWS_REGION} + awslogs-group: ${AWS_LOG_GROUP} + awslogs-stream: spring + networks: + - frontend + - backend + depends_on: + mysql: + condition: service_healthy + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE} + SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} + SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} + GMAIL_USERNAME: ${GMAIL_USERNAME} + GMAIL_APP_PASSWORD: ${GMAIL_APP_PASSWORD} + AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} + AWS_SECRET_KEY: ${AWS_SECRET_KEY} + AWS_REGION: ${AWS_REGION} + AWS_S3_BUCKET: ${AWS_S3_BUCKET} + DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL} + healthcheck: + test: curl --fail --silent --show-error http://spring:8080/actuator/health || exit 1 + interval: 30s + timeout: 5s + retries: 3 + start_period: 3m + start_interval: 5s + + nginx: + image: nginx:latest + container_name: nginx + restart: always + logging: + driver: awslogs + options: + awslogs-region: ${AWS_REGION} + awslogs-group: ${AWS_LOG_GROUP} + awslogs-stream: nginx + ports: + - "0.0.0.0:80:80/tcp" + - "0.0.0.0:443:443/tcp" + networks: + - frontend + depends_on: + spring: + condition: service_healthy + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./certbot/data/:/var/www/certbot/:ro + - ./certbot/conf/:/etc/letsencrypt/:ro + healthcheck: + test: curl --fail --silent --show-error http://nginx/health || exit 1 + interval: 30s + timeout: 5s + retries: 3 + start_period: 3m + start_interval: 5s + + certbot: + image: certbot/certbot:latest + container_name: certbot + restart: always + logging: + driver: awslogs + options: + awslogs-region: ${AWS_REGION} + awslogs-group: ${AWS_LOG_GROUP} + awslogs-stream: certbot + volumes: + - ./certbot/data:/var/www/certbot/:rw + - ./certbot/conf:/etc/letsencrypt/:rw + # If your nginx server has not yet been issued an SSL certificate, run the command below + # docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d gdsc-konkuk.dev + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12d & wait $${!}; done;'" + + autoheal: + image: willfarrell/autoheal:latest + container_name: autoheal + restart: always + logging: + driver: awslogs + options: + awslogs-region: ${AWS_REGION} + awslogs-group: ${AWS_LOG_GROUP} + awslogs-stream: autoheal + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + AUTOHEAL_CONTAINER_LABEL: all + AUTOHEAL_INTERVAL: 30 + +volumes: + mysql-data: + +networks: + frontend: + backend: + internal: true diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..776f41b --- /dev/null +++ b/nginx.conf @@ -0,0 +1,50 @@ +user nginx; +worker_processes auto; + +events { + worker_connections 512; +} + +http { + log_format main '$remote_addr - $remote_user [$status] "$request" ' + '$body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '$request_time'; + access_log /var/log/nginx/access.log main; + + server { + listen 80; + listen [::]:80; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location = /health { + access_log off; + add_header Content-Type application/json; + return 200 '{"status":"UP"}'; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 default_server ssl; + listen [::]:443 ssl; + + ssl_certificate /etc/letsencrypt/live/gdsc-konkuk.dev/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gdsc-konkuk.dev/privkey.pem; + + location / { + proxy_pass http://spring:8080; + proxy_set_header Host $host; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/sql/v0.sql b/sql/v0.sql new file mode 100644 index 0000000..8460155 --- /dev/null +++ b/sql/v0.sql @@ -0,0 +1,68 @@ +create table + if not exists gdsc.event ( + id bigint auto_increment primary key, + title varchar(255) NOT NULL, + content text NOT NULL, + location varchar(255) NOT NULL, + end_at datetime (6) NOT NULL, + start_at datetime (6) NOT NULL, + retrospect_content text NOT NULL + ); + +create table + if not exists gdsc.event_image ( + id bigint auto_increment primary key, + event_id bigint NOT NULL, + url varchar(255) NOT NULL, + CONSTRAINT FOREIGN KEY (event_id) REFERENCES gdsc.event (id) ON UPDATE CASCADE ON DELETE CASCADE + ); + +create table + if not exists gdsc.attendance ( + id bigint auto_increment primary key, + event_id bigint NOT NULL, + active_qr_uuid varchar(255) NULL, + CONSTRAINT FOREIGN KEY (event_id) REFERENCES gdsc.event (id) ON UPDATE CASCADE ON DELETE CASCADE + ); + +create table + if not exists gdsc.email_task ( + task_id bigint auto_increment primary key, + is_sent boolean DEFAULT FALSE, + send_at datetime (6) NOT NULL, + email_content text NOT NULL, + email_subject varchar(255) NOT NULL + ); + +create table + if not exists gdsc.email_receivers ( + task_id bigint NOT NULL, + receiver_email varchar(255) NOT NULL, + receiver_name varchar(255) NOT NULL, + CONSTRAINT FOREIGN KEY (task_id) REFERENCES gdsc.email_task (task_id) ON UPDATE CASCADE ON DELETE CASCADE + ); + +create table + if not exists gdsc.member ( + id bigint auto_increment primary key, + member_id varchar(255) UNIQUE NOT NULL, + member_name varchar(255) NOT NULL, + password varchar(255) NOT NULL, + member_role enum ('ADMIN', 'LEAD', 'MEMBER') NOT NULL, + batch varchar(255) NOT NULL, + department varchar(255) NOT NULL, + member_email varchar(255) NOT NULL, + is_activated boolean DEFAULT TRUE, + is_deleted boolean DEFAULT FALSE, + soft_deleted_at datetime (6) NULL + ); + +create table + if not exists gdsc.participant ( + id bigint auto_increment primary key, + member_id bigint NOT NULL, + attendance_id bigint NOT NULL, + attendance boolean DEFAULT FALSE, + CONSTRAINT FOREIGN KEY (member_id) REFERENCES gdsc.member (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT FOREIGN KEY (attendance_id) REFERENCES gdsc.attendance (id) ON UPDATE CASCADE ON DELETE CASCADE + ); diff --git a/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java b/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java index 786808c..2ef3dc3 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java @@ -107,7 +107,7 @@ public ClientRegistrationRepository clientRegistrationRepository() { private CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173", "https://stage.gdsc-konkuk.dev", "https://gdsc-konkuk.dev")); + configuration.setAllowedOrigins(allowedOrigins); configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE", "PATCH", "OPTIONS")); configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Arrays.asList("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization", "Location", "Range", "Cache-Control", "User-Agent", "DNT")); diff --git a/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java b/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java index 0f65e07..7fdcc79 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java @@ -1,10 +1,15 @@ package gdsc.konkuk.platformcore.global.consts; +import java.util.List; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class PlatformConstants { + public static final List allowedOrigins = List.of( + "http://localhost:5173", "https://stage.gdsc-konkuk.dev", + "https://gdsc-konkuk.dev", "https://admin.gdsc-konkuk.dev", + "https://member.gdsc-konkuk.dev", "https://landing.gdsc-konkuk.dev"); public static final Integer SOFT_DELETE_RETENTION_MONTHS = 3; diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..69d9b68 --- /dev/null +++ b/src/main/resources/application-prod.yaml @@ -0,0 +1,47 @@ +server: + servlet: + session: + timeout: 1800 + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: true + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + mail: + host: smtp.gmail.com + port: 587 + username: ${GMAIL_USERNAME} + password: ${GMAIL_APP_PASSWORD} + properties: + mail: + transport: + protocol: smtp + smtp: + auth: true + starttls: + enable: true + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: + static: ${AWS_REGION} + s3: + bucket: ${AWS_S3_BUCKET} + +discord: + webhook: + url: ${DISCORD_WEBHOOK_URL}