Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/deploy-getcloser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/devfactory-homepage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
cat > .env <<'EOF'
APP_HOST=${{ vars.APP_HOST }}
DATABASE_URL=${{ secrets.DATABASE_URL }}
ACCESS_LOGGING_IP_SALT=${{ secrets.ACCESS_LOGGING_IP_SALT }}
EOF

- name: Build & up (prod)
Expand Down
5 changes: 5 additions & 0 deletions cert/backend/.env.example
Original file line number Diff line number Diff line change
@@ -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
18 changes: 15 additions & 3 deletions cert/backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=["*"],
Expand Down
7 changes: 7 additions & 0 deletions getcloser/backend/.env.example
Original file line number Diff line number Diff line change
@@ -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
19 changes: 18 additions & 1 deletion getcloser/backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -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"))

Expand Down
3 changes: 3 additions & 0 deletions platform/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ APP_HOST=your-domain.com

# Database Setting
DATABASE_URL=postgresql://user:pass@devfactory-postgres:5432/dbname

# Logging Setting
ACCESS_LOGGING_IP_SALT=your-secret-salt-here
1 change: 1 addition & 0 deletions platform/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ services:
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL}
- ACCESS_LOGGING_IP_SALT=${ACCESS_LOGGING_IP_SALT}
- PORT=3000
networks:
- traefik
Expand Down
5 changes: 2 additions & 3 deletions platform/frontend/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1422,8 +1422,7 @@ body {
/* Activities Section */
#activities {
padding-top: 2rem;
padding-bottom: 4rem;
/* Reduced since it's the last section before footer */
padding-bottom: 6rem;
}

.activities-grid {
Expand Down Expand Up @@ -1783,6 +1782,6 @@ body {
}

#activities {
padding-bottom: 8rem;
padding-bottom: 6rem;
}
}
39 changes: 36 additions & 3 deletions platform/server/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require('dotenv').config();
const express = require('express');
const crypto = require('crypto');
const { Pool } = require('pg');
const cors = require('cors');

Expand All @@ -25,6 +26,35 @@ pool.query('SELECT NOW()', (err, res) => {
});

// API Routes

/**
* Extracts the client IP address from request headers or connection info.
*/
function getClientIp(req) {
// Check X-Forwarded-For header (common for reverse proxies)
const forwardedFor = req.headers['x-forwarded-for'];
if (forwardedFor) {
// Can be a comma-separated list; the first one is the original client
return forwardedFor.split(',')[0].trim();
}

// Check X-Real-IP header
const realIp = req.headers['x-real-ip'];
if (realIp) {
return realIp;
}

// Fallback to Express req.ip or socket address
return req.ip || req.socket.remoteAddress;
}

/**
* Hashes the IP address with a salt, matching the behavior in the cert system.
*/
function hashIp(ip, salt = '') {
if (!ip) return null;
return crypto.createHash('sha256').update(salt + ip).digest('hex');
}
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
Expand All @@ -33,12 +63,15 @@ app.get('/api/health', (req, res) => {
app.post('/api/stats/visit', async (req, res) => {
try {
const { path, userAgent } = req.body;
// ๊ธฐ์กด ๋กœ๊ทธ ํฌ๋งท์— ๋งž์ถฐ method๋Š” 'PAGEVIEW'๋กœ, referrer๋Š” ํ˜„์žฌ ํ˜ธ์ŠคํŠธ๋กœ ๊ธฐ๋ก
const referrer = req.headers.referer || '';

// Extract client IP and generate hash
const clientIp = getClientIp(req);
const ipHash = hashIp(clientIp, process.env.ACCESS_LOGGING_IP_SALT || '');

await pool.query(
'INSERT INTO logging.access_log (path, method, status, user_agent, referrer, ts) VALUES ($1, $2, $3, $4, $5, NOW())',
[path || '/', 'PAGEVIEW', 200, userAgent, referrer]
'INSERT INTO logging.access_log (path, method, status, ip_hash, user_agent, referrer, ts) VALUES ($1, $2, $3, $4, $5, $6, NOW())',
[path || '/', 'PAGEVIEW', 200, ipHash, userAgent, referrer]
);
res.status(201).json({ message: 'Visit logged successfully' });
} catch (err) {
Expand Down
Loading