첫 커밋: 로컬 프로젝트 업로드

This commit is contained in:
2026-06-10 15:51:34 +09:00
commit 6a8dbeb2e9
1211 changed files with 312864 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
#!/bin/bash
# =================================================================
# Baron SSO 인스턴스 자동 생성 스크립트 (Full Infrastructure)
# =================================================================
if [ "$#" -ne 2 ]; then
echo "❌ Usage: $0 [INSTANCE_NAME] [PORT_PREFIX]"
exit 1
fi
INSTANCE_NAME=$1
PORT_PREFIX=$2
BASE_DIR=$(cd $(dirname $0); pwd)
TARGET_DIR="${BASE_DIR}/../instances/${INSTANCE_NAME}"
echo "🚀 Creating instance: ${INSTANCE_NAME} (Port Prefix: ${PORT_PREFIX}xxx)"
# 1. 폴더 구조 생성
mkdir -p "${TARGET_DIR}/gateway"
mkdir -p "${TARGET_DIR}/config/.generated"
mkdir -p "${TARGET_DIR}/ory/init-db"
mkdir -p "${TARGET_DIR}/ory/templates/kratos"
mkdir -p "${TARGET_DIR}/ory/templates/hydra"
mkdir -p "${TARGET_DIR}/ory/templates/keto"
mkdir -p "${TARGET_DIR}/ory/templates/oathkeeper"
mkdir -p "${TARGET_DIR}/userfront"
mkdir -p "${TARGET_DIR}/adminfront"
mkdir -p "${TARGET_DIR}/devfront"
mkdir -p "${TARGET_DIR}/orgfront"
# 2. .env 생성 및 변수 로드
sed "s/{{INSTANCE_NAME}}/${INSTANCE_NAME}/g; s/{{PORT_PREFIX}}/${PORT_PREFIX}/g" \
"${BASE_DIR}/templates/.env.template" > "${TARGET_DIR}/.env"
# 포트 계산 (단순 치환)
BACKEND_PORT="${PORT_PREFIX}000"
USERFRONT_PORT="${PORT_PREFIX}500"
DOMAIN_SUFFIX=$(grep "DOMAIN_SUFFIX=" "${TARGET_DIR}/.env" | cut -d'=' -f2 | tr -d '\r')
ADMINFRONT_DOMAIN="${INSTANCE_NAME}-admin.${DOMAIN_SUFFIX}"
DEVFRONT_DOMAIN="${INSTANCE_NAME}-dev.${DOMAIN_SUFFIX}"
ORGFRONT_DOMAIN="${INSTANCE_NAME}-org.${DOMAIN_SUFFIX}"
# 3. Docker Compose & Config 복사 및 치환
cp "${BASE_DIR}/templates/docker-compose.yaml" "${TARGET_DIR}/"
# Gateway & UserFront Nginx
sed "s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" "${BASE_DIR}/templates/gateway/nginx.conf" > "${TARGET_DIR}/gateway/nginx.conf"
sed "s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" "${BASE_DIR}/templates/userfront/nginx.conf" > "${TARGET_DIR}/userfront/nginx.conf"
# Oathkeeper Rules template
sed "s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" "${BASE_DIR}/templates/ory/oathkeeper/rules.json" > "${TARGET_DIR}/ory/templates/oathkeeper/rules.json"
cp "${TARGET_DIR}/ory/templates/oathkeeper/rules.json" "${TARGET_DIR}/ory/templates/oathkeeper/rules.stage.json"
cp "${TARGET_DIR}/ory/templates/oathkeeper/rules.json" "${TARGET_DIR}/ory/templates/oathkeeper/rules.prod.json"
cp "${TARGET_DIR}/ory/templates/oathkeeper/rules.json" "${TARGET_DIR}/ory/templates/oathkeeper/rules.active.json"
# Kratos Config template
sed "s/{{BACKEND_PORT}}/${BACKEND_PORT}/g; s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g" \
"${BASE_DIR}/templates/ory/kratos/kratos.yml.template" > "${TARGET_DIR}/ory/templates/kratos/kratos.yml.template"
# Vite Configs
sed "s/{{ADMINFRONT_DOMAIN}}/${ADMINFRONT_DOMAIN}/g; s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" \
"${BASE_DIR}/templates/adminfront/vite.config.ts" > "${TARGET_DIR}/adminfront/vite.config.ts"
sed "s/{{DEVFRONT_DOMAIN}}/${DEVFRONT_DOMAIN}/g; s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" \
"${BASE_DIR}/templates/devfront/vite.config.ts" > "${TARGET_DIR}/devfront/vite.config.ts"
sed "s/{{ORGFRONT_DOMAIN}}/${ORGFRONT_DOMAIN}/g; s/{{BACKEND_PORT}}/${BACKEND_PORT}/g" \
"${BASE_DIR}/templates/orgfront/vite.config.ts" > "${TARGET_DIR}/orgfront/vite.config.ts"
# 4. 프론트엔드 auth.ts 주입 (하드코딩된 포트 해결)
sed "s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g; s/{{CLIENT_ID}}/adminfront/g" \
"${BASE_DIR}/templates/auth.template.ts" > "${TARGET_DIR}/adminfront/auth.ts"
sed "s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g; s/{{CLIENT_ID}}/devfront/g" \
"${BASE_DIR}/templates/auth.template.ts" > "${TARGET_DIR}/devfront/auth.ts"
sed "s/{{USERFRONT_PORT}}/${USERFRONT_PORT}/g" \
"${BASE_DIR}/templates/orgfront/auth.ts" > "${TARGET_DIR}/orgfront/auth.ts"
# 5. Ory template 복사 및 완성 config 렌더링
if [ -d "${BASE_DIR}/../docker/ory/init-db" ]; then cp -n "${BASE_DIR}/../docker/ory/init-db/"* "${TARGET_DIR}/ory/init-db/" 2>/dev/null || true; fi
if [ -d "${BASE_DIR}/../docker/ory/kratos" ]; then cp -n "${BASE_DIR}/../docker/ory/kratos/"* "${TARGET_DIR}/ory/templates/kratos/" 2>/dev/null || true; fi
if [ -d "${BASE_DIR}/../docker/ory/kratos/courier-templates" ]; then cp -a "${BASE_DIR}/../docker/ory/kratos/courier-templates" "${TARGET_DIR}/ory/templates/kratos/" 2>/dev/null || true; fi
if [ -d "${BASE_DIR}/../docker/ory/hydra" ]; then cp -n "${BASE_DIR}/../docker/ory/hydra/"* "${TARGET_DIR}/ory/templates/hydra/" 2>/dev/null || true; fi
if [ -d "${BASE_DIR}/../docker/ory/keto" ]; then cp -n "${BASE_DIR}/../docker/ory/keto/"* "${TARGET_DIR}/ory/templates/keto/" 2>/dev/null || true; fi
if [ -d "${BASE_DIR}/../docker/ory/oathkeeper" ]; then cp -n "${BASE_DIR}/../docker/ory/oathkeeper/"* "${TARGET_DIR}/ory/templates/oathkeeper/" 2>/dev/null || true; fi
ORY_CONFIG_ENV_FILES="${TARGET_DIR}/.env" \
ORY_CONFIG_TEMPLATE_ROOT="${TARGET_DIR}/ory/templates" \
ORY_CONFIG_OUTPUT_DIR="${TARGET_DIR}/config/.generated/ory" \
bash "${BASE_DIR}/../scripts/render_ory_config.sh"
# 6. 마무리
chmod +x "${TARGET_DIR}/.env"
echo "--------------------------------------------------"
echo "✅ Success! ALL files (Infra/Ory/Apps/FrontConfigs) are ready."
echo "📂 Location: ${TARGET_DIR}"
echo "🚀 Run: cd ${TARGET_DIR} && docker compose up -d"
echo "--------------------------------------------------"

