174 lines
8.7 KiB
HTML
174 lines
8.7 KiB
HTML
{% extends 'layout/base.html' %}
|
||
{% block title %}Домены · Админка{% endblock %}
|
||
{% block body %}
|
||
<div class="content">
|
||
{% include 'admin/_flash.html' %}
|
||
|
||
<div class="card">
|
||
<div class="card-head" style="display:flex;align-items:center">
|
||
<div class="card-title">Домены ({{ domains|length }})</div>
|
||
<span style="flex:1"></span>
|
||
<button type="button" class="btn btn-primary" data-action="domain-wizard-open">+ Добавить домен</button>
|
||
</div>
|
||
<div class="card-body" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||
{% for d in domains %}
|
||
<a class="btn{% if d == selected %} btn-primary{% endif %}" href="?d={{ d }}">{{ d }}</a>
|
||
{% else %}
|
||
<span style="color:var(--color-text-tertiary)">Доменов пока нет — нажмите «Добавить домен», чтобы пройти настройку по шагам.</span>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if selected %}
|
||
<div class="card" style="margin-top:14px">
|
||
<div class="card-head" style="display:flex;align-items:center;gap:10px">
|
||
<div class="card-title">DNS-записи для <code>{{ selected }}</code></div>
|
||
<span style="flex:1"></span>
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="action" value="dkim">
|
||
<input type="hidden" name="domain" value="{{ selected }}">
|
||
<button class="btn" type="submit" title="Создать DKIM-ключ для этого домена">Создать DKIM</button>
|
||
</form>
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="action" value="reload">
|
||
<button class="btn" type="submit" title="Перезапустить Postfix">Reload Postfix</button>
|
||
</form>
|
||
</div>
|
||
<div class="card-body" style="padding:0">
|
||
<table class="tbl">
|
||
<thead>
|
||
<tr><th>Тип</th><th>Имя</th><th>Значение</th><th>Назначение</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in records %}
|
||
<tr>
|
||
<td><span class="dns-type">{{ r.type }}</span></td>
|
||
<td><code>{{ r.name }}</code></td>
|
||
<td style="max-width:480px"><code style="word-break:break-all;white-space:pre-wrap">{{ r.value }}</code></td>
|
||
<td style="color:var(--color-text-secondary)">{{ r.hint }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="card-body" style="border-top:0.5px solid var(--color-border-tertiary);color:var(--color-text-secondary);font-size:12px">
|
||
Скопируйте записи в свой DNS-регистратор. После применения проверьте через
|
||
<code>dig MX {{ selected }}</code>, <code>dig TXT mail._domainkey.{{ selected }}</code>.
|
||
Для отправки наружу нужен также правильный rDNS на IP-адресе сервера.
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="overlay" id="domain-wizard-modal">
|
||
<div class="modal wiz-modal">
|
||
<div class="compose-head">
|
||
<div class="compose-title">Новый домен</div>
|
||
<div class="compose-head-actions">
|
||
<button class="compose-head-btn" data-action="modal-close" title="Закрыть">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="wiz-content" id="domain-wizard-content">
|
||
<div class="wiz-loading">Загрузка…</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.wiz-modal{width:min(960px,96vw);max-width:96vw;display:flex;flex-direction:column;max-height:90vh}
|
||
.wiz-content{padding:18px 22px;overflow-y:auto}
|
||
.wiz-loading{text-align:center;color:var(--color-text-tertiary);padding:40px}
|
||
.wiz-body .wiz-steps{display:flex;align-items:center;gap:14px;margin-bottom:18px;padding-bottom:14px;border-bottom:0.5px solid var(--color-border-tertiary)}
|
||
.wiz-body .wiz-step{display:flex;align-items:center;gap:8px;color:var(--color-text-tertiary);font-size:13px}
|
||
.wiz-body .wiz-step.active{color:var(--color-text-primary);font-weight:500}
|
||
.wiz-body .wiz-step.done{color:var(--color-text-secondary)}
|
||
.wiz-num{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:var(--color-background-secondary);border:0.5px solid var(--color-border-tertiary);font-size:12px}
|
||
.wiz-step.active .wiz-num{background:var(--color-text-primary);color:var(--color-background-primary);border-color:transparent}
|
||
.wiz-step.done .wiz-num{background:#2e7d32;color:#fff;border-color:transparent;font-size:0}
|
||
.wiz-step.done .wiz-num::before{content:'✓';font-size:13px}
|
||
.wiz-line{flex:1;height:0.5px;background:var(--color-border-tertiary);max-width:80px}
|
||
.wiz-title{font-size:14px;font-weight:500;margin-bottom:14px;display:flex;align-items:center;gap:10px}
|
||
.wiz-recheck{margin-left:auto;font-size:12px;color:var(--color-text-secondary);text-decoration:none;padding:3px 8px;border:0.5px solid var(--color-border-tertiary);border-radius:4px}
|
||
.wiz-recheck:hover{background:var(--color-background-secondary)}
|
||
.wiz-form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:14px}
|
||
.wiz-form-row label{color:var(--color-text-secondary);font-size:12px}
|
||
.wiz-input{flex:1;min-width:200px;border:0.5px solid var(--color-border-tertiary);border-radius:6px;padding:6px 10px;font-size:13px;font-family:inherit;background:var(--color-background-primary);color:var(--color-text-primary)}
|
||
.wiz-hint{color:var(--color-text-secondary);font-size:12px;margin-bottom:10px;line-height:1.5}
|
||
.wiz-table-wrap{border:0.5px solid var(--color-border-tertiary);border-radius:6px;overflow-x:auto;margin-bottom:14px}
|
||
.wiz-table-wrap .tbl td{padding:8px 12px}
|
||
.wiz-actions{display:flex;gap:8px;align-items:center;margin-top:18px;padding-top:14px;border-top:0.5px solid var(--color-border-tertiary)}
|
||
.wiz-actions .spacer{flex:1}
|
||
.dns-ok{color:#2e7d32;font-weight:600;font-size:14px}
|
||
.dns-fail{color:var(--color-text-danger);font-weight:600;font-size:14px}
|
||
.dns-type{display:inline-block;padding:1px 6px;background:var(--color-background-secondary);border:0.5px solid var(--color-border-tertiary);border-radius:4px;font-size:11px;font-family:monospace}
|
||
code{font-family:'SF Mono',Menlo,monospace;font-size:12px}
|
||
.break-all{word-break:break-all;white-space:pre-wrap}
|
||
.small{font-size:11px}
|
||
.muted{color:var(--color-text-secondary);font-size:12px}
|
||
.wiz-section{margin-top:18px;padding-top:14px;border-top:0.5px solid var(--color-border-tertiary)}
|
||
.wiz-subtitle{font-size:13px;font-weight:500;margin-bottom:6px}
|
||
.wiz-mb-list{display:flex;flex-direction:column;gap:4px}
|
||
.wiz-mb-item{font-size:12px;color:#2e7d32;padding:4px 0}
|
||
.flash-stack{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
|
||
.flash{padding:8px 12px;border-radius:6px;border:0.5px solid var(--color-border-tertiary);font-size:12px}
|
||
.flash-ok{background:#e8f5e9;color:#1b5e20;border-color:#c8e6c9}
|
||
.flash-err{background:#fce8e6;color:#9b2c2c;border-color:#f5c0c0}
|
||
body.dark .flash-ok{background:#0f3a18;color:#a8e8b9;border-color:#1f5a2d}
|
||
body.dark .flash-err{background:#3a1212;color:#f0a8a8;border-color:#5c2020}
|
||
</style>
|
||
|
||
<script>
|
||
(function(){
|
||
const root = document.getElementById('domain-wizard-content');
|
||
const modal = document.getElementById('domain-wizard-modal');
|
||
if(!root || !modal) return;
|
||
|
||
async function loadFrag(url, opts){
|
||
root.innerHTML = '<div class="wiz-loading">Загрузка…</div>';
|
||
try {
|
||
const r = await fetch(url, Object.assign({headers: {'X-Wizard-Frag': '1'}, redirect: 'follow'}, opts || {}));
|
||
const text = await r.text();
|
||
if(text.trim() === '__DONE__'){
|
||
modal.classList.remove('open');
|
||
location.reload();
|
||
return;
|
||
}
|
||
root.innerHTML = text;
|
||
} catch(e) {
|
||
root.innerHTML = '<div class="wiz-loading">Ошибка загрузки</div>';
|
||
}
|
||
}
|
||
|
||
document.addEventListener('click', e => {
|
||
const open = e.target.closest('[data-action="domain-wizard-open"]');
|
||
if(open){
|
||
e.preventDefault();
|
||
modal.classList.add('open');
|
||
loadFrag('/admin/domains/new?frag=1');
|
||
return;
|
||
}
|
||
const a = e.target.closest('#domain-wizard-content a[href]');
|
||
if(a){
|
||
e.preventDefault();
|
||
loadFrag(a.getAttribute('href'));
|
||
}
|
||
});
|
||
|
||
document.addEventListener('submit', e => {
|
||
if(!root.contains(e.target)) return;
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
const fd = new FormData(f);
|
||
const params = new URLSearchParams();
|
||
for(const [k,v] of fd.entries()) params.append(k, v);
|
||
const url = f.getAttribute('action') || '/admin/domains/new';
|
||
loadFrag(url, {
|
||
method: 'POST',
|
||
body: params,
|
||
headers: {'X-Wizard-Frag': '1', 'Content-Type': 'application/x-www-form-urlencoded'},
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|