Ariweb Suite 0.1.1 기반: 공통 라이브러리(db·http·세션·작업 엔진·할당/예약·알림·감사·보관소), 포털 뼈대, Webmaker/AriMail 도입 소스와 manifest

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-26 11:02:58 +00:00
co-authored by Claude Fable 5.1
parent 9ca4cee341
commit c7aea8d419
295 changed files with 90146 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# 운영 폴더를 지정 브랜치로 맞추고, 실행 중인 AI 작업이 없을 때 서비스를 재시작한다.
# 사용: deploy/deploy.sh [branch] (기본: 현재 브랜치)
set -euo pipefail
ROOT=/home/master/work/WebHoting_AI_Agent
BRANCH=${1:-}
cd "$ROOT"
if [ -n "$BRANCH" ]; then
if [ "$BRANCH" = "main" ]; then git checkout -q main; git reset -q --hard main;
else git checkout -q --detach "$BRANCH"; fi # 다른 worktree 가 쓰는 브랜치도 detach 로 안전하게 배포
fi
# 작업 트리가 HEAD 와 다르면(다른 worktree 에서 같은 브랜치에 커밋된 경우) 강제로 맞춘다
if ! git diff --quiet HEAD -- . ':!data'; then echo "작업 트리를 HEAD 에 맞춥니다"; git reset -q --hard HEAD; fi
echo "deploying $(git log --oneline -1)"
# 실행 중인 claude 작업 대기 (최대 20분)
for i in $(seq 1 240); do
if pgrep -u master -f "$HOME/.npm-global/bin/claude -p" >/dev/null 2>&1; then
[ $((i % 12)) -eq 1 ] && echo "AI 작업이 실행 중입니다. 끝날 때까지 기다립니다… ($((i*5))s)"
sleep 5
else break; fi
done
# nginx 설정도 함께 반영 (오류 페이지·프록시 설정 변경 포함)
if ! sudo cmp -s deploy/nginx-wmstats.conf /etc/nginx/conf.d/wmstats.conf; then sudo cp deploy/nginx-wmstats.conf /etc/nginx/conf.d/wmstats.conf && echo "nginx 통계 로그 형식 설치"; fi
if ! sudo cmp -s deploy/nginx-webhosting.conf /etc/nginx/sites-available/webhosting; then sudo cp deploy/nginx-webhosting.conf /etc/nginx/sites-available/webhosting && sudo nginx -t && sudo systemctl reload nginx && echo "nginx 설정 갱신"; fi
sudo cp deploy/webhosting-agent.service /etc/systemd/system/webhosting-agent.service
sudo systemctl daemon-reload
sudo systemctl restart webhosting-agent
sleep 1
systemctl --no-pager --lines=3 status webhosting-agent | head -5
@@ -0,0 +1,29 @@
# 공인 80/443 을 받는 프런트 nginx(예: 192.168.20.23)에 추가하는 webmaker.ibit.kr 전용 서버 블록.
# 1) 이 파일을 /etc/nginx/sites-available/webmaker.ibit.kr.conf 로 복사하고 sites-enabled 에 링크
# 2) sudo nginx -t && sudo systemctl reload nginx
# 3) sudo certbot --nginx -d webmaker.ibit.kr --redirect (Let's Encrypt 인증서 발급 + HTTPS 리디렉션 자동 추가)
# 다른 서비스의 server 블록은 건드리지 않는다. 오류 응답은 일반 안내 페이지로만 표시한다.
server {
listen 80;
listen [::]:80;
server_name webmaker.ibit.kr;
server_tokens off;
client_max_body_size 200m;
error_page 404 /__wm_404;
error_page 500 502 503 504 /__wm_50x;
location = /__wm_404 { internal; default_type text/html; return 404 '<!doctype html><html lang="ko"><head><meta charset="utf-8"><title>페이지를 찾을 수 없습니다</title></head><body style="font-family:sans-serif;text-align:center;padding:60px"><h1>페이지를 찾을 수 없습니다</h1><p>요청하신 페이지가 존재하지 않거나 이동되었습니다.</p></body></html>'; }
location = /__wm_50x { internal; default_type text/html; return 503 '<!doctype html><html lang="ko"><head><meta charset="utf-8"><title>일시적인 오류가 발생했습니다</title></head><body style="font-family:sans-serif;text-align:center;padding:60px"><h1>일시적인 오류가 발생했습니다</h1><p>잠시 후 다시 시도해 주세요.</p></body></html>'; }
location / {
proxy_pass http://192.168.20.233:80; # bear 의 웹서버(Caddy/nginx) → 127.0.0.1:3080 앱
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_intercept_errors on;
}
}
+184
View File
@@ -0,0 +1,184 @@
#!/bin/bash
# ============================================================================
# Arimail(구 mail-gateway) 설치·연동 (v1.5.7, 요구 4·5)
#
# 웹메이커의 "사이트 메일 시스템" 이 받는 메일·웹메일·IMAP/POP/SMTP 를 쓰려면 메일 서버 Arimail 이 있어야 한다.
# 이 스크립트는 Arimail 저장소의 표준 설치 절차(deploy/new-site.sh → setup.sh → deploy.sh)를 이 서버에서 자동으로 밟고,
# 끝나면 웹메이커 설정 파일(lib/config.js: mail.*)에 연동 정보를 등록한다. 이미 설치되어 있으면 등록만 한다.
#
# 사용 (서비스 서버에서 웹메이커 실행 계정으로, 비밀번호 없는 sudo 필요)
# 새로 설치 + 등록
# deploy/install-arimail.sh --host mail.example.com --mailbox admin@example.com
# [--source ~/work/mail-gateway | --bundle <git bundle> | --repo <git URL>] 소스(기본: ~/work/mail-gateway 가 있으면 그것, 없으면 --repo)
# [--user arimail] [--app-name arimail] [--port 8790] [--node <node 22.13+ 경로>] [--from-name "이름"] [--title "탭 제목"]
# [--limit-accounts N --limit-mailbox 500M --limit-attachment 20M …] (new-site.sh 옵션 그대로 전달)
# 이미 설치된(또는 다른 서버의) Arimail 등록만
# deploy/install-arimail.sh --register-only --url https://mail.example.com --domain example.com --admin-login webmaker-admin --admin-password '<비밀번호>' [--api-url http://127.0.0.1:8790]
# 이 서버에 설치된 Arimail 을 찾아 등록되어 있지 않으면 등록 (install-services.sh --check 가 부른다)
# deploy/install-arimail.sh --check
#
# 하는 일 (설치 모드)
# 1. 소스 확보: 로컬 체크아웃을 git bundle 로 묶거나(서버에 저장소 자격 증명을 두지 않음) 저장소를 복제한다.
# 2. 실행 계정(없으면 시스템 계정 생성)과 Node 22.13+ 확인(시스템 node 가 맞으면 그것, 아니면 /opt/<app>-node 에 바이너리).
# 3. Arimail 의 new-site.sh 로 사이트 설정(standalone, 로그인 ID@도메인 계정) → setup.sh(root: 디렉터리·환경 파일·systemd·sudoers·nginx·certbot 훅) → deploy.sh(실행 계정: 테스트·릴리스·마이그레이션·재시작).
# nginx 가 없는 서버에서는 setup.sh 가 nginx 단계에서 멈추므로 같은 템플릿(lib.sh render)으로 nginx 를 뺀 설치를 하고 프록시 설정 예시를 알려 준다.
# 4. 환경 파일(gateway.env)의 RESEND_API_KEY 가 비어 있으면 웹메이커 보관소의 메일 발송 키(resend)를 넣는다(같은 Resend 계정으로 보냄).
# 5. 웹메이커 전용 Arimail 관리자 계정(webmaker-admin)을 무작위 비밀번호로 만들고 → 웹메이커 설정 파일에 등록:
# mail.provider=arimail · mail.url=https://<host> · mail.apiUrl=http://127.0.0.1:<port> · mail.domain · mail.adminLogin · mail.adminPassword(보관소)
# 6. 남은 수동 작업(DNS A 레코드, Resend 웹훅 비밀값, 초기 관리자 admin/adminpasswd 비밀번호 변경)을 안내한다.
# 다시 실행해도 안전하다(있는 것은 건너뜀).
# ============================================================================
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
CFG() { (cd "$HERE" && node scripts/config.js "$@"); }
log() { echo "== $*"; }
die() { echo "오류: $*" >&2; exit 1; }
MODE=install; HOST=""; MAILBOX=""; URL=""; API_URL=""; DOMAIN=""; ADMIN_LOGIN=""; ADMIN_PW=""
APP_USER=arimail; APP_NAME=arimail; PORT=8790; NODE_BIN=""; SRC=""; BUNDLE=""; REPO="https://source.ibit.kr/OZ-Group/AriMail.git"; FROM_NAME=""; TITLE=""; SITE=""
EXTRA=()
while [ $# -gt 0 ]; do
case "$1" in
--host) HOST=$2; shift 2;; --mailbox) MAILBOX=$2; shift 2;; --url) URL=$2; shift 2;; --api-url) API_URL=$2; shift 2;; --domain) DOMAIN=$2; shift 2;;
--admin-login) ADMIN_LOGIN=$2; shift 2;; --admin-password) ADMIN_PW=$2; shift 2;;
--user) APP_USER=$2; shift 2;; --app-name) APP_NAME=$2; shift 2;; --port) PORT=$2; shift 2;; --node) NODE_BIN=$2; shift 2;;
--source) SRC=$2; shift 2;; --bundle) BUNDLE=$2; shift 2;; --repo) REPO=$2; shift 2;; --from-name) FROM_NAME=$2; shift 2;; --title) TITLE=$2; shift 2;; --site) SITE=$2; shift 2;;
--register-only) MODE=register; shift;; --check) MODE=check; shift;;
--limit-*|--large-*) EXTRA+=("$1" "$2"); shift 2;;
-h|--help) sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
*) die "알 수 없는 옵션: $1 (--help)";;
esac
done
# ---------- 공통: 웹메이커 설정 파일에 등록 ----------
register() { # register <공개 URL> <내부 API URL> <도메인> <관리자 ID> <비밀번호>
local url=$1 api=$2 dom=$3 login=$4 pw=$5
log "웹메이커 설정 파일에 Arimail 연동 등록 (mail.*)"
CFG set mail.provider arimail mail.url "$url" mail.apiUrl "${api:-$url}" mail.domain "$dom" mail.adminLogin "$login" mail.adminPassword "$pw"
(cd "$HERE" && node -e "require('./lib/connectors/arimail').status().then((s)=>{console.log(' 연결 확인: '+s.detail); process.exit(s.ok?0:3);})") || echo " (연결 확인 실패 — 주소·계정을 확인하거나 설정 페이지에서 다시 확인하세요)"
}
# Arimail 설치 안의 user-cli 로 웹메이커 전용 관리자 계정을 만들거나 비밀번호를 재설정한다. 인수: <APP_USER> <ETC_DIR> <NODE_BIN> <APP_DIR> <login> <pw>
ensure_admin_account() {
local user=$1 etc=$2 node=$3 app=$4 login=$5 pw=$6 envs
envs=$(sudo -n -u "$user" grep -E '^(DB_FILE|MAILBOX|LIMIT_ACCOUNTS)=' "$etc/gateway.env" | xargs)
if printf '%s' "$pw" | sudo -n -u "$user" env $envs PASSWORD="$pw" "$node" "$app/current/server/user-cli.mjs" add "$login" --admin --name "AI Webmaker" >/dev/null 2>&1; then echo " Arimail 관리자 계정 $login 생성"; else
printf '%s' "$pw" | sudo -n -u "$user" env $envs PASSWORD="$pw" "$node" "$app/current/server/user-cli.mjs" passwd "$login" >/dev/null && echo " Arimail 관리자 계정 $login 비밀번호 재설정"; fi
}
newpw() { openssl rand -base64 24 | tr -d '/+=' | cut -c1-24; }
# ---------- 등록만 ----------
if [ "$MODE" = register ]; then
[ -n "$URL" ] && [ -n "$DOMAIN" ] && [ -n "$ADMIN_LOGIN" ] || die "--register-only 에는 --url, --domain, --admin-login 이 필요합니다"
if [ -z "$ADMIN_PW" ]; then read -r -s -p "Arimail 관리자($ADMIN_LOGIN) 비밀번호: " ADMIN_PW; echo; fi
curl -fsS -m 8 "${API_URL:-$URL}/api/health" >/dev/null || die "Arimail 이 응답하지 않습니다: ${API_URL:-$URL}/api/health"
register "$URL" "$API_URL" "$DOMAIN" "$ADMIN_LOGIN" "$ADMIN_PW"; exit 0
fi
# ---------- 설치된 것 찾아 등록 (--check) ----------
if [ "$MODE" = check ]; then
if [ -n "$(CFG get mail.url)" ]; then echo "Arimail 연동이 이미 등록되어 있습니다: $(CFG get mail.url) (도메인 $(CFG get mail.domain))"; exit 0; fi
found=""
for f in /srv/*/site.env; do
[ -f "$f" ] || continue
# shellcheck disable=SC1090
eval "$(grep -E '^(APP_NAME|APP_USER|APP_DIR|ETC_DIR|NODE_BIN|PORT|MAIL_HOST|MAILBOX)=' "$f" | sed 's/#.*$//')"
[ -n "${APP_NAME:-}" ] && [ -n "${MAIL_HOST:-}" ] && [ -n "${MAILBOX:-}" ] || continue
systemctl is-active --quiet "$APP_NAME" 2>/dev/null || continue
found=$f; break
done
[ -n "$found" ] || { echo "이 서버에서 실행 중인 Arimail 을 찾지 못했습니다 (/srv/*/site.env). 새로 설치: $0 --host mail.<도메인> --mailbox admin@<도메인>"; exit 0; }
log "설치된 Arimail 발견: $APP_NAME ($MAIL_HOST, $MAILBOX) → 등록"
[ -x "$NODE_BIN" ] || NODE_BIN=$(command -v node)
PW=$(newpw); ensure_admin_account "$APP_USER" "$ETC_DIR" "$NODE_BIN" "$APP_DIR" webmaker-admin "$PW"
register "https://$MAIL_HOST" "http://127.0.0.1:$PORT" "${MAILBOX#*@}" webmaker-admin "$PW"; exit 0
fi
# ---------- 설치 ----------
[ -n "$HOST" ] && [ -n "$MAILBOX" ] || die "--host 와 --mailbox 가 필요합니다 (예: --host mail.example.com --mailbox admin@example.com)"
[[ "$MAILBOX" == *@*.* ]] || die "--mailbox 는 이메일 주소여야 합니다"
DOMAIN=${MAILBOX#*@}; [ -n "$SITE" ] || SITE=$(echo "$APP_NAME" | tr -c 'a-z0-9-\n' '-')
sudo -n true 2>/dev/null || die "비밀번호 없는 sudo 가 필요합니다"
command -v git >/dev/null || die "git 이 필요합니다 (apt install git)"
APP_DIR=/srv/$APP_NAME; ETC_DIR=/etc/$APP_NAME
log "Arimail 설치: $HOST · 기본 메일함 $MAILBOX · 서비스 $APP_NAME · 계정 $APP_USER · 포트 $PORT"
# 1. 소스 → git bundle (실행 계정이 읽을 수 있게 /tmp 에 644)
WORK=$(mktemp -d /tmp/arimail-install.XXXXXX); chmod 755 "$WORK"
if [ -z "$BUNDLE" ]; then
if [ -z "$SRC" ] && [ -d "$HOME/work/mail-gateway/.git" ]; then SRC=$HOME/work/mail-gateway; fi
if [ -n "$SRC" ]; then [ -d "$SRC/.git" ] || die "--source 는 git 체크아웃이어야 합니다: $SRC"; log "로컬 소스 $SRC → 번들"; git -C "$SRC" bundle create "$WORK/arimail.bundle" main >/dev/null 2>&1 || git -C "$SRC" bundle create "$WORK/arimail.bundle" HEAD:main >/dev/null;
else log "저장소 복제 $REPO"; git clone -q --depth 50 -b main "$REPO" "$WORK/src" || die "저장소를 복제할 수 없습니다 ($REPO). --source 또는 --bundle 로 소스를 주세요"; git -C "$WORK/src" bundle create "$WORK/arimail.bundle" main >/dev/null; fi
BUNDLE=$WORK/arimail.bundle
fi
# 실행 계정(APP_USER)이 읽어야 하므로 작업 폴더에 사본(644)을 둔다 (주어진 번들이 다른 계정 소유여도 되게)
if [ "$BUNDLE" != "$WORK/arimail.bundle" ]; then cp "$BUNDLE" "$WORK/arimail.bundle"; BUNDLE=$WORK/arimail.bundle; fi; chmod 644 "$BUNDLE"
# new-site.sh/setup.sh 를 실행할 작업 사본 — 복제가 되면 번들도 올바른 것이다 (git bundle verify 는 저장소 안에서만 동작해 쓰지 않는다)
git clone -q -b main "$BUNDLE" "$WORK/repo" || die "번들을 열 수 없습니다: $BUNDLE (git bundle create <파일> main 으로 만든 번들이어야 합니다)"
VER=$(node -p "require('$WORK/repo/package.json').version" 2>/dev/null || echo '?'); echo " Arimail(mail-gateway) 소스 버전 $VER"
# 2. Node 22.13+ 와 실행 계정
nodeok() { "$1" -e "const [a,b]=process.versions.node.split('.').map(Number); process.exit(a>22||(a===22&&b>=13)?0:1)" 2>/dev/null; }
if [ -z "$NODE_BIN" ]; then if command -v node >/dev/null && nodeok "$(command -v node)"; then NODE_BIN=$(command -v node); else NODE_BIN=/opt/$APP_NAME-node/bin/node; fi; fi
if ! [ -x "$NODE_BIN" ] || ! nodeok "$NODE_BIN"; then
log "Node 22.13+ 가 없어 nodejs.org 바이너리를 /opt/$APP_NAME-node 에 둡니다"
NV=v22.23.3; ARCH=$(uname -m); case "$ARCH" in x86_64) NA=x64;; aarch64) NA=arm64;; *) die "지원하지 않는 CPU: $ARCH";; esac
curl -fsSL "https://nodejs.org/dist/$NV/node-$NV-linux-$NA.tar.xz" -o "$WORK/node.tar.xz" || die "Node 내려받기 실패 — --node 로 22.13+ 실행 파일을 지정하세요"
sudo -n mkdir -p "/opt/$APP_NAME-node" && sudo -n tar -xJf "$WORK/node.tar.xz" -C "/opt/$APP_NAME-node" --strip-components=1; NODE_BIN=/opt/$APP_NAME-node/bin/node
fi
echo " node: $NODE_BIN ($("$NODE_BIN" --version))"
id "$APP_USER" >/dev/null 2>&1 || { sudo -n useradd --system --create-home --home-dir "/home/$APP_USER" --shell /usr/sbin/nologin "$APP_USER"; echo " 실행 계정 $APP_USER 생성"; }
# 3. 이미 설치되어 있으면 설치 단계는 건너뛴다
if [ -d "$APP_DIR/current" ] && systemctl list-unit-files "$APP_NAME.service" 2>/dev/null | grep -q "$APP_NAME.service"; then
log "$APP_NAME 이 이미 설치되어 있습니다 ($APP_DIR). 설치는 건너뛰고 등록만 합니다"
else
log "사이트 설정 파일 (new-site.sh)"
bash "$WORK/repo/deploy/new-site.sh" "$SITE" --host "$HOST" --mailbox "$MAILBOX" --user "$APP_USER" --app-name "$APP_NAME" --app-dir "$APP_DIR" --etc-dir "$ETC_DIR" --node "$NODE_BIN" --port "$PORT" ${FROM_NAME:+--from-name "$FROM_NAME"} ${TITLE:+--title "$TITLE"} --login standalone --repo "$REPO" --force "${EXTRA[@]}" | tail -1
if command -v nginx >/dev/null 2>&1; then
log "서버 준비 (setup.sh — 디렉터리·환경 파일·systemd·sudoers·nginx·certbot 훅)"
sudo -n env SITE="$SITE" SOURCE_BUNDLE="$BUNDLE" bash "$WORK/repo/deploy/setup.sh"
else
# nginx 가 없는 서버(Caddy 등): setup.sh 의 nginx 단계를 뺀 같은 작업을 템플릿으로 수행한다
log "nginx 가 없어 웹서버 설정을 뺀 준비를 합니다 (디렉터리·환경 파일·systemd·sudoers)"
sudo -n env SITE="$SITE" SOURCE_BUNDLE="$BUNDLE" REPO_WORK="$WORK/repo" bash -c '
set -euo pipefail; . "$REPO_WORK/deploy/lib.sh"; load_site "$REPO_WORK/deploy/sites/$SITE.env"
mkdir -p "$APP_DIR"/{repo,releases,data} "$ETC_DIR"; chown -R "$APP_USER:$APP_USER" "$APP_DIR"
install -o "$APP_USER" -g "$APP_USER" -m 644 "$REPO_WORK/deploy/sites/$SITE.env" "$APP_DIR/site.env"
if [ ! -f "$ETC_DIR/gateway.env" ]; then render "$REPO_WORK/deploy/gateway.env.in" "$ETC_DIR/gateway.env"; chown "$APP_USER:$APP_USER" "$ETC_DIR/gateway.env"; chmod 600 "$ETC_DIR/gateway.env"; fi
if [ ! -d "$APP_DIR/repo/.git" ]; then sudo -u "$APP_USER" git clone -q -b main "$SOURCE_BUNDLE" "$APP_DIR/repo"; sudo -u "$APP_USER" git -C "$APP_DIR/repo" remote set-url origin "$REPO_URL"; fi
render "$APP_DIR/repo/deploy/systemd/mail-gateway.service.in" "/etc/systemd/system/$APP_NAME.service"; chmod 644 "/etc/systemd/system/$APP_NAME.service"
sudoers_line > "/etc/sudoers.d/$APP_NAME"; chmod 440 "/etc/sudoers.d/$APP_NAME"; visudo -c -q
systemctl daemon-reload; systemctl enable -q "$APP_NAME"'
cat <<MSG
→ 웹서버에 $HOST 를 127.0.0.1:$PORT 로 프록시하는 설정을 직접 추가하세요. Caddy 예:
$HOST { reverse_proxy 127.0.0.1:$PORT }
(대용량 첨부 경로 /api/files 는 본문 크기 제한을 없애고, /dl/ 는 버퍼링 없이 — Arimail deploy/nginx/mail.conf.in 참고)
MSG
fi
# 4. 메일 발송 키: 웹메이커 보관소의 Resend 키를 Arimail 환경 파일에 (비어 있을 때만)
RESEND_KEY=$(cd "$HERE" && node -e "try{process.stdout.write(require('./lib/vault').secretOf('resend')||'')}catch(e){}")
if [ -n "$RESEND_KEY" ] && sudo -n -u "$APP_USER" grep -q '^RESEND_API_KEY=$' "$ETC_DIR/gateway.env"; then
printf '%s' "$RESEND_KEY" | sudo -n -u "$APP_USER" env ENVF="$ETC_DIR/gateway.env" bash -c 'k=$(cat); sed -i "s|^RESEND_API_KEY=.*|RESEND_API_KEY=$k|" "$ENVF"' && echo " gateway.env: RESEND_API_KEY ← 웹메이커 메일 발송 키"
fi
log "배포 (deploy.sh — 테스트·릴리스·DB 마이그레이션·서비스 시작)"
sudo -n -u "$APP_USER" env SOURCE_BUNDLE="$BUNDLE" "$APP_DIR/repo/deploy/deploy.sh"
fi
log "상태 확인"; for i in $(seq 1 30); do curl -fsS -m 3 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1 && break; sleep 1; [ "$i" = 30 ] && die "Arimail 이 응답하지 않습니다: journalctl -u $APP_NAME -n 50"; done
echo " $(curl -fsS -m 3 "http://127.0.0.1:$PORT/api/health")"
# 5. 웹메이커 전용 관리자 계정 → 설정 파일 등록
PW=$(newpw); ensure_admin_account "$APP_USER" "$ETC_DIR" "$NODE_BIN" "$APP_DIR" webmaker-admin "$PW"
register "https://$HOST" "http://127.0.0.1:$PORT" "$DOMAIN" webmaker-admin "$PW"
rm -rf "$WORK"
# 6. 남은 수동 작업
cat <<MSG
== 완료. Arimail $VER — 웹메일 https://$HOST/ 관리자 페이지 https://$HOST/admin (서비스 $APP_NAME, 내부 http://127.0.0.1:$PORT)
남은 작업:
1) DNS: $HOST A 레코드 → 이 서버 공인 IP (프록시 없이). 바뀌면 $APP_NAME-tls.timer 가 인증서를 받아 443·메일 포트 TLS 를 켭니다.
(바로: sudo bash $APP_DIR/repo/deploy/tls-when-ready.sh $APP_DIR/site.env)
2) Resend: 도메인 $DOMAIN 을 수신·발신 도메인으로 등록하고 Webhooks → https://$HOST/api/webhooks/resend (email.received) 의 Signing Secret 을
$ETC_DIR/gateway.env 의 RESEND_WEBHOOK_SECRET 에 넣은 뒤 sudo systemctl restart $APP_NAME
3) 초기 관리자 admin / adminpasswd 로 https://$HOST/admin 에 들어가 비밀번호를 바꾸세요 (웹메이커는 별도 계정 webmaker-admin 을 씁니다).
4) 웹메이커 관리자 콘솔 › 메일 시스템에서 메일 도메인이 $DOMAIN 인 사이트의 메일 시스템을 켜면 사이트 관리자 계정의 메일함이 Arimail 에 만들어집니다.
MSG
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# ============================================================================
# 서비스 서버 전용 "로컬 Gitea" 설치 (v1.4.14, 요구 8)
#
# 목적: 사이트 생성·수정 버전(브랜치 sites/<ID>/...)을 회사 소스 저장소(source.ibit.kr 의 OZ-Group/WebHosting_AI_agent)가
# 아니라 서비스 서버 안의 전용 Gitea 에 보관한다. 전용 계정(webmaker)과 비공개 저장소(webmaker/sites)를 만들고,
# 서비스가 그 계정의 SSH 키로 푸시하도록 systemd 환경 변수(SITES_GIT_REMOTE·SITES_GIT_WEB)를 바꾼다.
#
# 구성 (Docker, SQLite):
# 컨테이너 webmaker-gitea (gitea/gitea:1.27) 데이터 $DATA (기본 /srv/webmaker-gitea, 서비스 사용자 소유)
# HTTP $BIND:$HTTP_PORT (기본 LAN IP:3030 — 운영자가 브라우저로 버전을 볼 수 있게. 바깥에는 열지 않는다)
# SSH 127.0.0.1:$SSH_PORT (기본 2230 — 서비스가 같은 서버에서만 푸시하므로 로컬에만 연다)
# 가입 금지(DISABLE_REGISTRATION), 로그인해야 열람(REQUIRE_SIGNIN_VIEW), 저장소 기본 비공개
# 계정: webmaker-admin(관리자, 콘솔용) · webmaker(서비스 전용, 저장소 소유자) — 비밀번호는 $DATA/credentials.env (0600)
# SSH 키: ~/.ssh/id_ed25519_webmaker_gitea (없으면 생성) → webmaker 계정 키로 등록, ~/.ssh/config 에 Host webmaker-gitea
#
# 사용: deploy/install-gitea.sh [--bind IP] [--http-port 3030] [--ssh-port 2230] [--data /srv/webmaker-gitea]
# [--service webmaker] [--migrate-from <옛 원격 git 주소>] [--no-restart]
# 예) bear: deploy/install-gitea.sh --bind 192.168.20.23 --service webmaker --migrate-from git@source-webmaker:OZ-Group/WebHosting_AI_agent.git
# 66 : deploy/install-gitea.sh --bind 192.168.20.66 --service webhosting-agent --migrate-from git@source-webhosting:OZ-Group/WebHosting_AI_agent.git
# 전제: docker, 비밀번호 없는 sudo, curl, ssh-keygen. 다시 실행해도 안전하다(있는 것은 건너뜀).
# ============================================================================
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
BIND=""; HTTP_PORT=3030; SSH_PORT=2230; DATA=/srv/webmaker-gitea; SVC=""; MIGRATE=""; RESTART=1; IMAGE=gitea/gitea:1.27
while [ $# -gt 0 ]; do
case "$1" in
--bind) BIND=$2; shift 2;; --http-port) HTTP_PORT=$2; shift 2;; --ssh-port) SSH_PORT=$2; shift 2;; --data) DATA=$2; shift 2;;
--service) SVC=$2; shift 2;; --migrate-from) MIGRATE=$2; shift 2;; --no-restart) RESTART=0; shift;; --image) IMAGE=$2; shift 2;;
*) echo "알 수 없는 옵션: $1"; exit 1;;
esac
done
# 기본 바인드 주소: 첫 사설 IP (없으면 127.0.0.1). 운영자가 브라우저로 보려면 LAN IP 가 필요하다
[ -n "$BIND" ] || BIND=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' | head -1); [ -n "$BIND" ] || BIND=127.0.0.1
# 서비스 이름: 이 서버에 있는 유닛을 찾는다
if [ -z "$SVC" ]; then for c in webmaker webhosting-agent; do systemctl list-unit-files "$c.service" 2>/dev/null | grep -q "$c.service" && { SVC=$c; break; }; done; fi
UNIT=/etc/systemd/system/${SVC:-webmaker}.service
ME=$(id -un); MY_UID=$(id -u); MY_GID=$(id -g); HOSTN=$(hostname)
echo "== 로컬 Gitea 설치: 컨테이너 webmaker-gitea · HTTP $BIND:$HTTP_PORT · SSH 127.0.0.1:$SSH_PORT · 데이터 $DATA · 서비스 ${SVC:-?}"
command -v docker >/dev/null || { echo "docker 가 없습니다 (apt install docker.io)"; exit 1; }
sudo -n true 2>/dev/null || { echo "비밀번호 없는 sudo 가 필요합니다"; exit 1; }
DOCKER="docker"; docker info >/dev/null 2>&1 || DOCKER="sudo -n docker"
# ---- 1. 컨테이너 ----
sudo -n mkdir -p "$DATA" && sudo -n chown "$MY_UID:$MY_GID" "$DATA" && chmod 750 "$DATA"
if $DOCKER ps -a --format '{{.Names}}' | grep -qx webmaker-gitea; then
echo "-- 컨테이너가 이미 있습니다. 실행 상태로 맞춥니다"; $DOCKER start webmaker-gitea >/dev/null 2>&1 || true
else
echo "-- 이미지 $IMAGE 받기·컨테이너 만들기"
$DOCKER pull -q "$IMAGE" >/dev/null
# INSTALL_LOCK=true: 웹 설치 화면 없이 바로 쓴다. SSH_DOMAIN/SSH_PORT 는 화면에 보이는 clone 주소용(실제 포트는 -p 로 연 $SSH_PORT)
$DOCKER run -d --name webmaker-gitea --restart unless-stopped \
-e USER_UID="$MY_UID" -e USER_GID="$MY_GID" \
-e GITEA__database__DB_TYPE=sqlite3 -e GITEA__database__PATH=/data/gitea/gitea.db \
-e GITEA__server__ROOT_URL="http://$BIND:$HTTP_PORT/" -e GITEA__server__DOMAIN="$BIND" -e GITEA__server__HTTP_PORT=3000 \
-e GITEA__server__SSH_DOMAIN=127.0.0.1 -e GITEA__server__SSH_PORT="$SSH_PORT" -e GITEA__server__SSH_LISTEN_PORT=22 \
-e GITEA__security__INSTALL_LOCK=true \
-e GITEA__service__DISABLE_REGISTRATION=true -e GITEA__service__REQUIRE_SIGNIN_VIEW=true -e GITEA__service__SHOW_REGISTRATION_BUTTON=false \
-e GITEA__repository__DEFAULT_PRIVATE=private -e GITEA__repository__DEFAULT_PUSH_CREATE_PRIVATE=true \
-e GITEA__openid__ENABLE_OPENID_SIGNIN=false -e GITEA__openid__ENABLE_OPENID_SIGNUP=false \
-e GITEA__webhook__ALLOWED_HOST_LIST=private -e GITEA__other__SHOW_FOOTER_VERSION=false \
-e GITEA__ui__DEFAULT_THEME=gitea-auto -e GITEA__i18n__LANGS=ko-KR,en-US -e GITEA__i18n__NAMES="한국어,English" \
-p "$BIND:$HTTP_PORT:3000" -p "127.0.0.1:$SSH_PORT:22" \
-v "$DATA:/data" -v /etc/timezone:/etc/timezone:ro -v /etc/localtime:/etc/localtime:ro \
"$IMAGE" >/dev/null
fi
# REQUIRE_SIGNIN_VIEW 때문에 /api/v1/version 은 403 — 로그인 없이 열리는 /api/healthz 로 준비를 확인한다
echo "-- 준비 대기"; for i in $(seq 1 60); do curl -fsS -m 3 "http://$BIND:$HTTP_PORT/api/healthz" >/dev/null 2>&1 && break; sleep 2; [ "$i" = 60 ] && { echo "Gitea 가 120초 안에 응답하지 않습니다: $($DOCKER logs --tail 20 webmaker-gitea 2>&1 | tail -5)"; exit 1; }; done
echo " gitea $($DOCKER exec -u git webmaker-gitea gitea --version 2>/dev/null | awk '{print $3}')"
# ---- 2. 계정 ----
CRED="$DATA/credentials.env"
if [ -f "$CRED" ]; then . "$CRED"; else
ADMIN_PW=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24); WM_PW=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)
umask 077; printf 'ADMIN_USER=webmaker-admin\nADMIN_PW=%s\nWM_USER=webmaker\nWM_PW=%s\n' "$ADMIN_PW" "$WM_PW" > "$CRED"; umask 022
. "$CRED"
fi
gitea_cli() { $DOCKER exec -u git webmaker-gitea gitea "$@"; }
if gitea_cli admin user list 2>/dev/null | awk 'NR>1{print $2}' | grep -qx "$ADMIN_USER"; then echo "-- 관리자 계정 $ADMIN_USER 있음"; else
gitea_cli admin user create --admin --username "$ADMIN_USER" --password "$ADMIN_PW" --email "$ADMIN_USER@$HOSTN.local" --must-change-password=false >/dev/null && echo "-- 관리자 계정 $ADMIN_USER 생성"; fi
if gitea_cli admin user list 2>/dev/null | awk 'NR>1{print $2}' | grep -qx "$WM_USER"; then echo "-- 서비스 계정 $WM_USER 있음"; else
gitea_cli admin user create --username "$WM_USER" --password "$WM_PW" --email "$WM_USER@$HOSTN.local" --must-change-password=false >/dev/null && echo "-- 서비스 전용 계정 $WM_USER 생성"; fi
# ---- 3. 저장소 webmaker/sites ----
API="http://$BIND:$HTTP_PORT/api/v1"
code=$(curl -s -o /dev/null -w '%{http_code}' -u "$WM_USER:$WM_PW" "$API/repos/$WM_USER/sites")
if [ "$code" = "200" ]; then echo "-- 저장소 $WM_USER/sites 있음"; else
curl -fsS -u "$WM_USER:$WM_PW" -H 'Content-Type: application/json' -X POST "$API/user/repos" \
-d '{"name":"sites","private":true,"auto_init":false,"default_branch":"main","description":"AI Webmaker 사이트 소스 버전 (sites/<사이트ID>/main, sites/<사이트ID>/changes/NNNN-종류)"}' >/dev/null && echo "-- 저장소 $WM_USER/sites 생성 (비공개)"; fi
# ---- 4. SSH 키·설정 ----
KEY="$HOME/.ssh/id_ed25519_webmaker_gitea"; mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
[ -f "$KEY" ] || { ssh-keygen -q -t ed25519 -N '' -C "webmaker-service@$HOSTN" -f "$KEY"; echo "-- SSH 키 생성 $KEY"; }
PUB=$(cat "$KEY.pub")
if curl -fsS -u "$WM_USER:$WM_PW" "$API/user/keys" | grep -qF "$(echo "$PUB" | awk '{print $2}')"; then echo "-- 계정 키 등록됨"; else
curl -fsS -u "$WM_USER:$WM_PW" -H 'Content-Type: application/json' -X POST "$API/user/keys" -d "$(printf '{"title":"webmaker-service@%s","key":"%s","read_only":false}' "$HOSTN" "$PUB")" >/dev/null && echo "-- 공개키를 $WM_USER 계정에 등록"; fi
if ! grep -q '^Host webmaker-gitea$' "$HOME/.ssh/config" 2>/dev/null; then
printf '\n# AI Webmaker 로컬 Gitea (사이트 버전 보관, v1.4.14 deploy/install-gitea.sh)\nHost webmaker-gitea\n HostName 127.0.0.1\n Port %s\n User git\n IdentityFile %s\n IdentitiesOnly yes\n StrictHostKeyChecking accept-new\n' "$SSH_PORT" "$KEY" >> "$HOME/.ssh/config"; chmod 600 "$HOME/.ssh/config"; echo "-- ~/.ssh/config 에 Host webmaker-gitea 추가"
else
# 포트·키가 바뀌었으면 맞춘다
sed -i "/^Host webmaker-gitea$/,/^Host /{s#^ Port .*# Port $SSH_PORT#;s#^ IdentityFile .*# IdentityFile $KEY#}" "$HOME/.ssh/config"
fi
REMOTE="git@webmaker-gitea:$WM_USER/sites.git"; WEB="http://$BIND:$HTTP_PORT/$WM_USER/sites"
sleep 1; for i in 1 2 3 4 5; do GIT_SSH_COMMAND='ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new' git ls-remote "$REMOTE" >/dev/null 2>&1 && break; sleep 3; done
GIT_SSH_COMMAND='ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new' git ls-remote "$REMOTE" >/dev/null && echo "-- SSH 푸시 경로 확인: $REMOTE"
# ---- 5. 옛 저장소에서 사이트 브랜치 옮기기 ----
if [ -n "$MIGRATE" ]; then echo "== 마이그레이션 $MIGRATE → $REMOTE"; "$HERE/scripts/migrate-gitstore.sh" "$MIGRATE" "$REMOTE" --data-root "${DATA_ROOT:-$HERE/data}"; fi
# ---- 6. 웹메이커 설정 파일에 등록 (v1.5.7, 요구 6) ----
# 1.4.14~1.4.18 은 systemd 유닛의 Environment=SITES_GIT_REMOTE/WEB 을 고쳐 썼다. 이제 연동 정보는 설정 파일(data/config.json, source.*)에 두고
# 유닛의 옛 줄은 지운다(설정 파일이 우선이라 남아 있어도 무방하지만 두 곳에 있으면 헷갈린다). 서비스는 호출 때마다 설정을 읽으므로
# 푸시 주소는 재시작 없이도 다음 커밋부터 적용된다(--no-restart 로 재시작을 미룰 수 있다).
# API 토큰: 설정 페이지에서 상태 확인·저장소 만들기·공개키 등록을 API 로 하도록 webmaker 계정의 접근 토큰을 만들어 보관소에 넣는다(없을 때만).
CFGJS="$HERE/scripts/config.js"
[ -f "$HERE/data/config.json" ] || { [ -f "$UNIT" ] && (cd "$HERE" && node "$CFGJS" import-env --unit "$UNIT" >/dev/null) || true; }
HAS_TOKEN=$(cd "$HERE" && node -e "try{process.stdout.write(require('./lib/vault').secretOf('gitea')?'1':'')}catch(e){}")
TOKEN=""
if [ -z "$HAS_TOKEN" ]; then
TOKEN=$(curl -fsS -u "$WM_USER:$WM_PW" -H 'Content-Type: application/json' -X POST "$API/users/$WM_USER/tokens" -d "$(printf '{"name":"webmaker-service@%s-%s","scopes":["write:repository","write:user"]}' "$HOSTN" "$(date +%s)")" 2>/dev/null | sed -n 's/.*"sha1":"\([^"]*\)".*/\1/p' || true)
[ -n "$TOKEN" ] && echo "-- Gitea API 토큰 생성 ($WM_USER)" || echo "-- Gitea API 토큰을 만들지 못했습니다 (SSH 푸시만 사용, 설정 페이지에서 나중에 넣을 수 있음)"
fi
(cd "$HERE" && node "$CFGJS" set source.provider gitea source.remote "$REMOTE" source.web "$WEB" source.apiUrl "http://$BIND:$HTTP_PORT" source.user "$WM_USER" source.repo "$WM_USER/sites" ${SVC:+service.systemdUnit "$SVC"} ${TOKEN:+source.token "$TOKEN"})
echo "-- 설정 파일 $(cd "$HERE" && node "$CFGJS" path): source.remote=$REMOTE / source.web=$WEB / source.apiUrl=http://$BIND:$HTTP_PORT"
if [ -f "$UNIT" ] && sudo -n grep -qE '^Environment=SITES_GIT_(REMOTE|WEB|PREFIX)=' "$UNIT"; then
TMP=$(mktemp); sudo -n cat "$UNIT" | grep -vE '^Environment=SITES_GIT_(REMOTE|WEB|PREFIX)=' > "$TMP"; sudo -n install -m 644 "$TMP" "$UNIT"; rm -f "$TMP"; sudo -n systemctl daemon-reload
echo "-- $UNIT: 옛 SITES_GIT_* 환경 변수 제거 (설정 파일로 이전됨)"
fi
if [ "$RESTART" = 1 ] && [ -n "$SVC" ] && systemctl is-active --quiet "$SVC" 2>/dev/null; then
for i in $(seq 1 120); do pgrep -u "$ME" -f '[c]laude -p' >/dev/null 2>&1 && sleep 5 || break; done # 실행 중인 AI 작업이 끝날 때까지 기다린다
sudo -n systemctl restart "$SVC" && sleep 1 && echo "-- 서비스 $SVC 재시작: $(systemctl is-active "$SVC")"
fi
echo "== 완료. 웹: $WEB (계정 $ADMIN_USER / 비밀번호는 $CRED) · 서비스 푸시: $REMOTE"
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# ============================================================================
# 연동 서비스 설치·점검 (v1.5.7, 요구 4)
#
# 웹메이커가 제대로 동작하려면 함께 설치되어야 하는 서비스가 있다.
# - Gitea : 사이트 소스 버전 보관 (deploy/install-gitea.sh — Docker 컨테이너 webmaker-gitea)
# - Arimail: 받는 메일·웹메일·IMAP/POP/SMTP 메일 서버 (deploy/install-arimail.sh)
# 이 스크립트는 둘을 한 번에 다루고, 설치 뒤 설정 파일 점검(scripts/config.js check)과 초기 설정 페이지 주소를 알려 준다.
# deploy/install.sh · remote-install.sh 가 설치 끝에 부르고(배포 과정에 포함), remote-update.sh 는 --check 로 부른다.
#
# 사용:
# deploy/install-services.sh --gitea [--bind <LAN IP>] [--migrate-from <옛 원격>] Gitea 설치·등록
# deploy/install-services.sh --arimail --host mail.example.com --mailbox admin@example.com [install-arimail.sh 옵션…]
# deploy/install-services.sh --check 이미 설치된 서비스를 찾아 설정 파일에 등록되어 있지 않으면 등록만 (설치는 안 함)
# 공통: --service <유닛 이름> (기본: 설정 파일 service.systemdUnit, 없으면 webmaker/webhosting-agent 중 있는 것)
# ============================================================================
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
CFG() { (cd "$HERE" && node scripts/config.js "$@"); }
DO_GITEA=0; DO_ARIMAIL=0; CHECK=0; SVC=""; BIND=""; MIGRATE=""; ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--gitea) DO_GITEA=1; shift;; --arimail) DO_ARIMAIL=1; shift;; --check) CHECK=1; shift;;
--service) SVC=$2; shift 2;; --bind) BIND=$2; shift 2;; --migrate-from) MIGRATE=$2; shift 2;;
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
*) ARGS+=("$1"); shift;; # 나머지는 install-arimail.sh 로
esac
done
[ "$DO_GITEA$DO_ARIMAIL$CHECK" != "000" ] || { echo "옵션을 주세요: --gitea | --arimail … | --check (--help)"; exit 1; }
# 서비스 유닛 이름: 설정 파일 → 이 서버에 있는 유닛
[ -n "$SVC" ] || SVC=$(CFG get service.systemdUnit 2>/dev/null || true)
if [ -z "$SVC" ]; then for c in webmaker webhosting-agent; do systemctl list-unit-files "$c.service" 2>/dev/null | grep -q "$c.service" && { SVC=$c; break; }; done; fi
[ -n "$SVC" ] && CFG set service.systemdUnit "$SVC" >/dev/null 2>&1 || true
docker_cmd() { if docker info >/dev/null 2>&1; then echo docker; elif sudo -n docker info >/dev/null 2>&1; then echo "sudo -n docker"; fi; }
# ---- Gitea ----
if [ "$DO_GITEA" = 1 ]; then
if ! command -v docker >/dev/null 2>&1; then echo "== Gitea: docker 가 없어 건너뜁니다 (sudo apt install docker.io 뒤 deploy/install-gitea.sh --service $SVC)"; else
"$HERE/deploy/install-gitea.sh" ${SVC:+--service "$SVC"} ${BIND:+--bind "$BIND"} ${MIGRATE:+--migrate-from "$MIGRATE"} --no-restart
fi
elif [ "$CHECK" = 1 ]; then
D=$(docker_cmd)
if [ -n "$D" ] && $D ps -a --format '{{.Names}}' 2>/dev/null | grep -qx webmaker-gitea; then
if [ -z "$(CFG get source.remote)" ]; then echo "== Gitea 컨테이너는 있는데 설정 파일에 등록되지 않았습니다 → 등록"; "$HERE/deploy/install-gitea.sh" ${SVC:+--service "$SVC"} --no-restart; else echo "== Gitea: 등록됨 ($(CFG get source.remote))"; fi
else echo "== Gitea: 이 서버에 컨테이너(webmaker-gitea)가 없습니다. 설치: deploy/install-gitea.sh --service $SVC"; fi
fi
# ---- Arimail ----
if [ "$DO_ARIMAIL" = 1 ]; then "$HERE/deploy/install-arimail.sh" "${ARGS[@]}";
elif [ "$CHECK" = 1 ]; then "$HERE/deploy/install-arimail.sh" --check; fi
# ---- 마무리: 설정 점검과 초기 설정 페이지 주소 ----
echo "== 설정 파일 점검"; CFG check || true
if systemctl is-active --quiet webmaker-setup 2>/dev/null; then echo "== 초기 설정 페이지:"; CFG setup-url | sed 's/^/ /'; fi
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# 서버(192.168.20.66)에 nginx 설정과 systemd 서비스를 설치한다. sudo 필요.
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
sudo apt-get install -y nginx python3-docx >/dev/null
sudo mkdir -p /var/www/sites
sudo chown master:master /var/www/sites
sudo chmod 755 /var/www/sites
# v1.3.17 (B27): 사이트 설정이 쓰는 접속 통계 로그 형식(wmstats)을 먼저 설치 — 없으면 아래 nginx -t 가 실패해 설치가 멈췄다
sudo cp "$HERE/nginx-wmstats.conf" /etc/nginx/conf.d/wmstats.conf
sudo cp "$HERE/nginx-webhosting.conf" /etc/nginx/sites-available/webhosting
sudo ln -sf /etc/nginx/sites-available/webhosting /etc/nginx/sites-enabled/webhosting
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
# 방화벽(ufw): 사설망에서 80 포트 허용
if sudo ufw status | grep -q '^Status: active'; then
sudo ufw allow from 192.168.20.0/24 to any port 80 proto tcp comment 'AI web hosting agent (nginx)' >/dev/null
sudo ufw allow from 192.168.10.0/24 to any port 80 proto tcp comment 'AI web hosting agent (nginx) LAN 10' >/dev/null
fi
sudo systemctl reload nginx
sudo cp "$HERE/webhosting-agent.service" /etc/systemd/system/webhosting-agent.service
sudo systemctl daemon-reload
sudo systemctl enable --now webhosting-agent
sudo systemctl restart webhosting-agent
sleep 1
systemctl --no-pager --lines=5 status webhosting-agent
# v1.5.7 (요구 1~4): 설정 파일 · 초기 설정 페이지 · 연동 서비스
# - 유닛의 Environment= 값을 설정 파일(data/config.json)로 옮긴다(없는 항목만 — 기존 운영 값 보존)
# - 초기 설정 페이지 서비스(webmaker-setup, 기본 포트 8795 — v1.5.11)를 설치하고, 설정 페이지가 서비스를 재시작할 수 있게 sudoers 규칙을 둔다
# - 이미 설치된 연동 서비스(Gitea·Arimail)가 있으면 설정 파일에 등록한다
ROOT=$(cd "$HERE/.." && pwd)
(cd "$ROOT" && node scripts/config.js import-env --unit /etc/systemd/system/webhosting-agent.service && node scripts/config.js set service.systemdUnit webhosting-agent >/dev/null)
sudo cp "$HERE/webmaker-setup.service" /etc/systemd/system/webmaker-setup.service
printf '%s ALL=(root) NOPASSWD: /usr/bin/systemctl restart webhosting-agent, /usr/bin/systemctl restart webmaker-setup\n' "$(id -un)" | sudo tee /etc/sudoers.d/webmaker-setup >/dev/null && sudo chmod 440 /etc/sudoers.d/webmaker-setup
sudo systemctl daemon-reload && sudo systemctl enable --now webmaker-setup >/dev/null && sudo systemctl restart webmaker-setup
# v1.5.11 (요구 2): 설정 페이지 기본 포트 8795(모든 인터페이스) — 방화벽에 허용하고 1.5.7 의 3081 사설망 규칙은 지운다. 접속은 토큰·관리자 로그인·시도 제한으로 보호
SETUP_PORT=$(cd "$ROOT" && node scripts/config.js get service.setupPort); SETUP_PORT=${SETUP_PORT:-8795}
if sudo ufw status | grep -q '^Status: active'; then for net in 192.168.20.0/24 192.168.10.0/24; do sudo ufw delete allow from "$net" to any port 3081 proto tcp >/dev/null 2>&1 || true; done; sudo ufw allow "$SETUP_PORT"/tcp comment 'AI Webmaker setup page' >/dev/null; fi
(cd "$ROOT" && node scripts/backup-data.js --tag install 2>&1 | sed 's/^/ /') || true # v1.6.31 (요구 29)
"$HERE/install-services.sh" --check --service webhosting-agent || true
# v1.5.17 (요구 1): 서버에 미리 받아 둔 *.<플랫폼 도메인> 인증서가 있으면 알리고 서브 도메인 사이트에 적용 (서비스 기동 때도 자동)
echo "== 와일드카드 인증서"; (cd "$ROOT" && node scripts/ssl-wildcard.js --apply | sed 's/^/ /') || true
echo "UI: http://192.168.20.66/"
echo "== 초기 설정 페이지 (설치 점검표·설정 파일 — 접속 토큰 포함; 점검표가 끝나기 전에는 UI 첫 화면도 /setup 으로 넘어감):"; (cd "$ROOT" && node scripts/config.js setup-url | sed 's/^/ /')
@@ -0,0 +1,56 @@
# AI 웹호스팅 에이전트 — nginx 사이트 설정
# / -> 생성 UI (Node 서버, 127.0.0.1:3080)
# /<32자리 16진 ID>/... -> /var/www/sites/<ID>/... 정적 서비스
# 미등록 호스트 요청은 안내 페이지(404)로만 응답 — 어떤 서비스도 노출되지 않음
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
server_tokens off;
location / { default_type text/html; return 404 '<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>페이지를 찾을 수 없습니다</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;font-family:-apple-system,"Segoe UI","Noto Sans KR",sans-serif;background:#f4f5f7;color:#1f2933}main{text-align:center;padding:32px}h1{font-size:22px;margin:0 0 8px}p{color:#6b7684;margin:0}</style></head><body><main><h1>페이지를 찾을 수 없습니다</h1><p>요청하신 페이지가 존재하지 않거나 이동되었습니다.</p></main></body></html>'; }
}
server {
listen 80;
listen [::]:80;
server_name 192.168.20.66 localhost 127.0.0.1;
client_max_body_size 200m;
# 오류 페이지: 서버 종류·버전이나 다른 서비스의 내용이 드러나지 않도록 일반 안내로 대체
server_tokens off;
error_page 404 /__wh_404;
error_page 403 =404 /__wh_404;
error_page 500 502 503 504 /__wh_50x;
location = /__wh_404 { internal; default_type text/html; return 404 '<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>페이지를 찾을 수 없습니다</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;font-family:-apple-system,"Segoe UI","Noto Sans KR",sans-serif;background:#f4f5f7;color:#1f2933}main{text-align:center;padding:32px}h1{font-size:22px;margin:0 0 8px}p{color:#6b7684;margin:0}</style></head><body><main><h1>페이지를 찾을 수 없습니다</h1><p>요청하신 페이지가 존재하지 않거나 이동되었습니다.</p></main></body></html>'; }
location = /__wh_50x { internal; default_type text/html; return 503 '<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>일시적인 오류가 발생했습니다</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;font-family:-apple-system,"Segoe UI","Noto Sans KR",sans-serif;background:#f4f5f7;color:#1f2933}main{text-align:center;padding:32px}h1{font-size:22px;margin:0 0 8px}p{color:#6b7684;margin:0}</style></head><body><main><h1>일시적인 오류가 발생했습니다</h1><p>잠시 후 다시 시도해 주세요.</p></main></body></html>'; }
# 사용자 생성 사이트: http://192.168.20.66/<32hex>/ (index.html) 또는 /<32hex>/page.html
# 발행 취소된 사이트: 작업 폴더에 .unpublished 표식이 있으면 내용 대신 "사이트 수정 중" 안내 (v1.1.4)
location = /__wh_maint { internal; default_type text/html; add_header Cache-Control "no-store"; return 503 '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>Updating</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;font-family:-apple-system,"Segoe UI","Noto Sans KR","Noto Sans JP","Noto Sans SC",sans-serif;background:#f4f5f7;color:#1f2933}main{text-align:center;padding:32px 24px;max-width:640px}h1{font-size:28px;line-height:1.3;margin:0 0 18px}p{color:#4b5563;margin:6px 0;line-height:1.6;font-size:15px}.f{margin-top:26px;font-size:12px;color:#9aa3af}</style></head><body><main><h1>We&#39;re updating our site. Please check back soon.</h1><p lang="ko">사이트 개정 작업 중입니다. 잠시 후에 다시 접속해주세요.</p><p lang="ja">サイトの改訂作業中です。しばらくしてから再度アクセスしてください。</p><p lang="zh">网站正在改版中,请稍后再访问。</p><p lang="es">El sitio está en proceso de revisión. Vuelva a intentarlo en unos minutos.</p><p class="f">Webmaker is working .... supported by ibit.kr</p></main></body></html>'; }
location ~ "^/([a-f0-9]{32})(/|$)" {
root /var/www/sites;
index index.html;
if (-f $document_root/$1/.unpublished) { rewrite ^ /__wh_maint last; }
try_files $uri $uri/ =404;
add_header Cache-Control "no-cache";
# 접속 통계 (lib/stats.js 가 5분마다 읽음; 형식은 conf.d/wmstats.conf)
access_log /var/log/nginx/webmaker-sites.log wmstats;
location ~ /\. { return 404; }
}
# 생성 UI / API (SSE 지원을 위해 버퍼링 해제)
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_intercept_errors off;
}
}
@@ -0,0 +1,4 @@
# 웹메이커 접속 통계용 nginx 로그 형식 (http 컨텍스트에서 읽히도록 /etc/nginx/conf.d/ 에 설치)
# 필드: 시각(ms)|클라이언트IP|호스트|메서드|요청URI|상태|수신바이트(요청 전체)|송신바이트(응답 전체)|리퍼러|UA
# lib/stats.js 의 parseLine() 이 이 순서를 그대로 전제한다. 바꾸면 함께 바꿀 것.
log_format wmstats '$msec|$remote_addr|$host|$request_method|$request_uri|$status|$request_length|$bytes_sent|$http_referer|$http_user_agent';
+144
View File
@@ -0,0 +1,144 @@
#!/bin/bash
# 이 서비스(웹메이커)를 다른 서버에 설치하고 192.168.20.66 의 DB 를 복사한다.
# 복사 대상: data/users.json, data/sites/, data/git/(버전 저장소), data/billing.json, data/fx.json,
# data/deploy-targets.json(접속 정보만), settings.json 의 비밀 아닌 값(모델·환율·마진), 사이트 파일(/var/www/sites)
# 복사 제외(비밀값 — 새 서버 관리자 콘솔에서 다시 입력): settings.json 의 API 키/OAuth 토큰, session.secret, deploy-keys/(SSH 개인키), credentials.json(외부 연동 접속 정보, v1.3.1)
# v1.3.17 (B29·B32): settings.json 은 "비밀이 아닌 항목" 목록(허용 목록)만 옮긴다 — 외부 서비스 키(serviceApiKey)·수집 키(backlogKey)·
# 다른 AI 제공자 키(providers, session.secret 로 암호화되어 새 서버에서는 풀 수 없음)는 옮기지 않는다. data/site-keys/(사이트 원본 서버 접속 키)도 제외.
# 사용: deploy/remote-install.sh <ssh-host> [FQDN|-] [app-port] [sites-dir]
# 예: deploy/remote-install.sh bear webmaker.ibit.kr 3080 /var/www/webmaker-sites
# deploy/remote-install.sh newbox ← v1.5.11: FQDN 을 모르면 생략(또는 -). 서버 IP 로 설치하고 http://<IP>:8795/ 설정 페이지에서 나머지를 정한다
# 전제: <ssh-host> 에 비밀번호 없는 SSH 접속과 sudo 가 가능. 대상 서버의 기존 서비스(Caddy/nginx 등)는 건드리지 않고
# FQDN(또는 IP) 전용 사이트 블록만 추가한다.
# v1.5.11 (요구 1·2) 설치 중 입력을 받지 않는다. 처음 설치(FRESH=1 또는 FQDN 생략)는 이 PC 의 DB 를 복사하지 않고 빈 상태로 시작하며,
# v1.6.31: 고정 관리자 비밀번호는 만들지 않는다 — 설정 페이지(접속 토큰)에서 최초 관리자를 만든다. 설치가 끝나면 웹(설정 페이지)에서 나머지 설정을 잇는다.
set -euo pipefail
HOST=${1:?ssh host}; FQDN=${2:-}; PORT=${3:-3080}; SITES=${4:-/var/www/webmaker-sites}
[ "$FQDN" = "-" ] && FQDN=""
SRC=$(cd "$(dirname "$0")/.." && pwd)
APPDIR=/home/master/webmaker
echo "== 1/7 대상 서버 점검 ($HOST)"
ssh -o BatchMode=yes "$HOST" 'set -e; echo "user=$(id -un) host=$(hostname) os=$(. /etc/os-release; echo $PRETTY_NAME)"; sudo -n true && echo "sudo: ok"; command -v node && node --version || echo "node: missing"; command -v caddy && caddy version | head -1 || true; command -v nginx && nginx -v 2>&1 || true; (ss -tlnp 2>/dev/null | grep -E ":(80|443|'"$PORT"') " || true)'
# FQDN 이 없으면 서버의 대표 IP(공인 IP 가 있으면 그것, 없으면 첫 사설 IP)를 접속 주소로 쓴다
SERVER_IP=$(ssh "$HOST" "ip -4 -o addr show scope global | awk '{print \$4}' | cut -d/ -f1 | grep -vE '^(172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)' | head -1; ip -4 -o addr show scope global | awk '{print \$4}' | cut -d/ -f1 | head -1" | head -1)
if [ -z "$FQDN" ]; then FRESH=${FRESH:-1}; SITE_HOST=$SERVER_IP; PUBLIC_URL="http://$SERVER_IP"; echo " FQDN 없음 → 서버 IP $SERVER_IP 로 설치 (공개 주소 $PUBLIC_URL, 나중에 설정 페이지에서 바꿀 수 있음)"; else FRESH=${FRESH:-0}; SITE_HOST=$FQDN; PUBLIC_URL="https://$FQDN"; fi
echo "== 2/7 앱 코드 전송 (main 작업 트리, data 제외)"
ssh "$HOST" "mkdir -p $APPDIR"
tar -C "$SRC" --exclude=./data --exclude=./.git --exclude=./node_modules -cf - . | ssh "$HOST" "tar -C $APPDIR -xf -"
ssh "$HOST" "mkdir -p $APPDIR/data && sudo mkdir -p $SITES && sudo chown \$(id -un) $SITES && chmod 755 $SITES" # 사이트 폴더는 웹서버(www-data)가 읽어야 하므로 755
if [ "$FRESH" = "1" ]; then
echo "== 3/7 처음 설치 — DB 를 복사하지 않습니다 (빈 상태 — 최초 관리자는 설정 페이지에서 만듭니다)"
else
echo "== 3/7 DB 복사 (비밀값 제외)"
# v1.5.7: 설정 파일(config.json — 서버마다 다른 주소·연동 정보)과 초기 설정 페이지 토큰(setup.token)도 복사하지 않는다 (새 서버의 유닛·설정 페이지에서 새로 만듦)
tar -C "$SRC" --exclude=./data/settings.json --exclude=./data/session.secret --exclude=./data/deploy-keys --exclude=./data/credentials.json --exclude=./data/site-keys --exclude=./data/user-connections.json --exclude=./data/config.json --exclude=./data/setup.token -cf - ./data | ssh "$HOST" "tar -C $APPDIR -xf -"
# settings.json: 모델·환율·마진·LE 이메일만 복사, 인증 정보는 비움
python3 - "$SRC/data/settings.json" <<'PY' | ssh "$HOST" "cat > $APPDIR/data/settings.json && chmod 600 $APPDIR/data/settings.json"
import json,sys
try: s=json.load(open(sys.argv[1]))
except Exception: s={}
# 허용 목록: 비밀이 아닌 설정만 (v1.3.17)
KEEP=['defaultModel','tiers','defaultTier','fx','marginPct','letsEncryptEmail','maxEdits','signupCredit','theme','mailSystem','uiLang','passwordPolicy','tierGuide']
out={k:s[k] for k in KEEP if k in s}
m=s.get('mail') or {}
out['mail']={k:m[k] for k in ('defaultDomain','fromName','formBase') if k in m}
out['auth']={'mode':'server-login','apiKey':'','oauthToken':''}
print(json.dumps(out,ensure_ascii=False,indent=2))
PY
tar -C /var/www/sites -cf - . | ssh "$HOST" "tar -C $SITES -xf - && chmod -R a+rX $SITES"
fi
echo "== 4/7 node / claude CLI / 서비스 설치"
ssh "$HOST" bash -s "$APPDIR" "$PORT" "$SITES" "$SITE_HOST" "$PUBLIC_URL" "$FRESH" <<'REMOTE'
set -e; APPDIR=$1; PORT=$2; SITES=$3; FQDN=$4; PUBLIC_URL=$5; FRESH=$6
if ! command -v node >/dev/null 2>&1; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -n bash - >/dev/null && sudo -n apt-get install -y -q nodejs >/dev/null; fi
sudo -n apt-get install -y -q python3-docx git tar >/dev/null 2>&1 || true
if ! command -v codex >/dev/null 2>&1 && [ ! -x ~/.npm-global/bin/codex ]; then mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global && npm i -g @openai/codex >/dev/null 2>&1 && echo "codex CLI 설치됨"; fi
if ! command -v claude >/dev/null 2>&1 && [ ! -x ~/.npm-global/bin/claude ]; then mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global && npm i -g @anthropic-ai/claude-code >/dev/null 2>&1 && echo "claude CLI 설치됨"; fi
sudo -n cp "$APPDIR/deploy/nginx-wmstats.conf" /etc/nginx/conf.d/wmstats.conf 2>/dev/null || true # 접속 통계 로그 형식
sed -e "s#/home/master/work/WebHoting_AI_Agent#$APPDIR#g" -e "s#Environment=PORT=3080#Environment=PORT=$PORT#" -e "s#Environment=SITES_ROOT=/var/www/sites#Environment=SITES_ROOT=$SITES#" -e "s#Environment=PUBLIC_BASE_URL=http://192.168.20.66#Environment=PUBLIC_BASE_URL=$PUBLIC_URL#" -e "/^Environment=CLAUDE_BIN=/d" -e "s#^Environment=PATH=.*#Environment=PATH=$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin#" -e "s#^Environment=SITES_GIT_REMOTE=.*#Environment=SITES_GIT_REMOTE=#" -e "s#^Environment=HOME=.*#Environment=HOME=$HOME#" -e "s#^User=.*#User=$(id -un)#" -e "s#^Group=.*#Group=$(id -gn)#" "$APPDIR/deploy/webhosting-agent.service" | sudo -n tee /etc/systemd/system/webmaker.service >/dev/null
sed -i "s#/home/master/work/WebHoting_AI_Agent/hooks/guard.sh#$APPDIR/hooks/guard.sh#" "$APPDIR/hooks/settings.json"
sudo -n systemctl daemon-reload && sudo -n systemctl enable --now webmaker >/dev/null && sleep 1 && systemctl is-active webmaker
# v1.5.7 (요구 1~3): 유닛의 환경 변수를 설정 파일(data/config.json)로 옮기고, 초기 설정 페이지 서비스(webmaker-setup, 기본 포트 3081)와 재시작 sudoers 규칙을 설치
(cd "$APPDIR" && node scripts/config.js import-env --unit /etc/systemd/system/webmaker.service >/dev/null && node scripts/config.js set service.systemdUnit webmaker >/dev/null)
# v1.5.11: 본 서비스 유닛에 DATA_ROOT 가 있으면 설정 페이지 유닛에도 같은 값을 넣는다 (같은 설정 파일·관리자 계정·토큰)
DR=$(grep -oP '^Environment=DATA_ROOT=\K.*' /etc/systemd/system/webmaker.service | head -1 || true)
sed -e "s#/home/master/work/WebHoting_AI_Agent#$APPDIR#g" -e "s#^Environment=PATH=.*#Environment=PATH=$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin#" -e "s#^Environment=HOME=.*#Environment=HOME=$HOME#" -e "s#^User=.*#User=$(id -un)#" -e "s#^Group=.*#Group=$(id -gn)#" "$APPDIR/deploy/webmaker-setup.service" | { if [ -n "$DR" ]; then sed -e "s#^\# Environment=DATA_ROOT=.*#Environment=DATA_ROOT=$DR#"; else cat; fi; } | sudo -n tee /etc/systemd/system/webmaker-setup.service >/dev/null
printf '%s ALL=(root) NOPASSWD: /usr/bin/systemctl restart webmaker, /usr/bin/systemctl restart webmaker-setup\n' "$(id -un)" | sudo -n tee /etc/sudoers.d/webmaker-setup >/dev/null && sudo -n chmod 440 /etc/sudoers.d/webmaker-setup
sudo -n systemctl daemon-reload && sudo -n systemctl enable --now webmaker-setup >/dev/null && sudo -n systemctl restart webmaker-setup && echo "webmaker-setup: $(systemctl is-active webmaker-setup)"
# v1.5.11 (요구 2): 설정 페이지 포트(기본 8795, 모든 인터페이스)를 방화벽(ufw)이 켜져 있으면 허용 — http://<서버 IP>:8795/ 가 기본 접속 위치. 접속은 토큰·관리자 로그인·시도 제한으로 보호
SP=$(cd "$APPDIR" && node scripts/config.js get service.setupPort); SP=${SP:-8795}
if command -v ufw >/dev/null 2>&1 && sudo -n ufw status 2>/dev/null | grep -q '^Status: active'; then sudo -n ufw allow "$SP"/tcp comment 'AI Webmaker setup page' >/dev/null; fi
# v1.6.31 (요구 6): 처음 설치해도 고정 비밀번호 관리자를 만들지 않는다 — 최초 관리자는 설정 페이지(접속 토큰, 1회)에서 운영자가 아이디·비밀번호를 정해 만든다
if [ "$FRESH" = "1" ]; then (cd "$APPDIR" && DATA_ROOT="$APPDIR/data" node scripts/admin.js bootstrap); fi
echo "== 5/7 웹서버에 $FQDN 사이트 추가 (기존 서비스 유지, 오류는 일반 안내 페이지)" # FQDN 이 없으면 서버 IP 가 server_name
ERR404='<!doctype html><html lang="ko"><head><meta charset="utf-8"><title>페이지를 찾을 수 없습니다</title></head><body style="font-family:sans-serif;text-align:center;padding:60px"><h1>페이지를 찾을 수 없습니다</h1><p>요청하신 페이지가 존재하지 않거나 이동되었습니다.</p></body></html>'
# v1.5.27 (요구 5): 발행 취소 안내 페이지 — 영어 제목 + 한·일·중·서 안내 + "supported by <도메인>" (lib/maintpage.js 와 같은 문구; 도메인은 FQDN 의 상위 도메인)
WM_DOM=$([ -n "$FQDN" ] && echo "$FQDN" | awk -F. '{ if (NF>2) { for (i=2;i<=NF;i++) printf "%s%s", $i, (i<NF?".":"") } else print $0 }' || echo Webmaker)
MAINT=$(printf '%s' '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>Updating</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;font-family:-apple-system,"Segoe UI","Noto Sans KR","Noto Sans JP","Noto Sans SC",sans-serif;background:#f4f5f7;color:#1f2933}main{text-align:center;padding:32px 24px;max-width:640px}h1{font-size:28px;line-height:1.3;margin:0 0 18px}p{color:#4b5563;margin:6px 0;line-height:1.6;font-size:15px}.f{margin-top:26px;font-size:12px;color:#9aa3af}</style></head><body><main><h1>We&#39;re updating our site. Please check back soon.</h1><p lang="ko">사이트 개정 작업 중입니다. 잠시 후에 다시 접속해주세요.</p><p lang="ja">サイトの改訂作業中です。しばらくしてから再度アクセスしてください。</p><p lang="zh">网站正在改版中,请稍后再访问。</p><p lang="es">El sitio está en proceso de revisión. Vuelva a intentarlo en unos minutos.</p><p class="f">Webmaker is working .... supported by __WM_DOMAIN__</p></main></body></html>' | sed "s/__WM_DOMAIN__/$WM_DOM/g")
ERR50X='<!doctype html><html lang="ko"><head><meta charset="utf-8"><title>일시적인 오류가 발생했습니다</title></head><body style="font-family:sans-serif;text-align:center;padding:60px"><h1>일시적인 오류가 발생했습니다</h1><p>잠시 후 다시 시도해 주세요.</p></body></html>'
if command -v caddy >/dev/null 2>&1 && [ -f /etc/caddy/Caddyfile ]; then
sudo -n mkdir -p /etc/caddy/sites
# 공인 80/443 이 프런트 프록시를 거쳐 오므로 http:// 로 선언해 Caddy 의 자동 인증서 발급을 끈다(인증서는 프런트에서)
# v1.3.17 (B05·B06): 생성 사이트(/<32자리ID>/)는 Caddy 가 파일을 직접 내보낸다 — 예전에는 모든 경로를 앱으로 넘겨 사이트 주소가 404 였다.
# 발행 취소 표식(.unpublished)이 있으면 nginx 설정과 같이 "사이트 수정 중" 안내(503). 숨김 파일(.으로 시작)은 404.
sudo -n tee /etc/caddy/sites/webmaker.caddy >/dev/null <<CADDY
http://$FQDN {
encode gzip
handle_errors {
@nf expression {http.error.status_code} == 404
handle @nf {
header Content-Type "text/html; charset=utf-8"
respond \`$ERR404\` 404
}
header Content-Type "text/html; charset=utf-8"
respond \`$ERR50X\` {http.error.status_code}
}
@site path_regexp site ^/([a-f0-9]{32})(/|$)
handle @site {
root * $SITES
@dot path_regexp dot /\.
respond @dot 404
@unpub file /{re.site.1}/.unpublished
handle @unpub {
header Content-Type "text/html; charset=utf-8"
header Cache-Control "no-store"
respond \`$MAINT\` 503
}
header Cache-Control "no-cache"
file_server
}
handle {
reverse_proxy 127.0.0.1:$PORT {
flush_interval -1
}
}
}
CADDY
grep -q "import /etc/caddy/sites/\*.caddy" /etc/caddy/Caddyfile || echo 'import /etc/caddy/sites/*.caddy' | sudo -n tee -a /etc/caddy/Caddyfile >/dev/null
sudo -n caddy validate --config /etc/caddy/Caddyfile >/dev/null && sudo -n systemctl reload caddy && echo "caddy: $FQDN 사이트 추가·reload"
elif command -v nginx >/dev/null 2>&1; then
printf 'server {\n listen 80;\n server_name %s;\n server_tokens off;\n client_max_body_size 200m;\n error_page 404 /__wh_404; error_page 500 502 503 504 /__wh_50x;\n location = /__wh_404 { internal; default_type text/html; return 404 '"'"'%s'"'"'; }\n location = /__wh_50x { internal; default_type text/html; return 503 '"'"'%s'"'"'; }\n location = /__wh_maint { internal; default_type text/html; add_header Cache-Control "no-store"; return 503 '"'"'%s'"'"'; }\n # 생성된 사이트: https://FQDN/<32자리ID>/ — 발행 취소 표식(.unpublished)이 있으면 안내 페이지 (v1.3.17 B06)\n location ~ "^/([a-f0-9]{32})(/|$)" { root %s; index index.html; if (-f $document_root/$1/.unpublished) { rewrite ^ /__wh_maint last; } try_files $uri $uri/ =404; add_header Cache-Control "no-cache"; access_log /var/log/nginx/webmaker-sites.log wmstats; location ~ /\\. { return 404; } }\n location / { proxy_pass http://127.0.0.1:%s; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 3600s; }\n}\n' "$FQDN" "$ERR404" "$ERR50X" "$MAINT" "$SITES" "$PORT" | sudo -n tee /etc/nginx/sites-available/webmaker.conf >/dev/null
sudo -n ln -sf /etc/nginx/sites-available/webmaker.conf /etc/nginx/sites-enabled/webmaker.conf && sudo -n nginx -t && sudo -n systemctl reload nginx && echo "nginx: $FQDN 사이트 추가·reload"
else echo "웹서버(caddy/nginx)를 찾지 못했습니다. 앱은 127.0.0.1:$PORT 에서 실행 중입니다."; fi
if command -v ufw >/dev/null 2>&1 && sudo -n ufw status | grep -q '^Status: active'; then sudo -n ufw allow from 192.168.20.0/24 to any port 80 proto tcp >/dev/null; fi
echo "== 6/7 설치 확인"; curl -s -o /dev/null -w "local app: %{http_code}\n" "http://127.0.0.1:$PORT/login?direct=1"; curl -s -o /dev/null -w "via webserver (Host $FQDN): %{http_code} (설치 점검 전에는 / 가 /setup 으로 302)\n" -H "Host: $FQDN" http://127.0.0.1/
REMOTE
# v1.5.7 (요구 4): 연동 서비스 — Gitea 는 항상(docker 가 있으면), Arimail 은 ARIMAIL_HOST·ARIMAIL_MAILBOX 환경 변수를 주었을 때 함께 설치한다.
# 예: ARIMAIL_HOST=mail.example.com ARIMAIL_MAILBOX=admin@example.com deploy/remote-install.sh bear webmaker.example.com
# Arimail 소스는 이 PC 의 ~/work/mail-gateway 체크아웃을 git bundle 로 묶어 보낸다(서버에 저장소 자격 증명을 두지 않음).
echo "== 7/7 연동 서비스 (Gitea · Arimail)"
ssh "$HOST" "$APPDIR/deploy/install-services.sh --gitea --service webmaker" 2>&1 | sed 's/^/ /' || echo " Gitea 설치를 건너뛰었습니다 — 대상 서버에서 $APPDIR/deploy/install-gitea.sh --service webmaker 를 직접 실행하세요"
if [ -n "${ARIMAIL_HOST:-}" ] && [ -n "${ARIMAIL_MAILBOX:-}" ]; then
if [ -d "$HOME/work/mail-gateway/.git" ]; then git -C "$HOME/work/mail-gateway" bundle create /tmp/arimail-$$.bundle main >/dev/null 2>&1 && scp -q /tmp/arimail-$$.bundle "$HOST:/tmp/arimail.bundle" && rm -f /tmp/arimail-$$.bundle; BUNDLE_OPT="--bundle /tmp/arimail.bundle"; else BUNDLE_OPT=""; fi
ssh "$HOST" "$APPDIR/deploy/install-arimail.sh --host $ARIMAIL_HOST --mailbox $ARIMAIL_MAILBOX $BUNDLE_OPT ${ARIMAIL_OPTS:-}" 2>&1 | sed 's/^/ /' || echo " Arimail 설치 실패 — 대상 서버에서 $APPDIR/deploy/install-arimail.sh --host $ARIMAIL_HOST --mailbox $ARIMAIL_MAILBOX 를 직접 실행하세요"
else ssh "$HOST" "$APPDIR/deploy/install-arimail.sh --check" 2>&1 | sed 's/^/ /' || true; fi
# v1.5.17 (요구 1): 대상 서버에 이미 *.<도메인> 와일드카드 인증서가 있으면 인지해 알린다 (서브 도메인 사이트에 자동 적용됨)
echo "== 와일드카드 인증서"; ssh "$HOST" "cd $APPDIR && node scripts/ssl-wildcard.js" 2>&1 | sed 's/^/ /' || true
echo "== 완료. 설치 중 더 입력할 것은 없습니다 — 나머지는 웹에서 이어집니다:"
echo " 0) 초기 설정 페이지(설치 점검표: 관리자 비밀번호 · AI 엔진 인증 · 공개 주소 → 연동 정보)에서 설정하고 서비스를 재시작:"
ssh "$HOST" "cd $APPDIR && node scripts/config.js setup-url" | sed 's/^/ /'
echo " 서비스 첫 화면 $PUBLIC_URL/ 도 점검표가 끝나기 전에는 같은 설정 페이지(/setup)로 넘어갑니다. 최초 관리자는 설정 페이지의 "관리자 계정" 카드에서 만듭니다(고정 비밀번호 없음). 설치 점검이 끝나면 접속 토큰은 더 쓸 수 없습니다"
echo " (같은 화면이 서비스 안 https://$FQDN/setup 과 관리자 콘솔 › 시스템 설정 탭에도 있습니다)"
[ -n "$FQDN" ] && echo " 1) 대상 서버가 공인 80/443 을 직접 받으면 그 서버에서: sudo certbot --nginx -d $FQDN --redirect ; 다른 프런트 서버를 거치면 deploy/front-proxy-webmaker.conf 를 그 서버에 적용" || echo " 1) 도메인이 정해지면: 설정 페이지에서 서비스 공개 주소를 https://<도메인> 으로 바꾸고, 그 서버에서 nginx server_name 변경 후 sudo certbot --nginx -d <도메인> --redirect"
echo " 2) Claude 연결(서버 로그인/OAuth 토큰/API 키)은 설정 페이지의 \"AI 엔진 인증\" 에서(관리자 콘솔 › 설정과 같은 값), 배포 서버 탭에서 SSH 키 재생성·등록"
echo " 3) Gitea 를 건너뛰었다면 대상 서버에서 $APPDIR/deploy/install-gitea.sh --service webmaker [--migrate-from <옛 원격>]; Arimail 은 $APPDIR/deploy/install-arimail.sh --host mail.<도메인> --mailbox admin@<도메인>"
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# 다른 서버에 설치된 웹메이커의 "코드만" 갱신하고 재시작한다 (data/ 와 사이트 파일은 건드리지 않음).
# 사용: deploy/remote-update.sh <ssh-host> [app-dir] [service]
set -euo pipefail
HOST=${1:?ssh host}; APPDIR=${2:-/home/master/webmaker}; SVC=${3:-webmaker}
SRC=$(cd "$(dirname "$0")/.." && pwd)
echo "== 코드 전송 → $HOST:$APPDIR ($(git -C "$SRC" log --oneline -1 2>/dev/null || echo 'no git'))"
tar -C "$SRC" --exclude=./data --exclude=./.git --exclude=./node_modules -cf - . | ssh "$HOST" "tar -C $APPDIR -xf - && sed -i 's#/home/master/work/WebHoting_AI_Agent/hooks/guard.sh#$APPDIR/hooks/guard.sh#' $APPDIR/hooks/settings.json"
# v1.5.13: 재시작 전에 실행 중인 AI 작업이 끝날 때까지 기다린다 — claude 뿐 아니라 codex(OpenAI 서버 로그인 엔진)도. 최대 30분 (2026-10-14 11:29 UTC 배포가 codex 작업을 끊은 것을 보고 고침)
ssh "$HOST" "for i in \$(seq 1 360); do pgrep -u \$(id -un) -f '[c]laude -p|[c]odex exec' >/dev/null 2>&1 && { [ \$i -eq 1 ] && echo ' AI 작업 실행 중 — 끝날 때까지 기다립니다 (최대 30분)'; sleep 5; } || break; done; sudo -n systemctl restart $SVC && sleep 1 && systemctl is-active $SVC && curl -s -o /dev/null -w 'local app: %{http_code}\n' http://127.0.0.1:\$(grep -oP 'PORT=\K[0-9]+' /etc/systemd/system/$SVC.service | head -1)/login?direct=1"
# v1.5.7 (요구 1~4): 설정 파일로 옮기기(없는 항목만) · 초기 설정 페이지 서비스(webmaker-setup) 설치/재시작 · 설치된 연동 서비스 등록 점검
# v1.5.11 (요구 2): 설정 페이지 기본 포트 8795(0.0.0.0) — 유닛에 본 서비스와 같은 DATA_ROOT 를 넣고, 방화벽은 그 포트를 모든 곳에서 허용(토큰·관리자 로그인으로 보호), 옛 3081 규칙은 지운다
ssh "$HOST" bash -s "$APPDIR" "$SVC" <<'REMOTE'
set -e; APPDIR=$1; SVC=$2; cd "$APPDIR"
UNIT=/etc/systemd/system/$SVC.service
# 본 서비스 유닛의 DATA_ROOT(있으면)를 이 셸과 설정 페이지 유닛에도 쓴다 — 같은 설정 파일·관리자 계정·토큰을 보도록.
# (v1.5.11 배포 때 확인: 유닛이 DATA_ROOT=/data/webmaker 인 서버에서 이것을 빼먹으면 아래 node 명령이 앱 폴더의 data/ 에 엉뚱한 설정 파일을 만든다)
DR=$(grep -oP '^Environment=DATA_ROOT=\K.*' "$UNIT" | head -1 || true)
[ -n "$DR" ] && export DATA_ROOT="$DR"
node scripts/config.js import-env --unit "$UNIT" >/dev/null && node scripts/config.js set service.systemdUnit "$SVC" >/dev/null
mk_setup_unit() {
sed -e "s#/home/master/work/WebHoting_AI_Agent#$APPDIR#g" -e "s#^Environment=PATH=.*#Environment=PATH=$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin#" -e "s#^Environment=HOME=.*#Environment=HOME=$HOME#" -e "s#^User=.*#User=$(id -un)#" -e "s#^Group=.*#Group=$(id -gn)#" deploy/webmaker-setup.service \
| { if [ -n "$DR" ]; then sed -e "s#^\# Environment=DATA_ROOT=.*#Environment=DATA_ROOT=$DR#"; else cat; fi; } | sudo -n tee /etc/systemd/system/webmaker-setup.service >/dev/null
}
if [ ! -f /etc/systemd/system/webmaker-setup.service ] || { [ -n "$DR" ] && ! grep -q "^Environment=DATA_ROOT=$DR" /etc/systemd/system/webmaker-setup.service; }; then
mk_setup_unit
printf '%s ALL=(root) NOPASSWD: /usr/bin/systemctl restart %s, /usr/bin/systemctl restart webmaker-setup\n' "$(id -un)" "$SVC" | sudo -n tee /etc/sudoers.d/webmaker-setup >/dev/null && sudo -n chmod 440 /etc/sudoers.d/webmaker-setup
sudo -n systemctl daemon-reload && sudo -n systemctl enable --now webmaker-setup >/dev/null && echo "webmaker-setup 설치 (초기 설정 페이지)"
fi
sudo -n systemctl restart webmaker-setup; sleep 1
# 방화벽(ufw): 설정 페이지 포트를 허용(접속은 토큰·관리자 로그인·시도 제한으로 보호). 1.5.7 의 3081 사설망 규칙은 지운다
SP=$(node scripts/config.js get service.setupPort); SP=${SP:-8795}
if command -v ufw >/dev/null 2>&1 && sudo -n ufw status 2>/dev/null | grep -q '^Status: active'; then
for net in 192.168.20.0/24 192.168.10.0/24; do sudo -n ufw delete allow from "$net" to any port 3081 proto tcp >/dev/null 2>&1 || true; done
sudo -n ufw allow "$SP"/tcp comment 'AI Webmaker setup page' >/dev/null
fi
deploy/install-services.sh --check --service "$SVC" 2>&1 | sed 's/^/ /' || true
# v1.6.31 (요구 29): 재시작·코드 교체 전에 데이터를 백업한다 (DATA_ROOT/backups, 최근 10개 보관)
echo " == 데이터 백업"; node scripts/backup-data.js --tag update 2>&1 | sed 's/^/ /' || echo " 백업 실패 — 그래도 갱신을 계속합니다 (수동 백업 권장)"
# v1.5.31 (요구 4): 서비스 유닛의 보안 옵션(NoNewPrivileges·ProtectSystem) 점검 — 문제가 있으면 드롭인으로 보정하는 명령을 안내한다 (scripts/unit-check.js).
# FIX_UNIT=1 로 실행하면 드롭인을 만들고(재시작은 아래 서비스 재시작이 겸한다) 운영자가 유닛 파일을 손으로 고치지 않아도 된다.
echo " == 서비스 유닛 권한 점검"; if [ "${FIX_UNIT:-0}" = "1" ]; then sudo -n node scripts/unit-check.js --fix --unit "$SVC" 2>&1 | sed 's/^/ /' && sudo -n systemctl daemon-reload || true; else node scripts/unit-check.js --unit "$SVC" 2>&1 | sed 's/^/ /' || true; fi
# v1.5.17 (요구 1): 서버에 미리 받아 둔 와일드카드 인증서 감지 (적용은 재시작된 서비스가 기동 때 함)
echo " == 와일드카드 인증서"; node scripts/ssl-wildcard.js 2>&1 | sed 's/^/ /' || true
echo "초기 설정 페이지: $(node scripts/config.js setup-url | head -1)"
REMOTE
@@ -0,0 +1,28 @@
[Unit]
Description=AI Web Hosting Agent (Claude Opus 5.5 page builder)
After=network.target
[Service]
Type=simple
User=master
Group=master
WorkingDirectory=/home/master/work/WebHoting_AI_Agent
Environment=NODE_ENV=production
Environment=PORT=3080
Environment=HOST=127.0.0.1
Environment=SITES_ROOT=/var/www/sites
Environment=PUBLIC_BASE_URL=http://192.168.20.66
Environment=CLAUDE_MODEL=claude-opus-5-5
# CLAUDE_BIN 을 지정하지 않으면 PATH 와 알려진 설치 위치(~/.local/bin, ~/.npm-global/bin 등)에서 자동 탐색
Environment=PATH=/home/master/.local/bin:/home/master/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
Environment=HOME=/home/master
# 사이트 버전 저장소: v1.4.14 부터 서비스 서버 안의 전용 Gitea (deploy/install-gitea.sh 가 만들고 아래 두 값을 맞춘다)
Environment=SITES_GIT_REMOTE=git@webmaker-gitea:webmaker/sites.git
Environment=SITES_GIT_WEB=http://192.168.20.66:3030/webmaker/sites
ExecStart=/usr/bin/node /home/master/work/WebHoting_AI_Agent/server.js
Restart=always
RestartSec=3
UMask=0022
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,25 @@
[Unit]
# AI Webmaker 초기 설정 페이지 (v1.5.7, 요구 2) — 설정 파일(data/config.json)을 채우는 전용 웹 서비스. 본 서비스와 별도 포트(기본 3081).
# deploy/install.sh · remote-install.sh 가 경로·계정을 sed 로 바꿔 /etc/systemd/system/webmaker-setup.service 로 설치한다.
Description=AI Webmaker setup page (config editor on a dedicated port)
After=network.target
[Service]
Type=simple
User=master
Group=master
WorkingDirectory=/home/master/work/WebHoting_AI_Agent
Environment=NODE_ENV=production
# 본 서비스와 같은 데이터 폴더를 써야 같은 설정 파일·보관소·관리자 계정을 본다. (DATA_ROOT 를 바꿨다면 두 유닛 모두 같게)
# Environment=DATA_ROOT=/home/master/work/WebHoting_AI_Agent/data
# 포트·바인드 주소는 설정 파일 service.setupPort / service.setupBind (v1.5.11 기본 8795 / 0.0.0.0 — 겹치면 8796… 자동). 환경 변수 WEBMAKER_SETUP_PORT 로도 줄 수 있다.
# 본 서비스 유닛에 DATA_ROOT 가 있으면 설치 스크립트가 아래 줄을 같은 값으로 채운다 (같은 설정 파일·관리자 계정·접속 토큰을 보도록).
Environment=PATH=/home/master/.local/bin:/home/master/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
Environment=HOME=/home/master
ExecStart=/usr/bin/node /home/master/work/WebHoting_AI_Agent/setup-server.js
Restart=always
RestartSec=3
UMask=0022
[Install]
WantedBy=multi-user.target