mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-02 11:56:27 +02:00
feat(cloudflare): 添加自定义主机名编辑和刷新验证功能 - 新增 hostnames_update 方法用于更新自定义主机名的自定义源站配置 - 新增 hostnames_refresh 方法用于重新向 Cloudflare 发起验证请求 - 添加 extractCustomHostnameSslPayload 辅助方法处理 SSL 配置参数 - 完善 formatCustomHostnameRow 方法,增加更详细的验证状态信息展示 - 在前端界面添加编辑按钮和校验对话框,支持在线查看和刷新验证记录 - 优化自定义主机名列表页面,支持实时更新和状态显示 - 新增证书校验和所有权验证的详细信息展示界面 ```
421 lines
14 KiB
JavaScript
421 lines
14 KiB
JavaScript
var currentVerificationHostnameId = '';
|
|
|
|
$(document).ready(function(){
|
|
$("#form-store").bootstrapValidator();
|
|
loadFallbackOrigin();
|
|
$("#listTable").bootstrapTable({
|
|
url: '/cloudflare/hostnames/data/1',
|
|
method: 'post',
|
|
toolbar: '',
|
|
classes: 'table table-striped table-hover table-bordered',
|
|
uniqueId: 'id',
|
|
responseHandler: hostnameResponseHandler,
|
|
columns: [
|
|
{field: 'hostname', title: '主机名'},
|
|
{field: 'custom_origin_server', title: '自定义源站', formatter: function(v){ return v || '-'; }},
|
|
{field: 'ssl_status', title: '证书状态', formatter: formatStatus},
|
|
{field: 'ssl_validation_status', title: '证书校验', formatter: formatStatus},
|
|
{field: 'verification_status', title: '所有权校验', formatter: formatStatus},
|
|
{field: 'created_on', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
|
{field: 'validation_errors', title: '错误信息', formatter: function(v){ return v || '-'; }},
|
|
{
|
|
field: 'action',
|
|
title: '操作',
|
|
formatter: function(value, row){
|
|
return ''
|
|
+ '<a href="javascript:openEditDialog(\''+row.id+'\')" class="btn btn-info btn-xs">编辑</a> '
|
|
+ '<a href="javascript:openVerificationDialog(\''+row.id+'\')" class="btn btn-primary btn-xs">校验</a> '
|
|
+ '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
|
}
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
function hostnameResponseHandler(res){
|
|
if(res.code !== 0){
|
|
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
|
return {total: 0, rows: []};
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function refreshHostnameList(){
|
|
$("#listTable").bootstrapTable('refresh');
|
|
}
|
|
|
|
function formatStatus(value){
|
|
var v = String(value || '').toLowerCase();
|
|
if(v === 'active' || v === 'active_deployed' || v === 'valid'){
|
|
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
|
}
|
|
if(v === 'pending' || v === 'pending_validation' || v === 'initializing' || v === 'in_progress'){
|
|
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
|
}
|
|
if(v && v !== '-'){
|
|
return '<span class="label label-danger">'+htmlEscape(value)+'</span>';
|
|
}
|
|
return '-';
|
|
}
|
|
|
|
function getHostnameRow(id){
|
|
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
|
if(!row){
|
|
layer.alert('未找到自定义主机名数据,请先刷新列表后重试', {icon: 2});
|
|
return null;
|
|
}
|
|
return row;
|
|
}
|
|
|
|
function resetHostnameForm(){
|
|
$("#form-store")[0].reset();
|
|
$("#form-store input[name=hostname_id]").val('');
|
|
$("#form-store input[name=hostname]").prop('readonly', false);
|
|
$("#form-store").data("bootstrapValidator").resetForm(true);
|
|
}
|
|
|
|
function openAddDialog(){
|
|
resetHostnameForm();
|
|
$("#storeTitle").text('添加自定义主机名');
|
|
$("#hostnameHint").text('创建后主机名不能直接改名,如需改名请删除后重建。');
|
|
$("#modal-store").modal('show');
|
|
}
|
|
|
|
function openEditDialog(id){
|
|
var row = getHostnameRow(id);
|
|
if(!row){
|
|
return;
|
|
}
|
|
resetHostnameForm();
|
|
$("#storeTitle").text('编辑自定义主机名');
|
|
$("#hostnameHint").text('主机名不可直接改名,当前仅支持修改或清空自定义源站。');
|
|
$("#form-store input[name=hostname_id]").val(row.id);
|
|
$("#form-store input[name=hostname]").val(row.hostname).prop('readonly', true);
|
|
$("#form-store input[name=custom_origin_server]").val(row.custom_origin_server || '');
|
|
$("#modal-store").modal('show');
|
|
}
|
|
|
|
function submitHostname(){
|
|
$("#form-store").data("bootstrapValidator").validate();
|
|
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
|
return;
|
|
}
|
|
var hostnameId = $.trim($("#form-store input[name=hostname_id]").val());
|
|
var url = hostnameId ? '/cloudflare/hostnames/update/1' : '/cloudflare/hostnames/add/1';
|
|
var successMsg = hostnameId ? '更新自定义主机名成功' : '创建自定义主机名成功';
|
|
var ii = layer.load(2);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: url,
|
|
data: $("#form-store").serialize(),
|
|
dataType: 'json',
|
|
success: function(res){
|
|
layer.close(ii);
|
|
if(res.code === 0){
|
|
$("#modal-store").modal('hide');
|
|
layer.msg(res.msg || successMsg, {icon: 1, time: 1200});
|
|
if(res.data && res.data.id){
|
|
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
|
if(!$("#listTable").bootstrapTable('getRowByUniqueId', res.data.id)){
|
|
refreshHostnameList();
|
|
}
|
|
}else{
|
|
refreshHostnameList();
|
|
}
|
|
}else{
|
|
layer.alert(res.msg, {icon: 2});
|
|
}
|
|
},
|
|
error: function(){
|
|
layer.close(ii);
|
|
layer.alert('服务器错误', {icon: 2});
|
|
}
|
|
});
|
|
}
|
|
|
|
function openVerificationDialog(id){
|
|
var row = getHostnameRow(id);
|
|
if(!row){
|
|
return;
|
|
}
|
|
currentVerificationHostnameId = id;
|
|
renderVerificationDialog(row);
|
|
$("#modal-verification").modal('show');
|
|
}
|
|
|
|
function refreshHostnameValidation(){
|
|
if(!currentVerificationHostnameId){
|
|
layer.msg('请先选择自定义主机名');
|
|
return;
|
|
}
|
|
var ii = layer.load(2);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/cloudflare/hostnames/refresh/1',
|
|
data: {hostname_id: currentVerificationHostnameId},
|
|
dataType: 'json',
|
|
success: function(res){
|
|
layer.close(ii);
|
|
if(res.code === 0){
|
|
if(res.data && res.data.id){
|
|
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
|
renderVerificationDialog(res.data);
|
|
}else{
|
|
refreshHostnameList();
|
|
}
|
|
layer.msg(res.msg, {icon: 1, time: 1200});
|
|
}else{
|
|
layer.alert(res.msg, {icon: 2});
|
|
}
|
|
},
|
|
error: function(){
|
|
layer.close(ii);
|
|
layer.alert('服务器错误', {icon: 2});
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderVerificationDialog(row){
|
|
$("#verificationTitle").text('证书校验 - ' + row.hostname);
|
|
var html = '';
|
|
html += '<div class="alert alert-info"><strong>说明:</strong> 下列值直接来自 Cloudflare 返回结果,可直接复制到 DNS、源站或验证目录中。点击“刷新校验”会重新向 Cloudflare 发起一次校验。</div>';
|
|
html += '<div class="row">';
|
|
html += '<div class="col-sm-4">'+renderSummaryCard('证书状态', formatStatusText(row.ssl_status))+'</div>';
|
|
html += '<div class="col-sm-4">'+renderSummaryCard('证书校验', formatStatusText(row.ssl_validation_status))+'</div>';
|
|
html += '<div class="col-sm-4">'+renderSummaryCard('所有权校验', formatStatusText(row.verification_status))+'</div>';
|
|
html += '</div>';
|
|
|
|
var ownership = row.ownership_verification || {};
|
|
if(ownership.name || ownership.value){
|
|
html += renderSection('所有权 TXT 校验',
|
|
renderCopyInput('记录类型', ownership.type || 'txt', false)
|
|
+ renderCopyInput('TXT 名称', ownership.name || '', true)
|
|
+ renderCopyTextarea('TXT 值', ownership.value || '', true, 3)
|
|
);
|
|
}
|
|
|
|
var ownershipHttp = row.ownership_verification_http || {};
|
|
if(ownershipHttp.http_url || ownershipHttp.http_body){
|
|
html += renderSection('所有权 HTTP 校验',
|
|
renderCopyTextarea('HTTP URL', ownershipHttp.http_url || '', true, 2)
|
|
+ renderCopyTextarea('HTTP Body', ownershipHttp.http_body || '', true, 3)
|
|
);
|
|
}
|
|
|
|
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
|
if(records.length > 0){
|
|
var recordsHtml = '';
|
|
for(var i = 0; i < records.length; i++){
|
|
var item = records[i] || {};
|
|
var emails = $.isArray(item.emails) ? item.emails.join('\n') : '';
|
|
recordsHtml += '<div class="panel panel-default" style="margin-bottom:12px;">';
|
|
recordsHtml += '<div class="panel-heading"><strong>证书校验记录 #' + (i + 1) + '</strong><span class="pull-right">' + formatStatusText(item.status || '-') + '</span></div>';
|
|
recordsHtml += '<div class="panel-body">';
|
|
recordsHtml += renderCopyInput('TXT 名称', item.txt_name || '', true);
|
|
recordsHtml += renderCopyTextarea('TXT 值', item.txt_value || '', true, 3);
|
|
recordsHtml += renderCopyInput('CNAME 名称', item.cname_name || '', true);
|
|
recordsHtml += renderCopyTextarea('CNAME 目标', item.cname_target || '', true, 2);
|
|
recordsHtml += renderCopyTextarea('HTTP URL', item.http_url || '', true, 2);
|
|
recordsHtml += renderCopyTextarea('HTTP Body', item.http_body || '', true, 3);
|
|
recordsHtml += renderCopyTextarea('邮箱地址', emails, false, 2);
|
|
recordsHtml += '</div></div>';
|
|
}
|
|
html += renderSection('证书校验记录', recordsHtml);
|
|
}else{
|
|
html += '<div class="alert alert-warning">Cloudflare 当前尚未返回证书校验记录,请先等待状态进入 <code>pending_validation</code>,再点击“刷新校验”或稍后刷新列表。</div>';
|
|
}
|
|
|
|
if(row.validation_errors){
|
|
html += renderSection('错误信息', renderCopyTextarea('错误信息', row.validation_errors, false, 3));
|
|
}
|
|
|
|
$("#verificationContent").html(html);
|
|
}
|
|
|
|
function renderSummaryCard(title, value){
|
|
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + value + '</div></div>';
|
|
}
|
|
|
|
function renderSection(title, body){
|
|
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + body + '</div></div>';
|
|
}
|
|
|
|
function renderCopyInput(label, value, copyable){
|
|
var safeValue = String(value || '');
|
|
if(!safeValue){
|
|
return '';
|
|
}
|
|
var html = '<div class="form-group">';
|
|
html += '<label>' + htmlEscape(label) + '</label>';
|
|
if(copyable){
|
|
html += '<div class="input-group">';
|
|
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
|
html += '<span class="input-group-btn"><button type="button" class="btn btn-default" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></span>';
|
|
html += '</div>';
|
|
}else{
|
|
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
|
}
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function renderCopyTextarea(label, value, copyable, rows){
|
|
var safeValue = String(value || '');
|
|
if(!safeValue){
|
|
return '';
|
|
}
|
|
var html = '<div class="form-group">';
|
|
html += '<label>' + htmlEscape(label) + '</label>';
|
|
html += '<textarea class="form-control" rows="' + (rows || 3) + '" readonly>' + htmlEscape(safeValue) + '</textarea>';
|
|
if(copyable){
|
|
html += '<div class="text-right" style="margin-top:8px;"><button type="button" class="btn btn-default btn-xs" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></div>';
|
|
}
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function formatStatusText(value){
|
|
var text = value || '-';
|
|
if(text === '-'){
|
|
return '<span class="text-muted">-</span>';
|
|
}
|
|
return formatStatus(text);
|
|
}
|
|
|
|
function copyEncodedValue(btn){
|
|
copyText(decodeURIComponent($(btn).attr('data-copy') || ''));
|
|
}
|
|
|
|
function copyText(text){
|
|
var value = String(text || '');
|
|
if(!value){
|
|
layer.msg('没有可复制的内容');
|
|
return;
|
|
}
|
|
if(navigator.clipboard && window.isSecureContext){
|
|
navigator.clipboard.writeText(value).then(function(){
|
|
layer.msg('已复制', {icon: 1, time: 1000});
|
|
}).catch(function(){
|
|
fallbackCopyText(value);
|
|
});
|
|
return;
|
|
}
|
|
fallbackCopyText(value);
|
|
}
|
|
|
|
function fallbackCopyText(text){
|
|
var $temp = $('<textarea readonly></textarea>');
|
|
$('body').append($temp);
|
|
$temp.val(text).select();
|
|
try{
|
|
document.execCommand('copy');
|
|
layer.msg('已复制', {icon: 1, time: 1000});
|
|
}catch(e){
|
|
layer.alert('复制失败,请手动复制', {icon: 2});
|
|
}
|
|
$temp.remove();
|
|
}
|
|
|
|
function deleteHostname(id, hostname){
|
|
layer.confirm('确定要删除自定义主机名 ' + hostname + ' 吗?', {title: '提示', icon: 0}, function(){
|
|
var ii = layer.load(2);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/cloudflare/hostnames/delete/1',
|
|
data: {hostname_id: id, hostname: hostname},
|
|
dataType: 'json',
|
|
success: function(res){
|
|
layer.close(ii);
|
|
if(res.code === 0){
|
|
layer.closeAll();
|
|
layer.msg(res.msg, {icon: 1, time: 1000});
|
|
refreshHostnameList();
|
|
}else{
|
|
layer.alert(res.msg, {icon: 2});
|
|
}
|
|
},
|
|
error: function(){
|
|
layer.close(ii);
|
|
layer.alert('服务器错误', {icon: 2});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadFallbackOrigin(){
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/cloudflare/fallback/get/1',
|
|
dataType: 'json',
|
|
success: function(res){
|
|
if(res.code === 0){
|
|
$("#fallbackOrigin").val((res.data && res.data.origin) ? res.data.origin : '');
|
|
}else{
|
|
layer.alert(res.msg, {icon: 2});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function saveFallbackOrigin(){
|
|
var origin = $.trim($("#fallbackOrigin").val());
|
|
if(!origin){
|
|
layer.msg('请输入 Fallback Origin');
|
|
return;
|
|
}
|
|
var ii = layer.load(2);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/cloudflare/fallback/set/1',
|
|
data: {origin: origin},
|
|
dataType: 'json',
|
|
success: function(res){
|
|
layer.close(ii);
|
|
if(res.code === 0){
|
|
$("#fallbackOrigin").val(res.data.origin || origin);
|
|
layer.msg(res.msg, {icon: 1, time: 1200});
|
|
}else{
|
|
layer.alert(res.msg, {icon: 2});
|
|
}
|
|
},
|
|
error: function(){
|
|
layer.close(ii);
|
|
layer.alert('服务器错误', {icon: 2});
|
|
}
|
|
});
|
|
}
|
|
|
|
function clearFallbackOrigin(){
|
|
layer.confirm('确定要清空 Fallback Origin 吗?', {title: '提示', icon: 0}, function(){
|
|
var ii = layer.load(2);
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/cloudflare/fallback/delete/1',
|
|
dataType: 'json',
|
|
success: function(res){
|
|
layer.close(ii);
|
|
if(res.code === 0){
|
|
layer.closeAll();
|
|
$("#fallbackOrigin").val('');
|
|
layer.msg(res.msg, {icon: 1, time: 1200});
|
|
}else{
|
|
layer.alert(res.msg, {icon: 2});
|
|
}
|
|
},
|
|
error: function(){
|
|
layer.close(ii);
|
|
layer.alert('服务器错误', {icon: 2});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function htmlEscape(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|