View File

@@ -0,0 +1,64 @@
# Baron SSO 다중 인스턴스 배포 가이드
이 프로젝트는 한 대의 서버(머신)에서 여러 개의 독립된 서비스 인스턴스(예: stg, test2, prod 등)를 충돌 없이 실행하기 위한 **템플릿 기반 자동 배포 시스템**을 사용합니다.
## 1. 폴더 구조
- `deploy/templates/`: 모든 설정의 원천이 되는 마스터 템플릿.
- `deploy/create-instance.sh`: 새로운 인스턴스를 생성하는 마법사 스크립트.
- `instances/`: (자동 생성) 실제 실행되는 인스턴스별 설정 파일들이 저장되는 곳.
## 2. 핵심 배포 전략
### 2.1 포트 Prefix 시스템
모든 포트는 환경 변수 `P` (Port Prefix)를 기반으로 자동 계산됩니다.
- 예: 포트 Prefix를 `23`으로 설정 시:
- Backend: `23000`
- PostgreSQL: `23432`
- UserFront: `23500`
- AdminFront: `23173`
### 2.2 네임스페이스 격리
모든 리소스 이름에 `${INSTANCE_NAME}`이 자동으로 붙어 도커 엔진 내에서 격리됩니다.
- 컨테이너: `baron-sso-test2_backend`
- 네트워크: `baron-sso-test2_net`
- 볼륨: `db_data_test2`
## 3. 새로운 인스턴스 배포 방법
### Step 1: 인스턴스 생성
`deploy/` 폴더로 이동하여 스크립트를 실행합니다.
```bash
cd deploy
./create-instance.sh [인스턴스이름] [포트앞자리]
```
> **예시**: `test3` 환경을 `24xxx` 포트 대역으로 만들고 싶을 때
> ```bash
> ./create-instance.sh test3 24
> ```
### Step 2: 서비스 실행
생성된 인스턴스 폴더로 이동하여 도커 컴포즈를 실행합니다.
```bash
cd instances/test3
docker compose up -d
```
## 4. 도메인 및 URL 규칙
스크립트 실행 시 인스턴스 이름에 따라 다음 도메인이 자동으로 설정 파일에 주입됩니다. (`DOMAIN_SUFFIX``.env`에서 수정 가능)
- **SSO/UserFront**: `https://[이름]-sso.hmac.kr`
- **AdminFront**: `https://[이름]-admin.hmac.kr`
- **DevFront**: `https://[이름]-dev.hmac.kr`
## 5. 주요 설정 파일 관리
- **Nginx (Gateway & UserFront)**: 각 인스턴스의 백엔드 포트를 자동으로 감지하여 리버스 프록시를 수행합니다.
- **Ory Kratos**: `allowed_origins`, `allowed_return_urls`, `webhook` 주소 등이 인스턴스 포트에 맞게 자동 치환됩니다.
- **Ory Oathkeeper (`rules.json`, `rules.active.json`)**: 인증 규칙 내의 업스트림 주소가 현재 인스턴스 포트에 맞게 자동 치환됩니다.
- **Environment (`.env`)**: 모든 시크릿과 URL 설정이 중앙에서 관리됩니다.
## 6. Vite 프론트엔드 주의사항
Vite 개발 서버나 Preview 모드를 사용하는 경우, `vite.config.ts``allowedHosts` 설정이 새로운 도메인을 차단할 수 있습니다.
여러 인스턴스를 동적 도메인으로 운영할 경우, `vite.config.ts`에서 `allowedHosts: true` (Vite 5.1+) 설정을 권장합니다.
---
**주의**: 새로운 인스턴스 생성 후 외부에서 접속하려면, 앞단의 메인 리버스 프록시(Nginx 등)나 DNS 설정에서 해당 인스턴스의 포트(`USERFRONT_PORT`)로 라우팅해주는 작업이 필요합니다.

