Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Deploy Workinkorea Development Server
on:
push:
branches: [dev]

env:
BASE_URL: byeong98.xyz
DOCKER_IMAGE_NAME: workinkorea-server
PORT: 8000

jobs:
development-build-and-deploy:
runs-on: ubuntu-latest
environment: development
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
run: |
docker build -t ${{ env.DOCKER_IMAGE_NAME }} .
docker save ${{ env.DOCKER_IMAGE_NAME }} | gzip > image.tar.gz

- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

- name: Add SSH known hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts

- name: Copy image to remote server
run: |
scp -P ${{ secrets.SSH_PORT }} image.tar.gz ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/

- name: Deploy on remote server
run: |
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH'
set -e

IMAGE_NAME="${{ env.DOCKER_IMAGE_NAME }}"
BASE_URL="${{ env.BASE_URL }}"
PORT="${{ env.PORT }}"

echo "Loading Docker image..."
docker load < ~/image.tar.gz

echo "Stopping existing container..."
docker stop ${IMAGE_NAME} 2>/dev/null || true
docker rm ${IMAGE_NAME} 2>/dev/null || true

echo "Starting new container..."
docker run -d \
--name ${IMAGE_NAME} \
--network core_network \
--restart unless-stopped \
--label "traefik.enable=true" \
--label "traefik.http.routers.${IMAGE_NAME}.rule=Host(\`arw.${BASE_URL}\`)" \
--label "traefik.http.routers.${IMAGE_NAME}.entrypoints=websecure" \
--label "traefik.http.routers.${IMAGE_NAME}.tls.certresolver=le" \
--label "traefik.http.services.${IMAGE_NAME}.loadbalancer.server.port=${PORT}" \
-e COOKIE_DOMAIN="${{ secrets.COOKIE_DOMAIN }}" \
-e CLIENT_URL="${{ secrets.CLIENT_URL }}" \
-e ORIGINS_URLS="${{ secrets.ORIGINS_URLS }}" \
-e DATABASE_SYNC_URL="${{ secrets.DATABASE_SYNC_URL }}" \
-e DATABASE_ASYNC_URL="${{ secrets.DATABASE_ASYNC_URL }}" \
-e REDIS_HOST="${{ secrets.REDIS_HOST }}" \
-e REDIS_PORT="${{ secrets.REDIS_PORT }}" \
-e REDIS_DB="${{ secrets.REDIS_DB }}" \
-e GOOGLE_CLIENT_ID="${{ secrets.GOOGLE_CLIENT_ID }}" \
-e GOOGLE_CLIENT_SECRET="${{ secrets.GOOGLE_CLIENT_SECRET }}" \
-e GOOGLE_REDIRECT_URI="${{ secrets.GOOGLE_REDIRECT_URI }}" \
-e GOOGLE_AUTHORIZATION_URL="${{ secrets.GOOGLE_AUTHORIZATION_URL }}" \
-e GOOGLE_TOKEN_URL="${{ secrets.GOOGLE_TOKEN_URL }}" \
-e GOOGLE_USER_INFO_URL="${{ secrets.GOOGLE_USER_INFO_URL }}" \
-e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
-e JWT_ALGORITHM="${{ secrets.JWT_ALGORITHM }}" \
-e ACCESS_TOKEN_EXPIRE_MINUTES="${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" \
-e REFRESH_TOKEN_EXPIRE_MINUTES="${{ secrets.REFRESH_TOKEN_EXPIRE_MINUTES }}" \
-e MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" \
-e MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \
-e MAIL_FROM_NAME="${{ secrets.MAIL_FROM_NAME }}" \
-e MAIL_FROM="${{ secrets.MAIL_FROM }}" \
-e MAIL_PORT="${{ secrets.MAIL_PORT }}" \
-e MAIL_SERVER="${{ secrets.MAIL_SERVER }}" \
-e MINIO_ENDPOINT="${{ secrets.MINIO_ENDPOINT }}" \
-e MINIO_ACCESS_KEY="${{ secrets.MINIO_ACCESS_KEY }}" \
-e MINIO_SECRET_KEY="${{ secrets.MINIO_SECRET_KEY }}" \
-e MINIO_BUCKET_NAME="${{ secrets.MINIO_BUCKET_NAME }}" \
-e ADMIN_JWT_SECRET="${{ secrets.ADMIN_JWT_SECRET }}" \
-e ADMIN_JWT_ALGORITHM="${{ secrets.ADMIN_JWT_ALGORITHM }}" \
-e ADMIN_ACCESS_TOKEN_EXPIRE_MINUTES="${{ secrets.ADMIN_ACCESS_TOKEN_EXPIRE_MINUTES }}" \
-e ADMIN_REFRESH_TOKEN_EXPIRE_MINUTES="${{ secrets.ADMIN_REFRESH_TOKEN_EXPIRE_MINUTES }}" \
-e ADMIN_EMAILS="${{ secrets.ADMIN_EMAILS }}" \
${IMAGE_NAME}

echo "Cleaning up old images..."
docker image prune -f

echo "Removing temporary files..."
rm -f ~/image.tar.gz

echo "Deployment completed successfully!"
ENDSSH

- name: Send Discord Deployment Notification
if: always()
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL_DEV }}
status: ${{ job.status }}
title: "Workinkorea-Server / Development"
description: |
**Deployment ${{ job.status == 'success' && 'Successful' || 'Failed' }}**

• **Branch**: `${{ github.ref_name }}`
• **Commit**: `${{ github.sha }}`
• **Message**: ${{ github.event.head_commit.message }}
• **Environment**: Development
• **URL**: https://arw.${{ env.BASE_URL }}
41 changes: 10 additions & 31 deletions .github/workflows/deploy-pro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,6 @@ jobs:
uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GCP_SA_KEY }}'

