Files
AriWeb-Suite/portal/domains/public/domains.js
T

23 lines
9.3 KiB
JavaScript
Raw Normal View History

/* portal/domains/public/domains.js — 도메인 화면 스크립트(목록·연결·상세). */
(() => {
const $ = (s, r = document) => r.querySelector(s); const esc = aw.esc; const fmt = aw.fmt;
const OWN = { pending: ['warning', '확인 전'], verified: ['ok', '확인됨'] }; const DNS = { pending: ['warning', '미반영'], wrong: ['failed', '잘못된 값'], ok: ['ok', '정상'], customer: ['neutral', '고객 관리'], 'n/a': ['neutral', '해당 없음'] }; const CERT = { none: ['neutral', '없음'], issuing: ['running', '발급 중'], valid: ['ok', '정상'], expiring_soon: ['warning', '만료 임박'], failed: ['failed', '실패'], customer_managed: ['neutral', '고객 관리'] }; const MDNS = { ok: ['ok', '정상'], warn: ['warning', '권장 개선'], error: ['failed', '오류'], unknown: ['neutral', '확인 전'] };
const badge = (m, k) => { const v = m[k] || ['neutral', k || '-']; return `<span class="badge ${v[0]}">${({ ok: '●', failed: '✘', warning: '!', running: '◌', neutral: '○' })[v[0]]} ${esc(v[1])}</span>`; };
const onErr = (el, e, retry) => aw.state(el, e.status === 403 ? 'forbidden' : e.code === 'service_unreachable' || e.status === 503 ? 'stale' : 'failed', e.message, { retry });
async function list() { const el = $('#list'); try { const r = await aw.api('GET', '/api/domains'); if (!r.domains.length) return aw.state(el, 'empty', '연결된 도메인이 없습니다', { href: '/domains/connect', hrefLabel: '도메인 연결' }); el.innerHTML = `<table class="r"><thead><tr><th>도메인</th><th>연결</th><th>용도</th><th>소유 확인</th><th>DNS</th><th>HTTPS</th><th>메일 보안</th><th>마지막 확인</th></tr></thead><tbody>${r.domains.map((d) => `<tr><td data-l="도메인"><a href="/domains/${d.id}"><b>${esc(d.name)}</b></a>${d.isPrimary ? ' ★' : ''}${d.actionNeeded ? `<br><a href="${esc(d.actionNeeded.href)}">! ${esc(d.actionNeeded.text)}</a>` : ''}</td><td data-l="연결">${d.site ? `사이트 <a href="/sites/${d.site.id}">${esc(d.site.name)}</a>` : ''}${d.mailService ? `<br>메일 <a href="/mail/${d.mailService.id}">${esc(d.mailService.name)}</a>` : ''}${!d.site && !d.mailService ? '<span class="muted">없음</span>' : ''}</td><td data-l="용도">${esc({ web: '웹', mail: '메일', both: '웹+메일' }[d.purpose])}</td><td data-l="소유">${badge(OWN, d.ownershipState)}</td><td data-l="DNS">${badge(DNS, d.dnsState)}</td><td data-l="HTTPS">${badge(CERT, d.certState)}${d.certExpiresAt ? `<br><span class="sub">만료 ${esc(d.certExpiresAt.slice(0, 10))}</span>` : ''}</td><td data-l="메일 보안">${d.mailDnsState ? badge(MDNS, d.mailDnsState) : '-'}</td><td data-l="확인">${d.lastCheckedAt ? esc(fmt(d.lastCheckedAt)) : '-'}${d.recentFailures ? `<br><span class="err">최근 실패 ${d.recentFailures}회</span>` : ''}</td></tr>`).join('')}</tbody></table>`; } catch (e) { onErr(el, e, list); } }
async function connect() { const f = $('#connect'); const sel = $('#siteId'); try { const r = await aw.api('GET', '/api/sites'); sel.innerHTML = '<option value="">선택 안 함</option>' + r.sites.map((s) => `<option value="${s.id}" ${f.dataset.site === s.id ? 'selected' : ''}>${esc(s.name)} (${esc(s.lifecycle)})</option>`).join(''); } catch (_) {}
f.onsubmit = async (e) => { e.preventDefault(); f.querySelectorAll('.err').forEach((x) => { x.textContent = ''; }); const fd = new FormData(f); try { const r = await aw.api('POST', '/api/domains', { name: fd.get('name'), purpose: fd.get('purpose'), siteId: fd.get('siteId') || null, makePrimary: fd.get('makePrimary') === 'on' }); location.href = `/domains/${r.domain.id}`; } catch (err) { (f.querySelector(`[data-err="${err.detail && err.detail.field || 'form'}"]`) || f.querySelector('[data-err=form]')).innerHTML = esc(err.message) + (err.detail && err.detail.href ? ` <a href="${esc(err.detail.href)}">열기</a>` : ''); } }; }
async function detail() { const el = $('#detail'); const id = el.dataset.domain;
const load = async () => { try { const r = await aw.api('GET', `/api/domains/${id}`); const d = r.domain; const job = d.job;
el.innerHTML = `<div class="cards"><div class="card"><h2>상태</h2><dl class="kv"><dt>용도</dt><dd>${esc({ web: '웹', mail: '메일', both: '웹+메일' }[d.purpose])}</dd><dt>연결</dt><dd>${d.site ? `사이트 <a href="/sites/${d.site.id}">${esc(d.site.name)}</a>${d.site.deployMode === 'external' ? ' (외부 배포 — DNS·인증서는 고객 관리)' : ''}` : '사이트 없음'}${d.mailService ? ` · 메일 <a href="/mail/${d.mailService.id}">${esc(d.mailService.name)}</a>` : ''}</dd><dt>대표 도메인</dt><dd>${d.isPrimary ? '★ 대표' : d.site ? '<button class="btn" id="btnPrimary">대표로 지정</button>' : '-'}${d.redirectFrom ? `<br><span class="sub">이전 주소 ${esc(d.redirectFrom)} 는 이 도메인으로 이동 처리</span>` : ''}</dd><dt>소유 확인</dt><dd>${badge(OWN, d.ownershipState)}</dd><dt>DNS</dt><dd>${badge(DNS, d.dnsState)}${d.dnsDetail && d.dnsDetail.got ? ` <span class="sub">지금 값: ${esc(d.dnsDetail.got.join(', ') || '없음')} / 필요 ${esc(d.dnsDetail.expected)}</span>` : ''}</dd><dt>HTTPS 인증서</dt><dd>${badge(CERT, d.certState)}${d.certExpiresAt ? ` <span class="sub">만료 ${esc(d.certExpiresAt.slice(0, 10))}</span>` : ''}${d.certError ? `<br><span class="err">${esc(d.certError)}</span>` : ''}</dd>${d.mailDnsState ? `<dt>메일 보안 레코드</dt><dd>${badge(MDNS, d.mailDnsState)} <a href="/mail?domain=${esc(d.name)}">메일 화면에서 확인</a></dd>` : ''}<dt>마지막 외부 확인</dt><dd>${d.externalCheck ? `${d.externalCheck.ok ? '정상' : '실패'} (${d.externalCheck.status || '-'}, TLS ${d.externalCheck.tls && d.externalCheck.tls.valid ? '유효' : '무효/미확인'})` : '-'}</dd><dt>마지막 확인</dt><dd>${d.lastCheckedAt ? esc(fmt(d.lastCheckedAt)) : '-'}</dd></dl><div class="actions"><button class="btn primary" id="btnCheck">다시 확인</button><button class="btn danger" id="btnRelease">연결 해제</button></div></div>
<div class="card"><h2>필요한 DNS 값</h2><p class="hint">DNS 를 직접 관리하면 아래 값을 그대로 넣으세요. 미반영(레코드 없음)과 잘못된 값(다른 값)을 구분해 표시합니다.</p><table class="r"><thead><tr><th>종류</th><th>이름</th><th>값</th><th>용도</th><th>상태</th></tr></thead><tbody>${r.records.map((x) => `<tr><td data-l="종류">${esc(x.type)}</td><td data-l="이름"><span class="mono copy" data-action="copy" data-copy="${esc(x.name)}">${esc(x.name)}</span></td><td data-l="값">${x.value ? `<span class="mono copy" data-action="copy" data-copy="${esc(x.value)}">${esc(x.value)}</span>` : `<span class="muted">${esc(x.note || '-')}</span>`}</td><td data-l="용도">${esc(x.purpose)}</td><td data-l="상태">${esc({ ok: '정상', pending: '미반영', wrong: '잘못된 값', optional: '선택', customer: '고객 관리', unknown: '확인 전', 'n/a': '-' }[x.state] || x.state || '-')}</td></tr>`).join('')}</tbody></table></div></div>
<div class="card"><h2>연결 진행</h2><div id="job"></div></div><div class="card"><h2>확인 기록</h2>${d.checks.length ? `<table class="r"><thead><tr><th>시각</th><th>종류</th><th>결과</th></tr></thead><tbody>${d.checks.map((c) => `<tr><td data-l="시각">${esc(fmt(c.checked_at))}</td><td data-l="종류">${esc(c.kind)}</td><td data-l="결과">${c.ok ? '✔ 성공' : '✘ 실패'} <span class="sub">${esc(JSON.stringify(c.detail || {})).slice(0, 160)}</span></td></tr>`).join('')}</tbody></table>` : '<p class="muted">기록 없음</p>'}</div>`;
if (job) aw.job($('#job'), job, { onAction: async (a, j) => { try { await aw.api('POST', `/api/domains/${id}/jobs/${j.id}/${a === 'go' ? 'resume' : a}`); load(); } catch (e) { aw.toast(e.message); } } }); else $('#job').innerHTML = '<p class="muted">진행 중인 확인 작업이 없습니다. "다시 확인" 으로 시작하세요</p>';
$('#btnCheck').onclick = async () => { try { await aw.api('POST', `/api/domains/${id}/check`, {}); aw.toast('확인을 시작했습니다'); load(); } catch (e) { aw.toast(e.message); } };
const bp = $('#btnPrimary'); if (bp) bp.onclick = async () => { const redirect = confirm('기존 대표 주소를 새 대표 도메인으로 이동(301) 처리할까요?'); try { await aw.api('POST', `/api/domains/${id}/primary`, { redirectOld: redirect }); aw.toast('대표 도메인으로 지정했습니다'); load(); } catch (e) { aw.toast(e.message + (e.detail ? ` (소유 ${e.detail.ownership}, DNS ${e.detail.dns}, 인증서 ${e.detail.cert})` : '')); } };
$('#btnRelease').onclick = async () => { if (!(await aw.confirmDanger({ title: '도메인 연결 해제', target: d.name, impacts: [d.isPrimary ? '대표 도메인이라 사이트가 "도메인 설정 필요" 상태가 됩니다' : '이 도메인으로의 접속이 끊깁니다', '인증서·DNS 상태 기록이 지워집니다(도메인 자체는 등록기관에 그대로 있음)'], typeToConfirm: d.name }))) return; try { await aw.api('DELETE', `/api/domains/${id}`); location.href = '/domains'; } catch (e) { aw.toast(e.message); } };
return !!(job && ['queued', 'running'].includes(job.status)); } catch (e) { onErr(el, e, load); return false; } };
if (await load()) aw.poll(load, 4000); }
const page = document.querySelector('[data-page]')?.dataset.page; ({ 'domains-list': list, 'domains-connect': connect, 'domains-detail': detail }[page] || (() => {}))();
})();