View File

@@ -0,0 +1,77 @@
# === [1] 프로젝트 식별 (중요: 인스턴스마다 다르게 설정) ===
INSTANCE_NAME={{INSTANCE_NAME}}
COMPOSE_PROJECT_NAME=baron-sso-{{INSTANCE_NAME}}
APP_ENV=production
# === [2] 포트 Prefix 설정 (예: 23 입력 시 23000, 23432 등 생성) ===
P={{PORT_PREFIX}}
# 인프라 포트
DB_PORT=${P}432
REDIS_PORT=${P}399
CLICKHOUSE_PORT_HTTP=${P}123
CLICKHOUSE_PORT_NATIVE=${P}000
# 서비스 포트
BACKEND_PORT=${P}000
USERFRONT_PORT=${P}500
ADMINFRONT_PORT=${P}173
DEVFRONT_PORT=${P}174
ORGFRONT_PORT=${P}175
OATHKEEPER_PROXY_PORT=${P}467
# === [3] 도메인 설정 (별도 도메인 구조) ===
# {{INSTANCE_NAME}}이 stg면 sso-stg.hmac.kr 형식이 되도록 가이드
DOMAIN_SUFFIX=hmac.kr
USERFRONT_URL=https://{{INSTANCE_NAME}}-sso.${DOMAIN_SUFFIX}
ADMINFRONT_URL=https://{{INSTANCE_NAME}}-admin.${DOMAIN_SUFFIX}
DEVFRONT_URL=https://{{INSTANCE_NAME}}-dev.${DOMAIN_SUFFIX}
ORGFRONT_URL=https://{{INSTANCE_NAME}}-org.${DOMAIN_SUFFIX}
# OIDC/Auth URL
VITE_OIDC_AUTHORITY=${USERFRONT_URL}/oidc
ADMINFRONT_CALLBACK_URLS=${ADMINFRONT_URL}/auth/callback
DEVFRONT_CALLBACK_URLS=${DEVFRONT_URL}/auth/callback
ORGFRONT_CALLBACK_URLS=${ORGFRONT_URL}/auth/callback
# Ory URL
KRATOS_UI_URL=${USERFRONT_URL}/auth
KRATOS_BROWSER_URL=${USERFRONT_URL}/auth
KRATOS_ADMIN_URL=http://kratos:4434
HYDRA_PUBLIC_URL=${USERFRONT_URL}/oidc
HYDRA_ADMIN_URL=http://hydra:4445
OATHKEEPER_PUBLIC_URL=${USERFRONT_URL}
KETO_READ_URL=http://keto:4466
KETO_WRITE_URL=http://keto:4467
# Ory versions
KRATOS_VERSION=v26.2.0
HYDRA_VERSION=v26.2.0
KETO_VERSION=v26.2.0
OATHKEEPER_VERSION=v26.2.0
ORY_POSTGRES_TAG=17-alpine
# === [4] IDP 및 DB Config ===
IDP_PROVIDER=ory
DB_PASSWORD=password
ORY_POSTGRES_USER=ory
ORY_POSTGRES_PASSWORD=generated_secret_here
ORY_POSTGRES_DB=ory
KRATOS_DB=ory_kratos
HYDRA_DB=ory_hydra
KETO_DB=ory_keto
OATHKEEPER_UID=1001
OATHKEEPER_GID=1001
OATHKEEPER_INTROSPECT_CLIENT_ID=oathkeeper-introspect
OATHKEEPER_INTROSPECT_CLIENT_SECRET=oathkeeper-secret
CLICKHOUSE_PASSWORD=password
REDIS_ADDR=redis:6379
# Secrets (At least 32 chars)
COOKIE_SECRET=at_least_32_characters_long_secret_12345
JWT_SECRET=at_least_32_characters_long_secret_12345
CSRF_COOKIE_SECRET=at_least_32_characters_long_secret_12345
# Admin 초기 계정
ADMIN_EMAIL=admin@baron.co.kr
ADMIN_PASSWORD=adminPasswordIsNotSimple

