Files
AriWeb-Suite/modules/webmaker/lib/recovery.js

34 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// ============================================================================
// 외부 작업 복구 목록 (v1.6.31, 요구 20) — 배포·도메인 구매·메일 계정·인증서·데이터 파일 중 정상 완료되지 않은 것을 모아 관리자에게 보인다.
//
// 항목 모양: { kind, id, siteId, siteTitle, userId, state, at, detail, userTodo, actions: [{ id, label }], needsAdmin }
// kind: deploy(마지막 배포가 준비 완료가 아님) · purchase(구매 원장 pending/unknown) · mail(메일 계정 deprovision_pending/needs-admin/login_conflict)
// · cert(와일드카드 인증서 만료 임박·갱신 실패) · data(손상 파일) · job(서버 재시작으로 끊긴 작업)
// actions 는 server.js 의 POST /api/admin/recovery/action 이 실행한다 (이미 끝난 부분은 다시 하지 않는다 — 각 실행 함수가 멱등).
// ============================================================================
const path = require('path');
function items(ctx) {
const { store, deploy, domains, mailsys, jsonstore, ssl } = ctx; const out = [];
const PHASE = { failed: '전송 실패', 'vhost-wait': '웹서버 설정 적용 대기', 'vhost-failed': '도메인 연결 실패', 'dns-wait': 'DNS 설정 대기', 'cert-wait': '인증서 준비 중(HTTP 로 열림)', 'check-failed': '접속 확인 실패', transferred: '전송 완료(확인 생략)' };
for (const s0 of store.list()) {
const site = store.load(s0.id); if (!site) continue;
const last = [...(site.deployments || [])].reverse().find((d) => d.kind !== 'unpublish'); if (!last) continue;
if (last.ok && last.ready && last.phase === 'ready') continue;
if (last.kind === 'rollback') continue;
const phase = last.phase || (last.ok ? 'transferred' : 'failed'); if (phase === 'transferred' && last.ok) continue;
const todo = phase === 'dns-wait' ? '사용자: 도메인 DNS 의 A 레코드가 이 서버를 가리키게 하고 전파를 기다린 뒤 "다시 배포"' : phase === 'cert-wait' ? '운영자: 와일드카드/사이트 인증서 발급 (관리자 콘솔 › 설정 › SSL)' : phase === 'vhost-failed' ? '운영자: 배포 서버 sudo·nginx 상태 확인(배포 서버 탭 연결 테스트) 뒤 "다시 배포"' : '사용자/운영자: 원인을 확인하고 "다시 배포"';
out.push({ kind: 'deploy', id: site.id, siteId: site.id, siteTitle: site.title, userId: site.ownerId, state: phase, stateLabel: PHASE[phase] || phase, at: last.at, detail: last.phaseDetail || last.detail || '', userTodo: todo, actions: [{ id: 'recheck', label: '다시 확인' } /* v1.6.33 */, { id: 'redeploy', label: '다시 배포' }, ...(last.hasPrev || (deploy.get(last.targetId) || {}).type === 'ssh' ? [{ id: 'rollback', label: '이전 배포본으로' }] : [])], needsAdmin: phase === 'vhost-failed' || phase === 'cert-wait' });
}
for (const p of domains.purchases({ open: true })) out.push({ kind: 'purchase', id: p.id, siteId: null, userId: p.userId, state: p.state, stateLabel: { pending_provider: '업체 호출 중/끊김', provider_succeeded: '업체 등록됨 · 차감 전', charged: '차감됨 · 기록 전', unknown: '확인 필요(응답 없음)' }[p.state] || p.state, at: p.updatedAt || p.at, detail: `${p.name} (${p.op}, ${p.period}년, 견적 ${p.quoteKrw}원, 업체 ${p.registrar})${p.error ? ' — ' + p.error : ''}`, userTodo: '관리자: "업체에 확인" 으로 등록 여부를 조회해 반영(등록됐으면 차감·기록, 아니면 실패로 닫음)', actions: [{ id: 'reconcile-purchase', label: '업체에 확인' }], needsAdmin: true });
// v1.6.32: 등록 업체가 비동기로 처리 중인 도메인(Route 53 Domains) — 완료 확인·영역 생성을 여기서도 할 수 있다
for (const x of domains.adminList().filter((x) => x.status === 'PENDING' || x.status === 'FAILED')) out.push({ kind: 'domain', id: x.id, siteId: null, userId: x.userId, state: x.status === 'FAILED' ? 'registration-failed' : 'registration-pending', stateLabel: x.status === 'FAILED' ? '도메인 등록 실패(업체)' : '도메인 등록 처리 중(업체)', at: x.createdAt, detail: `${x.name} (${x.registrar})${x.zoneError ? ' — ' + x.zoneError : ''}`, userTodo: x.status === 'FAILED' ? '관리자: AWS 콘솔 › Route 53 › 등록된 도메인 › 요청에서 실패 원인 확인. 비용이 차감됐으면 회원·청구에서 환불' : '관리자: 등록자 이메일 확인 요청(AWS)이 있으면 사용자에게 승인 안내, "등록 상태 확인" 으로 완료 반영', actions: x.status === 'FAILED' ? [] : [{ id: 'finalize-domain', label: '등록 상태 확인' }], needsAdmin: x.status === 'FAILED' });
for (const a of mailsys.listAccounts()) { if (a.state === 'active' || !a.state) continue; out.push({ kind: 'mail', id: `${a.siteId}:${a.memberId}`, siteId: a.siteId, siteTitle: (store.load(a.siteId) || {}).title, userId: (store.load(a.siteId) || {}).ownerId, state: a.state, stateLabel: { deprovision_pending: '메일 서버 계정 삭제 대기', 'needs-admin': '관리자 확인 필요(소유 불명확)' }[a.state] || a.state, at: a.updatedAt, detail: `${a.login || ''}${a.address ? ' <' + a.address + '>' : ''}${a.providerId ? ' · 공급자 id ' + a.providerId : ''}${a.lastError ? ' — ' + a.lastError : ''}`, userTodo: a.state === 'needs-admin' ? '관리자: 메일 서버(Arimail 관리자 페이지)에서 이 계정이 이 사이트 것인지 확인 → "소유 확인 후 삭제" 또는 "기록만 정리"' : '관리자: 메일 서버 연결을 확인한 뒤 "삭제 다시 시도"', actions: a.state === 'needs-admin' ? [{ id: 'mail-delete-confirmed', label: '소유 확인 후 삭제' }, { id: 'mail-forget', label: '기록만 정리(서버 계정 유지)' }] : [{ id: 'mail-delete-retry', label: '삭제 다시 시도' }], needsAdmin: true }); }
try { const st = ssl && ssl.lastState ? ssl.lastState() : null; if (st && st.last && !st.last.ok) out.push({ kind: 'cert', id: 'wildcard', state: 'issue-failed', stateLabel: '와일드카드 인증서 발급 실패', at: st.last.at, detail: st.last.detail, userTodo: '운영자: 관리자 콘솔 › 설정 › SSL 에서 원인(DNS TXT 등)을 확인하고 다시 발급', actions: [], needsAdmin: true }); } catch (_) {}
for (const h of jsonstore.health()) out.push({ kind: 'data', id: h.file, state: h.recoveredFromBackup ? 'recovered' : 'corrupt', stateLabel: h.recoveredFromBackup ? '손상 — 백업(.bak)으로 읽는 중' : '손상 — 읽을 수 없음', at: h.at, detail: `${path.basename(h.file)}: ${h.error}`, userTodo: '관리자: 서비스를 멈추고 .bak 또는 backups/ 의 백업으로 파일을 복구한 뒤 재시작 (docs/CONFIG.md 복구 절차)', actions: [], needsAdmin: true });
for (const s0 of store.list()) { const site = store.load(s0.id); if (!site) continue; const last = (site.commands || []).slice(-1)[0]; if (last && last.status === 'interrupted') out.push({ kind: 'job', id: site.id, siteId: site.id, siteTitle: site.title, userId: site.ownerId, state: 'interrupted', stateLabel: '서버 재시작으로 끊긴 작업', at: last.finishedAt || last.startedAt, detail: `#${last.no} ${last.kind}: ${String(last.instruction || '').slice(0, 80)}`, userTodo: '사용자: 수정 탭에서 같은 지시를 다시 실행 (지금까지의 변경은 보존됨)', actions: [], needsAdmin: false }); }
out.sort((a, b) => String(b.at || '').localeCompare(String(a.at || '')));
return out;
}
module.exports = { items };