diff --git a/.github/workflows/devfactory-homepage.yml b/.github/workflows/devfactory-homepage.yml index b7b99a9..4265a17 100644 --- a/.github/workflows/devfactory-homepage.yml +++ b/.github/workflows/devfactory-homepage.yml @@ -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) diff --git a/platform/.env.example b/platform/.env.example index 8b0e869..64685b6 100644 --- a/platform/.env.example +++ b/platform/.env.example @@ -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 diff --git a/platform/docker-compose.yml b/platform/docker-compose.yml index 96d4834..ab83126 100644 --- a/platform/docker-compose.yml +++ b/platform/docker-compose.yml @@ -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 diff --git a/platform/server/src/index.js b/platform/server/src/index.js index 504b6c2..563dd71 100644 --- a/platform/server/src/index.js +++ b/platform/server/src/index.js @@ -1,5 +1,6 @@ require('dotenv').config(); const express = require('express'); +const crypto = require('crypto'); const { Pool } = require('pg'); const cors = require('cors'); @@ -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' }); }); @@ -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) {