View File

@@ -0,0 +1,32 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"],
cacheDir:
process.env.ADMINFRONT_VITE_CACHE_DIR ??
"/tmp/baron-sso-adminfront-vite-cache",
server: {
host: "127.0.0.1",
// 인스턴스별 도메인을 자동으로 허용
allowedHosts: ["{{ADMINFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
preview: {
host: "127.0.0.1",
port: 5173,
allowedHosts: ["{{ADMINFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
});

View File

@@ -0,0 +1,21 @@
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = {
authority:
import.meta.env.VITE_OIDC_AUTHORITY || `${window.location.protocol}//${window.location.hostname}:{{USERFRONT_PORT}}/oidc`,
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "{{CLIENT_ID}}",
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
scope: "openid offline_access profile email",
post_logout_redirect_uri: window.location.origin,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: false,
};
export const userManager = new UserManager({
...oidcConfig,
authority: oidcConfig.authority || "",
client_id: oidcConfig.client_id || "",
redirect_uri: oidcConfig.redirect_uri || "",
});

View File

@@ -0,0 +1,29 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
envPrefix: ["VITE_", "USERFRONT_"],
server: {
host: "127.0.0.1",
// 인스턴스별 도메인을 자동으로 허용
allowedHosts: ["{{DEVFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
preview: {
host: "127.0.0.1",
port: 5173,
allowedHosts: ["{{DEVFRONT_DOMAIN}}", "localhost", "127.0.0.1"],
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
});

View File

@@ -0,0 +1,375 @@
name: ${COMPOSE_PROJECT_NAME}
services:
# --- Infrastructure ---
postgres:
image: postgres:17-alpine
container_name: ${COMPOSE_PROJECT_NAME}_db
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- "${DB_PORT}:5432"
volumes:
- db_data:/var/lib/postgresql/data
networks: [app_net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
redis:
image: redis:7-alpine
container_name: ${COMPOSE_PROJECT_NAME}_redis
ports:
- "${REDIS_PORT}:6379"
networks: [app_net]
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: ${COMPOSE_PROJECT_NAME}_clickhouse
environment:
- CLICKHOUSE_USER=baron
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
ports:
- "${CLICKHOUSE_PORT_HTTP}:8123"
- "${CLICKHOUSE_PORT_NATIVE}:9000"
volumes:
- clickhouse_data:/var/lib/clickhouse
networks: [app_net]
# --- Ory Stack ---
postgres_ory:
image: postgres:${ORY_POSTGRES_TAG:-17-alpine}
container_name: ${COMPOSE_PROJECT_NAME}_ory_db
environment:
- POSTGRES_USER=${ORY_POSTGRES_USER:-ory}
- POSTGRES_PASSWORD=${ORY_POSTGRES_PASSWORD}
- POSTGRES_DB=${ORY_POSTGRES_DB:-ory}
volumes:
- ory_db_data:/var/lib/postgresql/data
- ./ory/init-db:/docker-entrypoint-initdb.d:ro
networks: [app_net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${ORY_POSTGRES_USER:-ory} -d ${KRATOS_DB:-ory_kratos}"]
interval: 5s
kratos-migrate:
image: oryd/kratos:${KRATOS_VERSION:-v26.2.0}
env_file: .env
environment:
- DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20
- KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL}
- KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434}
- KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}
- KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]}
- KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error
- KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled
- KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery
- KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification
- KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login
- KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration
- KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login
volumes:
- ./config/.generated/ory/kratos:/etc/config/kratos:ro
command: migrate sql up -e -c /etc/config/kratos/kratos.yml --yes
networks: [app_net]
depends_on:
postgres_ory: { condition: service_healthy }
kratos:
image: oryd/kratos:${KRATOS_VERSION:-v26.2.0}
container_name: ${COMPOSE_PROJECT_NAME}_kratos
env_file: .env
environment:
- DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB:-ory_kratos}?sslmode=disable&max_conns=20
- COOKIE_SECRET=${COOKIE_SECRET}
- KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL}
- KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434}
- KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}
- KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["${KRATOS_UI_URL}","${KRATOS_UI_URL}/","${USERFRONT_URL}","${USERFRONT_URL}/","${USERFRONT_URL}/ko","${USERFRONT_URL}/ko/","${USERFRONT_URL}/en","${USERFRONT_URL}/en/","${USERFRONT_URL}/auth/callback","${USERFRONT_URL}/ko/auth/callback","${USERFRONT_URL}/en/auth/callback","${ADMINFRONT_URL}/auth/callback","${DEVFRONT_URL}/auth/callback","${ORGFRONT_URL}/auth/callback"]}
- KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL}/error
- KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL}/error?error=settings_disabled
- KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL}/recovery
- KRATOS_SELFSERVICE_FLOWS_VERIFICATION_UI_URL=${KRATOS_UI_URL}/verification
- KRATOS_SELFSERVICE_FLOWS_LOGIN_UI_URL=${KRATOS_UI_URL}/login
- KRATOS_SELFSERVICE_FLOWS_REGISTRATION_UI_URL=${KRATOS_UI_URL}/registration
- KRATOS_SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL}/login
volumes:
- ./config/.generated/ory/kratos:/etc/config/kratos:ro
command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
networks: [app_net]
depends_on:
kratos-migrate: { condition: service_completed_successfully }
hydra-migrate:
image: oryd/hydra:${HYDRA_VERSION:-v26.2.0}
env_file: .env
environment:
- DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20
command: migrate sql up -e --yes
networks: [app_net]
depends_on:
postgres_ory: { condition: service_healthy }
hydra:
image: oryd/hydra:${HYDRA_VERSION:-v26.2.0}
container_name: ${COMPOSE_PROJECT_NAME}_hydra
env_file: .env
environment:
- DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB:-ory_hydra}?sslmode=disable&max_conns=20
- URLS_SELF_ISSUER=${HYDRA_PUBLIC_URL}
- URLS_LOGIN=${HYDRA_LOGIN_URL:-${USERFRONT_URL}/login}
- URLS_CONSENT=${HYDRA_CONSENT_URL:-${USERFRONT_URL}/consent}
- URLS_ERROR=${HYDRA_ERROR_URL:-${USERFRONT_URL}/error}
- SECRETS_SYSTEM=${ORY_POSTGRES_PASSWORD}
volumes:
- ./config/.generated/ory/hydra:/etc/config/hydra:ro
command: serve -c /etc/config/hydra/hydra.yml all --dev
networks: [app_net]
depends_on:
hydra-migrate: { condition: service_completed_successfully }
keto-migrate:
image: oryd/keto:${KETO_VERSION:-v26.2.0}
env_file: .env
environment:
- DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20
volumes:
- ./config/.generated/ory/keto:/etc/config/keto:ro
command: ["migrate", "up", "-c", "/etc/config/keto/keto.yml", "--yes"]
networks: [app_net]
depends_on:
postgres_ory: { condition: service_healthy }
keto:
image: oryd/keto:${KETO_VERSION:-v26.2.0}
container_name: ${COMPOSE_PROJECT_NAME}_keto
env_file: .env
environment:
- DSN=postgres://${ORY_POSTGRES_USER:-ory}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KETO_DB:-ory_keto}?sslmode=disable&max_conns=20
volumes:
- ./config/.generated/ory/keto:/etc/config/keto:ro
command: serve -c /etc/config/keto/keto.yml
networks: [app_net]
depends_on:
keto-migrate: { condition: service_completed_successfully }
oathkeeper_logs_init:
image: alpine:latest
command: ["sh", "-c", "mkdir -p /var/log/oathkeeper && chown -R ${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001} /var/log/oathkeeper"]
volumes:
- oathkeeper_logs:/var/log/oathkeeper
networks: [app_net]
oathkeeper:
image: oryd/oathkeeper:${OATHKEEPER_VERSION:-v26.2.0}
container_name: ${COMPOSE_PROJECT_NAME}_oathkeeper
env_file: .env
user: "${OATHKEEPER_UID:-1001}:${OATHKEEPER_GID:-1001}"
ports:
- "${OATHKEEPER_PROXY_PORT}:4455"
environment:
- APP_ENV=${APP_ENV:-production}
- LOG_LEVEL=debug
- OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}
- OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}
volumes:
- ./config/.generated/ory/oathkeeper:/etc/config/oathkeeper:ro
- oathkeeper_logs:/var/log/oathkeeper
entrypoint: ["/etc/config/oathkeeper/entrypoint.sh"]
networks: [app_net]
depends_on:
oathkeeper_logs_init: { condition: service_completed_successfully }
kratos: { condition: service_started }
hydra: { condition: service_started }
ory_stack_check:
image: alpine:latest
container_name: ${COMPOSE_PROJECT_NAME}_ory_stack_check
command: >
/bin/sh -c "
apk add --no-cache curl;
echo 'Wait for Ory services...';
check_ready() {
name=\"$$1\";
url=\"$$2\";
max=\"$${ORY_STACK_CHECK_MAX_ATTEMPTS:-60}\";
i=1;
while [ \"$$i\" -le \"$$max\" ]; do
if curl --connect-timeout 2 --max-time 3 -fsS \"$$url\" >/dev/null; then
echo \"Ory service ready: $$name\";
return 0;
fi;
echo \"Waiting for Ory service: $$name ($$i/$$max)\";
i=$$((i + 1));
sleep 1;
done;
echo \"ERROR: Ory service not ready: $$name after $$max attempts ($$url)\" >&2;
echo \"ERROR: Check service logs: docker logs $${COMPOSE_PROJECT_NAME}_$$name\" >&2;
return 1;
};
check_ready kratos http://kratos:4433/health/ready || exit 1;
check_ready hydra http://hydra:4444/health/ready || exit 1;
check_ready keto http://keto:4466/health/ready || exit 1;
echo 'Ory stack is ready.';"
depends_on:
- kratos
- hydra
- keto
networks: [app_net]
init-rp:
image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0}
env_file: .env
entrypoint: ["/bin/sh", "-ec"]
command:
- |
upsert_client() {
ID=$$1
shift
if hydra get oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" "$$ID" >/dev/null 2>&1; then
hydra update oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" "$$ID" "$$@"
else
hydra create oauth2-client --endpoint "$${HYDRA_ADMIN_URL:-http://hydra:4445}" --id "$$ID" "$$@"
fi
}
upsert_client "adminfront" \
--name "AdminFront" \
--grant-type authorization_code,refresh_token \
--response-type code \
--scope openid,offline_access,profile,email \
--token-endpoint-auth-method none \
--redirect-uri "$${ADMINFRONT_CALLBACK_URLS:-$${ADMINFRONT_URL}/auth/callback}"
upsert_client "devfront" \
--name "DevFront" \
--grant-type authorization_code,refresh_token \
--response-type code \
--scope openid,offline_access,profile,email \
--token-endpoint-auth-method none \
--redirect-uri "$${DEVFRONT_CALLBACK_URLS:-$${DEVFRONT_URL}/auth/callback}"
upsert_client "orgfront" \
--name "OrgFront" \
--grant-type authorization_code,refresh_token \
--response-type code \
--scope openid,offline_access,profile,email \
--token-endpoint-auth-method none \
--redirect-uri "$${ORGFRONT_CALLBACK_URLS:-$${ORGFRONT_URL}/auth/callback}"
upsert_client "$${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect}" \
--secret "$${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret}" \
--grant-type client_credentials \
--response-type token \
--scope openid,offline_access,profile,email
depends_on:
ory_stack_check: { condition: service_completed_successfully }
networks: [app_net]
# --- Application Services ---
backend:
image: baron-backend:latest
container_name: ${COMPOSE_PROJECT_NAME}_backend
env_file: .env
environment:
- PORT=${BACKEND_PORT}
- APP_ENV=${APP_ENV:-production}
- IDP_PROVIDER=${IDP_PROVIDER:-ory}
- USERFRONT_URL=${USERFRONT_URL}
- KRATOS_ADMIN_URL=${KRATOS_ADMIN_URL:-http://kratos:4434}
- HYDRA_ADMIN_URL=${HYDRA_ADMIN_URL:-http://hydra:4445}
- HYDRA_PUBLIC_URL=${HYDRA_PUBLIC_URL}
- KETO_READ_URL=${KETO_READ_URL:-http://keto:4466}
- KETO_WRITE_URL=${KETO_WRITE_URL:-http://keto:4467}
- DB_HOST=postgres
- REDIS_ADDR=redis:6379
- CLICKHOUSE_HOST=clickhouse
- SEED_TENANT_CSV_PATH=/app/seed-tenant.csv
ports:
- "${BACKEND_PORT}:${BACKEND_PORT}"
volumes:
- ../../adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro
networks: [app_net]
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_started }
oathkeeper: { condition: service_started }
gateway:
build:
context: ../..
dockerfile: ./userfront/Dockerfile
target: production
container_name: ${COMPOSE_PROJECT_NAME}_gateway
ports:
- "${USERFRONT_PORT}:80"
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
networks: [app_net]
adminfront:
build:
context: ../..
dockerfile: ./adminfront/Dockerfile
args:
VITE_ADMIN_PUBLIC_URL: ${ADMINFRONT_URL}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY}
VITE_OIDC_CLIENT_ID: adminfront
ORGFRONT_URL: ${ORGFRONT_URL}
container_name: ${COMPOSE_PROJECT_NAME}_adminfront
env_file: .env
environment:
- APP_ENV=${APP_ENV:-production}
- API_PROXY_TARGET=http://backend:${BACKEND_PORT}
ports:
- "${ADMINFRONT_PORT}:5173"
networks: [app_net]
devfront:
build:
context: ../..
dockerfile: ./devfront/Dockerfile
args:
VITE_DEVFRONT_PUBLIC_URL: ${DEVFRONT_URL}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY}
VITE_OIDC_CLIENT_ID: devfront
container_name: ${COMPOSE_PROJECT_NAME}_devfront
env_file: .env
environment:
- APP_ENV=${APP_ENV:-production}
- API_PROXY_TARGET=http://backend:${BACKEND_PORT}
ports:
- "${DEVFRONT_PORT}:5173"
networks: [app_net]
orgfront:
build:
context: ../..
dockerfile: ./orgfront/Dockerfile
args:
VITE_ORGFRONT_PUBLIC_URL: ${ORGFRONT_URL}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY}
VITE_OIDC_CLIENT_ID: orgfront
container_name: ${COMPOSE_PROJECT_NAME}_orgfront
env_file: .env
environment:
- APP_ENV=${APP_ENV:-production}
- API_PROXY_TARGET=http://backend:${BACKEND_PORT}
- USERFRONT_URL=${USERFRONT_URL}
ports:
- "${ORGFRONT_PORT}:5175"
networks: [app_net]
networks:
app_net:
name: ${COMPOSE_PROJECT_NAME}_net
volumes:
db_data:
name: db_data_${INSTANCE_NAME}
ory_db_data:
name: ory_db_data_${INSTANCE_NAME}
clickhouse_data:
name: clickhouse_data_${INSTANCE_NAME}
oathkeeper_logs:
name: oathkeeper_logs_${INSTANCE_NAME}