- name: database migration
run: |
pip install --upgrade pip
pip install alembic asyncpg psycopg2-binary sqlalchemy python-dotenv pydantic-settings pydantic redis
wget https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.8.0/cloud-sql-proxy.linux.amd64 -O cloud-sql-proxy
chmod +x cloud-sql-proxy
./cloud-sql-proxy workinkorea-main:asia-northeast3:workinkorea-postgresql &
sleep 10
export DATABASE_SYNC_URL="${{ secrets.DATABASE_SYNC_URL }}"
export DATABASE_ASYNC_URL="${{ secrets.DATABASE_ASYNC_URL }}"
export REDIS_HOST="${{ secrets.REDIS_HOST }}"
export REDIS_PORT="${{ secrets.REDIS_PORT }}"
export REDIS_DB="${{ secrets.REDIS_DB }}"
alembic upgrade head
pkill cloud-sql-proxy


- name: Build and Push Container
run: |
Expand Down Expand Up @@ -97,22 +80,18 @@ jobs:
--env-vars-file env.yaml

# discord notification
- name: send success message
if: success()
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
status: ${{ job.status }}
title: "Workinkorea-Server-Production"
description: |
**Image: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}**

- name: send failure message
if: failure()
- name: Send Discord Deployment Notification
if: always()
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
status: ${{ job.status }}
title: "Workinkorea-Server-Production"
title: "Workinkorea-Server / Production"
description: |
**Image: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}**
**Deployment ${{ job.status == 'success' && 'Successful' || 'Failed' }}**

• **Branch**: `${{ github.ref_name }}`
• **Commit**: `${{ github.sha }}`
• **Message**: ${{ github.event.head_commit.message }}
• **Environment**: Production
• **URL**: https://workinkorea.com
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim

WORKDIR /app

# Redis 설치
RUN apt-get update && \
apt-get install -y redis-server && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Redis 설정 (localhost만 접근 가능하도록)
RUN sed -i 's/bind 127.0.0.1 ::1/bind 127.0.0.1/' /etc/redis/redis.conf && \
sed -i 's/protected-mode yes/protected-mode no/' /etc/redis/redis.conf

# 의존성 파일 복사
COPY pyproject.toml uv.lock ./

Expand Down
15 changes: 7 additions & 8 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,13 @@ pipeline {
steps {
echo "Determining colors.."
script {

def blueRunning = sh(
script: "docker ps -aq -f 'name=workinkorea-server-blue'",
script: "docker ps -q -f 'name=workinkorea-server-blue'",
returnStdout: true
).trim()

def greenRunning = sh(
script: "docker ps -aq -f 'name=workinkorea-server-green'",
script: "docker ps -q -f 'name=workinkorea-server-green'",
returnStdout: true
).trim()

Expand Down Expand Up @@ -157,7 +156,7 @@ pipeline {
echo "Health checking.."
script {
def healthCheck = sh(
script: "docker inspect -f '{{.State.Running}}' ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR}",
script: "docker inspect -f '{{.State.Running}}' ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} 2>/dev/null || echo 'false'",
returnStdout: true
).trim()

Expand Down Expand Up @@ -285,8 +284,8 @@ pipeline {
script {
sh """
docker stop ${env.DOCKER_IMAGE_NAME}-${env.COLOR} || true
docker container prune -f || true
docker image prune -f || true
docker rm ${env.DOCKER_IMAGE_NAME}-${env.COLOR} || true
docker rmi ${env.DOCKER_IMAGE_NAME}-${env.COLOR} || true
"""
}
discordSend description: "${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} deployed successfully",
Expand All @@ -301,8 +300,8 @@ pipeline {
try{
sh """
docker stop ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} || true
docker container prune -f || true
docker image prune -f || true
docker rm ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} || true
docker rmi ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} || true
"""
if (env.COLOR != "none") {
echo "Rolling back to ${env.COLOR} container..."
Expand Down
20 changes: 10 additions & 10 deletions app/admin/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import jwt
from app.auth.models import User
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException, status, Request
from app.core.settings import SETTINGS
from app.database import get_async_session
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -22,7 +22,8 @@ def get_auth_repository(


async def get_admin_user(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)),
auth_repository: AuthRepository = Depends(get_auth_repository)
) -> User:
"""
Expand All @@ -36,7 +37,13 @@ async def get_admin_user(
raises:
HTTPException
"""
access_token = credentials.credentials
access_token = None
if credentials:
access_token = credentials.credentials

if not access_token:
access_token = request.cookies.get("access_token")

env_admin_emails: str = SETTINGS.ADMIN_EMAILS

if not access_token:
Expand All @@ -51,7 +58,6 @@ async def get_admin_user(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Admin JWT configuration not found"
)

try:
payload = jwt.decode(
access_token,
Expand All @@ -66,7 +72,6 @@ async def get_admin_user(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)

# 어드민 토큰 타입 체크
if token_type != "admin_access":
raise HTTPException(
Expand All @@ -83,33 +88,28 @@ async def get_admin_user(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)

user = await auth_repository.get_user_by_email(email)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)

# user_gubun이 'admin'인지 체크
if user.user_gubun != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required - invalid user type"
)

# 어드민 이메일 리스트에 있는지 체크 (이중 체크)
if not SETTINGS.ADMIN_EMAILS:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Admin emails not configured"
)

admin_emails = [email.strip() for email in env_admin_emails.split(",")]
if user.email not in admin_emails:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required - email not authorized"
)

return user
Loading