From e451d9fdaec1d8ef87a44440df9cb294f6f59b75 Mon Sep 17 00:00:00 2001 From: yesinkim Date: Fri, 6 Feb 2026 19:21:53 +0900 Subject: [PATCH 1/4] feat(cert): improve eligibility error handling and messages --- cert/backend/src/constants/error_codes.py | 5 ++++- cert/backend/src/services/certificate_service.py | 9 +++++++++ cert/backend/src/utils/notion_client.py | 15 ++++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cert/backend/src/constants/error_codes.py b/cert/backend/src/constants/error_codes.py index ca1fce2..84c6905 100644 --- a/cert/backend/src/constants/error_codes.py +++ b/cert/backend/src/constants/error_codes.py @@ -5,7 +5,10 @@ class ErrorCodes: class ErrorMessages: """에러 메시지 상수""" - NO_HISTORY = "수료이력이 확인되지 않습니다. 🥲\n디스코드를 통해 김찬란에게 문의해주세요." + NO_HISTORY = "수료 명단에 존재하지 않습니다. 🥲\n입력하신 이름 '{name}'(이)가 명단에 있는지 확인하거나, 스터디 빌더 혹은 질문게시판에 문의해주세요." + USER_DROPPED_OUT = "스터디를 수료하지 않았습니다. 🥲\n스터디 빌더 혹은 질문게시판에 문의해주세요." + STUDY_NOT_COMPLETED = "수료증은 스터디가 완료된 이후 발급 가능합니다.\n스터디 빌더 혹은 질문게시판에 문의해주세요." + PROJECT_NOT_FOUND = "해당 프로젝트가 검색되지 않습니다.\n기수와 스터디명을 다시 확인하거나, 스터디 빌더 혹은 질문게시판에 문의해주세요." PIPELINE_ERROR = "발급 처리 중 오류가 발생했습니다." CONTACT_INFO = "시스템 상의 오류로 수료증 발급에 실패했습니다. 🥲\n디스코드를 통해 김수현(kyopbi)에게 문의해주세요" diff --git a/cert/backend/src/services/certificate_service.py b/cert/backend/src/services/certificate_service.py index 9d180b6..0404e61 100644 --- a/cert/backend/src/services/certificate_service.py +++ b/cert/backend/src/services/certificate_service.py @@ -589,6 +589,15 @@ async def _create_new_certificate( ) ) + except NotEligibleError as e: + # 수료 대상 아님 (명단에 없거나 이탈자 등) + logger.warning(f"수료 대상 아님: {str(e)}") + if request_id: + await notion_client.update_certificate_status( + page_id=request_id, + status=CertificateStatus.NOT_ELIGIBLE + ) + raise e except Exception as e: # 시스템 오류 logger.exception("신규 수료증 발급 중 시스템 오류") diff --git a/cert/backend/src/utils/notion_client.py b/cert/backend/src/utils/notion_client.py index 50c29d9..1b52c6f 100644 --- a/cert/backend/src/utils/notion_client.py +++ b/cert/backend/src/utils/notion_client.py @@ -6,7 +6,7 @@ import aiohttp from typing import Optional, Dict, Any, List -from ..constants.error_codes import NotEligibleError, ResponseStatus +from ..constants.error_codes import NotEligibleError, ResponseStatus, ErrorMessages from ..models.certificate import CertificateStatus from ..models.project import Project, SeasonGroup, ProjectsBySeasonResponse @@ -156,14 +156,17 @@ async def verify_user_participation( user_role = "BUILDER" elif user_name in runner_names: user_role = "RUNNER" + # '수료자' 필드에 이름이 포함되어 있는지 확인 + elif any(user_name in c for c in completer_names): + user_role = "RUNNER" # 2. 사용자가 이탈자에 있는지 확인 if user_name in dropout_names: - raise NotEligibleError(f"수료 명단에 존재하지 않습니다. 🥲\n디스코드를 통해 질문게시판에 문의해주세요.") + raise NotEligibleError(ErrorMessages.USER_DROPPED_OUT) # 3. 사용자가 참여자 목록에 있는지 확인 if user_role is None: - raise NotEligibleError(f"수료 명단에 존재하지 않습니다. 🥲\n디스코드를 통해 질문게시판에 문의해주세요.") + raise NotEligibleError(ErrorMessages.NO_HISTORY.format(name=user_name)) study_status = properties.get("단계", {}).get("select", {}) period_raw = project.get("properties", {}).get("기간", {}).get("date", {}) or {} @@ -174,9 +177,7 @@ async def verify_user_participation( ) if study_status.get("name") != "완료": - raise NotEligibleError( - "수료증은 스터디가 완료된 이후 발급 가능합니다.\n디스코드를 통해 질문게시판에 문의해주세요." - ) + raise NotEligibleError(ErrorMessages.STUDY_NOT_COMPLETED) fallback_period = self.default_periods.get(str(season), {}) raw_start = period_raw.get("start") @@ -273,7 +274,7 @@ async def verify_user_participation( "course_name": course_name, }, ) - raise Exception("해당 프로젝트가 검색되지 않습니다. \n디스코드를 통해 질문게시판에 문의해주세요.") + raise NotEligibleError(ErrorMessages.PROJECT_NOT_FOUND) except Exception as e: raise e From 2907e2c29204322fb0287c9f20cfcefe0a19206a Mon Sep 17 00:00:00 2001 From: soohyunme Date: Sun, 8 Feb 2026 04:34:44 +0900 Subject: [PATCH 2/4] chore(getcloser): enhance security config with environment variables and validation --- getcloser/backend/.env.example | 7 +++++++ getcloser/backend/app/core/config.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 getcloser/backend/.env.example diff --git a/getcloser/backend/.env.example b/getcloser/backend/.env.example new file mode 100644 index 0000000..e54c956 --- /dev/null +++ b/getcloser/backend/.env.example @@ -0,0 +1,7 @@ +# DATABASE_URL=postgresql+psycopg2://user:password@db:5432/app_db + +# 보안을 위해 무작위 문자열을 생성하여 설정하세요. +# 예: openssl rand -hex 32 +SECRET_KEY=your-super-secret-key-here + +# ACCESS_TOKEN_EXPIRE_MINUTES=60 diff --git a/getcloser/backend/app/core/config.py b/getcloser/backend/app/core/config.py index 898895d..4974341 100644 --- a/getcloser/backend/app/core/config.py +++ b/getcloser/backend/app/core/config.py @@ -1,12 +1,29 @@ import os +from pydantic import field_validator from pydantic_settings import BaseSettings class Settings(BaseSettings): + ENVIRONMENT: str = os.getenv("ENVIRONMENT", "dev") DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql+psycopg2://user:password@db:5432/app_db") + """ JWT 안쓸 것 같아 일단 주석 처리하고 추후 확정 시 삭제 """ - SECRET_KEY: str = os.getenv("SECRET_KEY", "change-me-in-prod") + # Secret key for JWT signing. Must be overridden in production using environment variables. + DEFAULT_SECRET_KEY = "default-secret-key-change-it" + SECRET_KEY: str = os.getenv("SECRET_KEY", DEFAULT_SECRET_KEY) + + @field_validator("SECRET_KEY") + @classmethod + def check_secret_key(cls, v, info): + """ + Validate that SECRET_KEY is not using the default placeholder value in production. + """ + env = os.getenv("ENVIRONMENT", "dev").lower() + if env in ["prod", "production"] and v == cls.DEFAULT_SECRET_KEY: + raise ValueError("SECRET_KEY must be a unique, non-default value in production environments.") + return v + ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60")) From 08837603476637e6068b7e3c30a2b590b21d7082 Mon Sep 17 00:00:00 2001 From: soohyunme Date: Sun, 8 Feb 2026 04:46:11 +0900 Subject: [PATCH 3/4] chore(ci): add SECRET_KEY and ENVIRONMENT to getcloser deployment workflow --- .github/workflows/deploy-getcloser.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy-getcloser.yml b/.github/workflows/deploy-getcloser.yml index 14b9af3..571c406 100644 --- a/.github/workflows/deploy-getcloser.yml +++ b/.github/workflows/deploy-getcloser.yml @@ -47,6 +47,8 @@ jobs: echo "TEAM_SIZE=${{ vars.TEAM_SIZE}}" >> .env echo "PENDING_TIMEOUT_MINUTES=${{ vars.PENDING_TIMEOUT_MINUTES}}" >> .env echo "DATA_DIR_HOST=${{ vars.DATA_DIR_HOST }}" >> .env + echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env + echo "ENVIRONMENT=prod" >> .env - name: 🚀 Deploy to PROD run: | From aca24f31ad8485288a774891ca683d6abfc4093d Mon Sep 17 00:00:00 2001 From: soohyunme Date: Sun, 8 Feb 2026 04:56:56 +0900 Subject: [PATCH 4/4] chore(cert): enhance cors config with environment variables --- cert/backend/.env.example | 5 +++++ cert/backend/src/main.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 cert/backend/.env.example diff --git a/cert/backend/.env.example b/cert/backend/.env.example new file mode 100644 index 0000000..c13c356 --- /dev/null +++ b/cert/backend/.env.example @@ -0,0 +1,5 @@ +# LOG_LEVEL=INFO +# ENVIRONMENT=dev + +# CORS Origins (쉼표로 구분) +CORS_ORIGINS=https://cert.pseudo-lab.com,https://dev-cert.pseudolab-devfactory.com,http://localhost:5173 diff --git a/cert/backend/src/main.py b/cert/backend/src/main.py index e024221..662b3d3 100644 --- a/cert/backend/src/main.py +++ b/cert/backend/src/main.py @@ -40,11 +40,23 @@ def configure_logging() -> None: # Access log middleware app.middleware("http")(access_log_middleware) -# CORS 미들웨어 설정 -origins = os.getenv("CORS_ORIGINS", "").split(",") +# CORS configuration +# Load allowed origins from CORS_ORIGINS environment variable (comma-separated) +cors_origins_str = os.getenv("CORS_ORIGINS", "") +if cors_origins_str: + origins = [origin.strip() for origin in cors_origins_str.split(",") if origin.strip()] +else: + # Default origins for local development and known production/dev domains + origins = [ + "http://localhost:3000", + "http://localhost:5173", + "https://cert.pseudo-lab.com", + "https://dev-cert.pseudolab-devfactory.com", + ] + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"],