View File

@@ -0,0 +1,42 @@
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
# 인스턴스별로 계산된 포트 주입
upstream backend_srv {
server backend:{{BACKEND_PORT}};
}
upstream oathkeeper_srv {
server oathkeeper:4455;
}
server {
listen 80;
# SSO 메인 도메인 및 API 처리
location /api {
proxy_pass http://backend_srv;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /auth {
proxy_pass http://oathkeeper_srv;
proxy_set_header Host $host;
}
location /oidc {
proxy_pass http://oathkeeper_srv;
proxy_set_header Host $host;
}
# 기본 정적 파일 (UserFront)
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
}

View File

@@ -0,0 +1,23 @@
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = {
authority:
import.meta.env.VITE_OIDC_AUTHORITY ||
`${window.location.protocol}//${window.location.hostname}:{{USERFRONT_PORT}}/oidc`,
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "orgfront",
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
scope: "openid offline_access profile email",
post_logout_redirect_uri: window.location.origin,
popup_redirect_uri: `${window.location.origin}/auth/callback`,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: false,
};
export const userManager = new UserManager({
...oidcConfig,
authority: oidcConfig.authority || "",
client_id: oidcConfig.client_id || "",
redirect_uri: oidcConfig.redirect_uri || "",
});

View File

@@ -0,0 +1,36 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
const allowedHosts = [
"{{ORGFRONT_DOMAIN}}",
"baron-orgchart.hmac.kr",
"localhost",
"127.0.0.1",
].filter(Boolean);
export default defineConfig({
plugins: [react()],
envPrefix: ["VITE_", "USERFRONT_"],
server: {
host: "127.0.0.1",
// 인스턴스별 도메인을 자동으로 허용합니다.
allowedHosts,
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
preview: {
host: "127.0.0.1",
port: 5175,
allowedHosts,
proxy: {
"/api": {
target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}",
changeOrigin: true,
},
},
},
});

View File

@@ -0,0 +1,102 @@
version: v26.2.0
dsn: ${KRATOS_DSN}
serve:
public:
base_url: http://localhost:4433/
cors:
enabled: true
allowed_origins:
- http://backend:{{BACKEND_PORT}}
- http://localhost:{{USERFRONT_PORT}}
admin:
base_url: http://localhost:4434/
session:
cookie:
domain: ${KRATOS_SESSION_COOKIE_DOMAIN}
same_site: Lax
path: /
selfservice:
default_browser_return_url: http://localhost:{{USERFRONT_PORT}}/
allowed_return_urls:
- http://localhost:{{USERFRONT_PORT}}
- http://localhost:{{USERFRONT_PORT}}/
- http://localhost:{{USERFRONT_PORT}}/ko
- http://localhost:{{USERFRONT_PORT}}/ko/
- http://localhost:{{USERFRONT_PORT}}/en
- http://localhost:{{USERFRONT_PORT}}/en/
- http://localhost:{{USERFRONT_PORT}}/auth/callback
- http://localhost:{{USERFRONT_PORT}}/ko/auth/callback
- http://localhost:{{USERFRONT_PORT}}/en/auth/callback
methods:
password:
enabled: true
link:
enabled: true
code:
enabled: true
passwordless_enabled: true
flows:
error:
ui_url: http://localhost:{{USERFRONT_PORT}}/error
settings:
ui_url: http://localhost:{{USERFRONT_PORT}}/error?error=settings_disabled
privileged_session_max_age: 15m
recovery:
ui_url: http://localhost:{{USERFRONT_PORT}}/recovery
use: code
verification:
ui_url: http://localhost:{{USERFRONT_PORT}}/verification
use: code
logout:
after:
default_browser_return_url: http://localhost:{{USERFRONT_PORT}}/login
login:
ui_url: http://localhost:{{USERFRONT_PORT}}/login
lifespan: 10m
registration:
ui_url: http://localhost:{{USERFRONT_PORT}}/registration
lifespan: 10m
log:
level: debug
format: text
leak_sensitive_values: true
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
cipher:
- 32-LONG-SECRET-NOT-SECURE-AT-ALL
ciphers:
algorithm: xchacha20-poly1305
hashers:
algorithm: bcrypt
bcrypt:
cost: 8
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
courier:
template_override_path: /etc/config/kratos/courier-templates
delivery_strategy: http
http:
request_config:
url: http://backend:{{BACKEND_PORT}}/api/v1/auth/webhooks/kratos-courier
method: POST
body: file:///etc/config/kratos/courier-http.jsonnet
headers:
Content-Type: application/json
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

View File

@@ -0,0 +1,159 @@
[
{
"id": "public-health",
"description": "공개 헬스체크",
"match": {
"url": "<.*>://<[^/]+>/health",
"methods": ["GET"]
},
"upstream": {
"url": "http://backend:{{BACKEND_PORT}}"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-preflight",
"description": "CORS preflight",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["OPTIONS"]
},
"upstream": {
"url": "http://backend:{{BACKEND_PORT}}"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "public-auth",
"description": "인증/회원가입 등 공개 엔드포인트",
"match": {
"url": "<.*>://<[^/]+>/api/v1/auth/<.*>",
"methods": ["GET", "POST", "OPTIONS"]
},
"upstream": {
"url": "http://backend:{{BACKEND_PORT}}"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "backend-command",
"description": "Command 요청은 Backend로 전달 (Audit 강제)",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["POST", "PUT", "PATCH", "DELETE"]
},
"upstream": {
"url": "http://backend:{{BACKEND_PORT}}"
},
"authenticators": [{ "handler": "cookie_session" }],
"authorizer": { "handler": "remote_json" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "backend-query",
"description": "Backend Query (admin/dev 포함)",
"match": {
"url": "<.*>://<[^/]+>/api/v1/<.*>",
"methods": ["GET"]
},
"upstream": {
"url": "http://backend:{{BACKEND_PORT}}"
},
"authenticators": [{ "handler": "cookie_session" }],
"authorizer": { "handler": "remote_json" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-well-known",
"description": "Hydra OIDC Discovery & JWKS",
"match": {
"url": "<.*>://<[^/]+>/.well-known/<.*>",
"methods": ["GET", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-well-known-oidc",
"description": "Hydra OIDC Discovery & JWKS (with /oidc prefix)",
"match": {
"url": "<.*>://<[^/]+>/oidc/.well-known/<.*>",
"methods": ["GET", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444",
"strip_path": "/oidc"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-oauth2",
"description": "Hydra OAuth2 Endpoints",
"match": {
"url": "<.*>://<[^/]+>/oauth2/<.*>",
"methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-oauth2-oidc",
"description": "Hydra OAuth2 Endpoints (with /oidc prefix)",
"match": {
"url": "<.*>://<[^/]+>/oidc/oauth2/<.*>",
"methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444",
"strip_path": "/oidc"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-userinfo",
"description": "Hydra Userinfo",
"match": {
"url": "<.*>://<[^/]+>/userinfo",
"methods": ["GET", "POST", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
},
{
"id": "hydra-userinfo-oidc",
"description": "Hydra Userinfo (with /oidc prefix)",
"match": {
"url": "<.*>://<[^/]+>/oidc/userinfo",
"methods": ["GET", "POST", "OPTIONS"]
},
"upstream": {
"url": "http://hydra:4444",
"strip_path": "/oidc"
},
"authenticators": [{ "handler": "noop" }],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
}
]

View File

@@ -0,0 +1,71 @@
# ISO8601 시간을 "YYYY-MM-DD HH:mm:ss" 형식으로 변환
map $time_iso8601 $time_custom {
"~^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})" "$1-$2-$3 $4:$5:$6";
}
# Go slog 포맷과 맞춘 JSON 액세스 로그
log_format json_combined escape=json
'{'
'"time":"$time_custom",'
'"level":"INFO",'
'"msg":"http_access",'
'"svc":"baron-userfront",'
'"status":$status,'
'"method":"$request_method",'
'"path":"$request_uri",'
'"latency":"${request_time}s",'
'"ip":"$remote_addr",'
'"forwarded_for":"$http_x_forwarded_for",'
'"user_agent":"$http_user_agent"'
'}';
server {
listen 5000;
include /etc/nginx/mime.types;
types {
application/javascript mjs;
application/wasm wasm;
}
error_log /dev/stderr warn;
access_log /var/log/nginx/access.log json_combined;
# --- Backend API Proxy ---
location /api {
proxy_pass http://backend:{{BACKEND_PORT}};
proxy_set_header Host $host;
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;
}
# --- UserFront Static Files ---
location ~* \.(js|css|html|json|mjs|wasm)$ {
root /usr/share/nginx/html;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri =404;
}
location ~* \.mjs$ {
root /usr/share/nginx/html;
default_type application/javascript;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri =404;
}
# dart2wasm 바이너리 MIME 명시
location ~* \.wasm$ {
root /usr/share/nginx/html;
default_type application/wasm;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri =404;
}
location / {
root /usr/share/nginx/html;
index index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri $uri/ /index.html;
}
}