manage_zone.html 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. {% extends "layout.html" %}
  2. {% block content %}
  3. <div id="zone-dynamic-content">
  4. {% set is_reverse = '.arpa' in domain %}
  5. {% set friendly_name = domain %}
  6. {% set base_ip = '' %}
  7. {% if is_reverse %}
  8. {% set rev_parts = domain.replace('.in-addr.arpa', '').replace('.ip6.arpa', '').split('.') %}
  9. {% set base_ip = rev_parts | reverse | join('.') %}
  10. {% set friendly_name = base_ip ~ '.x (Reverse)' %}
  11. {% endif %}
  12. <div class="flex justify-between items-center mb-8 bg-[#12161f] p-6 rounded-2xl border border-gray-800 shadow-2xl">
  13. <div>
  14. <h2 class="text-3xl font-black font-mono text-blue-400 tracking-tighter">{% if domain == '.' %}. (Root Zone){% else %}{{ friendly_name }}{% endif %}</h2>
  15. {% if is_reverse %}<p class="text-[10px] text-gray-500 font-mono tracking-widest mt-1">{{ domain }}</p>{% endif %}
  16. <div class="mt-2 flex items-center gap-3">
  17. <span class="text-[10px] bg-blue-900/30 text-blue-400 px-3 py-1 rounded-full uppercase font-black border border-blue-800/50">{{ type }}</span>
  18. {% if type == 'slave' %}
  19. <button onclick="runDigTest('{{ domain }}')" class="text-[10px] bg-green-600/20 text-green-400 px-3 py-1 rounded-full uppercase font-black border border-green-500/30 hover:bg-green-600 hover:text-white transition">
  20. <i class="fa-solid fa-terminal mr-1"></i> Test Transfer (Dig)
  21. </button>
  22. {% endif %}
  23. {% if type == 'slave' %}
  24. <div class="flex items-center gap-2 border-l border-gray-800 pl-3">
  25. <span class="text-[10px] text-purple-400 font-bold uppercase tracking-widest">Master DNS: {{ master_ips }}</span>
  26. <button type="button" onclick="openEditIpsModal('{{ master_ips }}')" class="text-[9px] bg-gray-800 hover:bg-gray-700 px-2 py-1 rounded-lg text-gray-300 font-bold uppercase transition">Edit IPs</button>
  27. <a href="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/convert_to_master" onclick="handleAjaxLink(event, this, 'zone-dynamic-content', restoreRecordTab, 'Convert to Master?')" class="text-[9px] bg-yellow-600/20 text-yellow-500 px-2 py-1 rounded-lg font-black uppercase transition border border-yellow-500/30">Convert to Master</a>
  28. </div>
  29. {% elif type == 'master' %}
  30. <div class="flex items-center gap-2 border-l border-gray-800 pl-3">
  31. <button type="button" onclick="openConvertToSlaveModal()" class="text-[9px] bg-purple-600/20 text-purple-400 px-2 py-1 rounded-lg font-black uppercase transition border border-purple-500/30">Convert to Slave</button>
  32. </div>
  33. {% elif type == 'hint' %}
  34. <span class="text-[10px] bg-red-900/30 text-red-500 px-3 py-1 rounded-full uppercase font-black border border-red-800/50">ROOT CONFIG</span>
  35. {% elif type == 'forward' %}
  36. <span class="text-[10px] bg-blue-900/30 text-blue-400 px-3 py-1 rounded-full uppercase font-black border border-blue-800/50">FORWARDER</span>
  37. {% endif %}
  38. </div>
  39. </div>
  40. <a href="/" class="text-gray-500 hover:text-white font-bold text-sm transition flex items-center gap-2">← Back</a>
  41. </div>
  42. <div class="flex border-b border-gray-800 mb-6 gap-6 overflow-x-auto whitespace-nowrap scrollbar-hide">
  43. <button onclick="switchMainTab('records')" id="mainTab_records" class="pb-4 text-[10px] font-black uppercase tracking-widest text-blue-500 border-b-2 border-blue-500 transition focus:outline-none">DNS Records</button>
  44. {% if type == 'master' %}
  45. <button onclick="switchMainTab('audit')" id="mainTab_audit" class="pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none">Audit & Time Machine</button>
  46. <button onclick="switchMainTab('security')" id="mainTab_security" class="pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none">Security (DNSSEC)</button>
  47. {% endif %}
  48. </div>
  49. <div id="mainSection_records" class="main-section">
  50. {% if type == 'master' %}
  51. <div class="bg-[#12161f] p-8 rounded-2xl border border-gray-800 mb-8 shadow-2xl">
  52. <h3 class="font-black mb-6 text-green-500 uppercase text-xs tracking-widest">Add New Resource Record</h3>
  53. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/add_record" method="POST" class="flex flex-col lg:flex-row gap-4 items-start" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); })">
  54. <div class="w-full lg:w-1/4">
  55. <input type="text" name="name" id="addName" required placeholder="{% if is_reverse %}e.g. {{ base_ip }}.10{% else %}Name (@){% endif %}" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-4 h-[46px] text-white font-mono outline-none focus:border-blue-500 text-sm transition">
  56. <p class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3">{% if is_reverse %}IP ADDRESS (e.g. {{ base_ip }}.x){% else %}RECORD NAME{% endif %}</p>
  57. </div>
  58. <div class="w-full lg:w-32">
  59. <select name="rtype" id="addRtype" onchange="updateValidation('add')" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-3 h-[46px] text-white font-bold outline-none cursor-pointer text-sm">
  60. {% if is_reverse %}
  61. <option value="PTR">PTR</option>
  62. <option value="NS">NS</option>
  63. {% else %}
  64. <option value="A">A</option><option value="AAAA">AAAA</option><option value="CNAME">CNAME</option><option value="MX">MX</option><option value="TXT">TXT</option><option value="NS">NS</option><option value="SRV">SRV</option>
  65. {% endif %}
  66. </select>
  67. </div>
  68. <div class="flex-1 w-full">
  69. <input type="text" name="value" id="addValue" required placeholder="Value" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-4 h-[46px] text-white font-mono outline-none focus:border-blue-500 text-sm transition">
  70. <p id="addHelper" class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3 transition-colors"></p>
  71. </div>
  72. <button type="submit" class="w-full lg:w-auto bg-blue-600 hover:bg-blue-500 text-white px-10 h-[46px] rounded-xl font-black uppercase text-[11px] tracking-widest transition shadow-xl">Add</button>
  73. </form>
  74. </div>
  75. {% elif type == 'slave' %}
  76. <div class="bg-[#12161f] p-6 rounded-2xl border border-gray-800 mb-8 shadow-xl">
  77. <div class="flex justify-between items-center mb-4 border-b border-gray-800 pb-4">
  78. <h3 class="font-black text-purple-400 uppercase text-xs tracking-widest">Live Sync Logs</h3>
  79. <a href="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/sync" onclick="handleAjaxLink(event, this, 'zone-dynamic-content', restoreRecordTab, null)" class="text-[10px] bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded-lg text-white font-black uppercase transition flex items-center gap-2">
  80. <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
  81. Sync Now
  82. </a>
  83. </div>
  84. <div class="bg-black/50 p-4 rounded-xl font-mono text-[10px] h-40 overflow-y-auto custom-scroll flex flex-col-reverse shadow-inner">
  85. {% if logs %}
  86. {% for log in logs %}
  87. <div class="mb-1 text-gray-500 border-b border-gray-900 last:border-0 pb-1">{{ log }}</div>
  88. {% endfor %}
  89. {% else %}
  90. <div class="text-gray-700 italic font-bold">No recent logs found or transfer pending.</div>
  91. {% endif %}
  92. </div>
  93. </div>
  94. {% endif %}
  95. {% if type != 'forward' %}
  96. {% if has_records %}
  97. <div class="flex border-b border-gray-800 mb-6 gap-2 overflow-x-auto custom-scroll pb-1">
  98. {% for rtype, rlist in records.items() if rlist %}
  99. <button onclick="switchTab('{{ rtype }}')" id="btn-{{ rtype }}" class="tab-btn px-6 py-3 text-[11px] font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2 {% if loop.first %}border-blue-500 text-blue-400{% else %}border-transparent text-gray-500 hover:text-gray-300{% endif %}">
  100. {{ rtype }} <span class="bg-gray-800/80 text-gray-400 px-2 py-0.5 rounded-full text-[9px]">{{ rlist|length }}</span>
  101. </button>
  102. {% endfor %}
  103. </div>
  104. <div class="bg-[#12161f] border border-gray-800 rounded-2xl overflow-hidden shadow-2xl min-h-[200px]">
  105. {% for rtype, rlist in records.items() if rlist %}
  106. <div id="tab-{{ rtype }}" class="tab-content {% if not loop.first %}hidden{% endif %}">
  107. <table class="w-full text-sm text-left font-mono">
  108. <thead class="bg-black/20 text-[10px] font-black text-gray-500 uppercase tracking-widest border-b border-gray-800">
  109. <tr><th class="px-6 py-4 w-1/3">Name / IP</th><th class="px-6 py-4">Value</th><th class="px-6 py-4 text-right">Actions</th></tr>
  110. </thead>
  111. <tbody class="divide-y divide-gray-800/50">
  112. {% for r in rlist %}
  113. <tr class="hover:bg-white/[0.02] group transition-colors cursor-pointer no-select"
  114. {% if type == 'master' %}
  115. data-raw="{{ r.raw|e }}"
  116. data-name="{{ r.friendly_name|default(r.name)|e }}"
  117. data-type="{{ rtype }}"
  118. data-val="{{ r.value|e }}"
  119. ondblclick="triggerEditModal(this)"
  120. {% endif %}>
  121. <td class="px-6 py-4 text-blue-400 font-bold truncate max-w-[200px]" title="{{ r.name }}">{{ r.friendly_name | default(r.name) }}</td>
  122. <td class="px-6 py-4 text-gray-400 text-xs break-all">{{ r.value }}</td>
  123. <td class="px-6 py-4 text-right opacity-0 group-hover:opacity-100 transition whitespace-nowrap">
  124. {% if type == 'master' %}
  125. <button type="button"
  126. data-raw="{{ r.raw|e }}"
  127. data-name="{{ r.friendly_name|default(r.name)|e }}"
  128. data-type="{{ rtype }}"
  129. data-val="{{ r.value|e }}"
  130. onclick="triggerEditModal(this)"
  131. class="text-blue-500 hover:text-blue-400 text-[10px] font-black uppercase mr-4 transition">
  132. Edit
  133. </button>
  134. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/delete_record" method="POST" class="inline" onsubmit="if(!confirm('Delete this record?')) return false; handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)"><input type="hidden" name="raw_line" value="{{ r.raw|e }}"><button type="submit" class="text-red-500/50 hover:text-red-500 text-[10px] font-black uppercase transition">Delete</button></form>
  135. {% else %}
  136. <span class="text-gray-600 text-[9px] uppercase font-black tracking-widest italic">Read Only</span>
  137. {% endif %}
  138. </td>
  139. </tr>
  140. {% endfor %}
  141. </tbody>
  142. </table>
  143. </div>
  144. {% endfor %}
  145. </div>
  146. {% if type == 'master' %}
  147. <p class="text-center text-[10px] text-gray-500 font-bold mt-4 uppercase tracking-widest animate-pulse">Tip: Double-click any row to edit instantly</p>
  148. {% endif %}
  149. {% else %}
  150. <div class="flex flex-col items-center justify-center p-16 text-center border-2 border-dashed border-gray-800 rounded-2xl">
  151. <svg class="w-12 h-12 text-gray-700 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707-.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
  152. <p class="text-gray-500 font-bold uppercase tracking-widest text-[11px]">No records found yet.</p>
  153. </div>
  154. {% endif %}
  155. {% endif %}
  156. </div>
  157. <div id="mainSection_audit" class="main-section hidden">
  158. <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
  159. <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl flex flex-col h-[500px]">
  160. <div class="p-6 border-b border-gray-800 bg-gray-900/30"><h3 class="text-lg font-black text-white uppercase tracking-tighter italic">Audit Log (Recent Changes)</h3></div>
  161. <div class="flex-1 overflow-y-auto custom-scroll p-6 font-mono text-[11px]">
  162. {% if audit_logs %}
  163. {% for log in audit_logs %}
  164. <div class="mb-2 pb-2 border-b border-gray-800 last:border-0 text-gray-400">
  165. <span class="text-gray-600 block mb-1">{{ log.time }}</span>
  166. <span class="text-blue-400 font-bold">{{ log.user }}</span>: {{ log.action }}
  167. </div>
  168. {% endfor %}
  169. {% else %}
  170. <p class="text-gray-600 text-center italic mt-10">No recent changes found.</p>
  171. {% endif %}
  172. </div>
  173. </div>
  174. <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl flex flex-col h-[500px]">
  175. <div class="p-6 border-b border-gray-800 bg-gray-900/30"><h3 class="text-lg font-black text-yellow-500 uppercase tracking-tighter italic flex items-center gap-2"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>Time Machine</h3></div>
  176. <div class="flex-1 overflow-y-auto custom-scroll p-6 font-mono text-[11px]">
  177. {% if backups %}
  178. {% for b in backups %}
  179. <div class="mb-3 pb-3 border-b border-gray-800 last:border-0 flex justify-between items-center group">
  180. <div>
  181. <span class="text-white block font-bold text-sm">{{ b.time }}</span>
  182. <span class="text-gray-600 text-[9px]">{{ b.file }}</span>
  183. </div>
  184. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/rollback/{{ b.ts }}" method="POST" onsubmit="if(!confirm('DANGER! This will overwrite the current zone file with this backup. Continue?')) return false; handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  185. <button type="submit" class="bg-yellow-600/20 text-yellow-500 hover:bg-yellow-500 hover:text-white transition px-4 py-2 rounded-lg font-black uppercase text-[10px]">Rollback</button>
  186. </form>
  187. </div>
  188. {% endfor %}
  189. {% else %}
  190. <p class="text-gray-600 text-center italic mt-10 text-sm">No backups found. Make a change to create one!</p>
  191. {% endif %}
  192. </div>
  193. </div>
  194. </div>
  195. </div>
  196. <div id="mainSection_security" class="main-section hidden">
  197. <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl p-8 max-w-4xl mx-auto text-center">
  198. <h2 class="text-3xl font-black uppercase tracking-tighter italic mb-4 text-white">DNSSEC Activation</h2>
  199. <p class="text-gray-400 text-sm font-bold mb-8">Secure your domain against spoofing with a single click. BIND will handle cryptographic signatures automatically (inline-signing).</p>
  200. <div class="flex flex-col items-center justify-center p-8 rounded-xl border-2 {% if dnssec_enabled %}border-green-500/30 bg-green-900/10{% else %}border-gray-800 bg-black/40{% endif %} mb-8">
  201. {% if dnssec_enabled %}
  202. <svg class="w-16 h-16 text-green-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
  203. <h3 class="text-2xl font-black text-green-400 uppercase tracking-widest mb-2">Active & Protected</h3>
  204. <p class="text-gray-400 text-xs mb-6">Zone is cryptographically signed via BIND inline-signing.</p>
  205. {% else %}
  206. <svg class="w-16 h-16 text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
  207. <h3 class="text-2xl font-black text-gray-500 uppercase tracking-widest mb-2">Not Protected</h3>
  208. {% endif %}
  209. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/dnssec" method="POST" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  210. {% if dnssec_enabled %}
  211. <button type="submit" class="bg-red-600/20 text-red-500 hover:bg-red-500 hover:text-white px-8 py-3 rounded-xl font-black uppercase transition text-xs border border-red-500/30">Disable DNSSEC</button>
  212. {% else %}
  213. <button type="submit" class="bg-green-600 text-white hover:bg-green-500 px-8 py-3 rounded-xl font-black uppercase transition text-xs shadow-xl flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg> Enable DNSSEC</button>
  214. {% endif %}
  215. </form>
  216. </div>
  217. {% if dnssec_enabled and ds_records %}
  218. <div class="text-left bg-black/60 p-6 rounded-xl border border-gray-800">
  219. <h4 class="text-sm font-black text-blue-400 uppercase tracking-widest mb-4">DS Records for Registrar</h4>
  220. <p class="text-xs text-gray-400 mb-4">Copy and paste these Delegation Signer (DS) records to your Domain Registrar (e.g. GoDaddy, Namecheap) to complete the chain of trust.</p>
  221. <div class="space-y-2">
  222. {% for ds in ds_records %}
  223. <input type="text" readonly value="{{ ds }}" class="w-full bg-[#0b0e14] border border-gray-800 rounded-lg p-3 text-green-400 font-mono text-[10px] outline-none cursor-pointer hover:border-green-500/50 transition" onclick="this.select(); document.execCommand('copy'); AppToast.fire({icon:'success', title:'Copied to clipboard!'});">
  224. {% endfor %}
  225. </div>
  226. </div>
  227. {% elif dnssec_enabled %}
  228. <p class="text-xs text-yellow-500 font-bold italic">Keys are generating in the background. It may take a minute for DS records to appear here.</p>
  229. {% endif %}
  230. </div>
  231. </div>
  232. <div id="editModal" class="hidden fixed inset-0 bg-black/95 items-center justify-center z-50 p-6 transition-opacity">
  233. <div class="bg-[#12161f] p-8 rounded-3xl border border-gray-800 shadow-2xl w-full max-w-xl">
  234. <h3 class="font-black text-xl mb-8 text-blue-400 uppercase border-b border-gray-800 pb-4">Modify Record</h3>
  235. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/edit_record" method="POST" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); closeEditModal(); })">
  236. <input type="hidden" name="old_raw" id="editOldRaw">
  237. <div class="grid grid-cols-3 gap-6">
  238. <div class="col-span-2">
  239. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Name / IP</label>
  240. <input type="text" name="name" id="editName" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white outline-none focus:border-blue-500 transition-colors">
  241. </div>
  242. <div>
  243. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Type</label>
  244. <input type="text" name="rtype" id="editRtype" readonly class="w-full bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-gray-500 font-black outline-none cursor-not-allowed">
  245. </div>
  246. </div>
  247. <div>
  248. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Value</label>
  249. <input type="text" name="value" id="editValue" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white outline-none focus:border-blue-500 transition-colors">
  250. <p id="editHelper" class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3 transition-colors"></p>
  251. </div>
  252. <div class="flex gap-4 pt-6">
  253. <button type="button" onclick="closeEditModal()" class="flex-1 bg-gray-800 hover:bg-gray-700 transition-colors text-gray-400 font-bold py-4 rounded-xl uppercase text-xs">Cancel</button>
  254. <button type="submit" class="flex-1 bg-blue-600 hover:bg-blue-500 transition-colors text-white font-black py-4 rounded-xl uppercase text-xs flex justify-center items-center">Save Changes</button>
  255. </div>
  256. </form>
  257. </div>
  258. </div>
  259. <div id="editIpsModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-md items-center justify-center z-50 p-6 transition-opacity">
  260. <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
  261. <h3 class="text-xl font-black text-white mb-6 uppercase">Edit DNS Target IPs</h3>
  262. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/edit_ips" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  263. <div id="editIpsContainer" class="space-y-2"></div>
  264. <button type="button" onclick="addEditIpField('')" class="w-full py-3 border-2 border-dashed border-gray-800 rounded-xl text-gray-500 text-[11px] font-black uppercase hover:border-gray-600 hover:text-gray-300 transition">+ Add New IP</button>
  265. <div class="flex gap-4 pt-6">
  266. <button type="button" onclick="closeEditIpsModal()" class="flex-1 bg-gray-800 text-gray-400 transition-colors font-bold py-3 rounded-xl uppercase text-xs">Cancel</button>
  267. <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl">Save</button>
  268. </div>
  269. </form>
  270. </div>
  271. </div>
  272. <div id="convertToSlaveModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-md items-center justify-center z-50 p-6 transition-opacity">
  273. <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
  274. <h3 class="text-xl font-black text-purple-400 mb-6 uppercase border-b border-gray-800 pb-4">Convert to Slave</h3>
  275. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/convert_to_slave" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  276. <p class="text-xs text-gray-400 font-bold mb-4">Please provide the Master DNS server IPs for this zone.</p>
  277. <div id="slaveIpsContainer" class="space-y-2">
  278. <div class="flex gap-2">
  279. <input type="text" name="master_ips[]" placeholder="Master IP" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono text-sm outline-none focus:border-purple-500 transition-colors">
  280. </div>
  281. </div>
  282. <button type="button" onclick="addSlaveIpField()" class="w-full py-3 border-2 border-dashed border-gray-800 rounded-xl text-gray-500 text-[11px] font-black uppercase transition hover:text-gray-300">+ Add Another IP</button>
  283. <div class="flex gap-4 pt-6">
  284. <button type="button" onclick="closeConvertToSlaveModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-3 rounded-xl uppercase text-xs">Cancel</button>
  285. <button type="submit" class="flex-1 bg-purple-600 hover:bg-purple-500 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl transition">Convert</button>
  286. </div>
  287. </form>
  288. </div>
  289. </div>
  290. <div id="digModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-sm items-center justify-center z-[100] p-6">
  291. <div class="bg-[#12161f] border border-gray-800 w-full max-w-2xl rounded-3xl p-8 shadow-2xl flex flex-col max-h-[90vh]">
  292. <div class="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
  293. <h3 class="text-xl font-black text-green-400 uppercase italic">AXFR Transfer Simulation</h3>
  294. <button onclick="closeDigModal()" class="text-gray-500 hover:text-white text-2xl font-bold">&times;</button>
  295. </div>
  296. <div id="digLoading" class="py-20 text-center">
  297. <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto mb-4"></div>
  298. <p class="text-gray-500 font-black uppercase text-xs tracking-widest">Running Dig AXFR...</p>
  299. </div>
  300. <div id="digContent" class="hidden overflow-y-auto">
  301. <div class="grid grid-cols-2 gap-4 mb-6" id="digAnalysis">
  302. </div>
  303. <h4 class="text-[10px] font-black text-gray-500 uppercase mb-2 tracking-widest">Raw Output</h4>
  304. <pre id="digRaw" class="bg-black p-4 rounded-xl text-[10px] text-green-600 font-mono overflow-x-auto border border-gray-800 custom-scroll"></pre>
  305. </div>
  306. <div class="mt-8">
  307. <button onclick="closeDigModal()" class="w-full bg-gray-800 hover:bg-gray-700 py-4 rounded-xl text-white font-black uppercase text-xs transition">Close</button>
  308. </div>
  309. </div>
  310. </div>
  311. </div>
  312. <script>
  313. let activeRecordTab = '';
  314. let activeMainTab = 'records';
  315. const recordRules = {
  316. 'A': { regex: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, msg: "Must be a valid IPv4 address", placeholder: "192.168.1.1" },
  317. 'AAAA': { regex: /^[a-fA-F0-9:]+$/, msg: "Must be a valid IPv6 address", placeholder: "2001:db8::1" },
  318. 'CNAME': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a domain without spaces", placeholder: "target.example.com." },
  319. 'NS': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a valid Nameserver domain", placeholder: "ns1.example.com." },
  320. 'TXT': { regex: /^.*$/, msg: "Any text value (Usually wrapped in quotes)", placeholder: '"text here"' },
  321. 'MX': { regex: /^\d+\s+[a-zA-Z0-9\-\_\.]+$/, msg: "Format: Priority Domain", placeholder: "10 mail.example.com." },
  322. 'SRV': { regex: /^\d+\s+\d+\s+\d+\s+[a-zA-Z0-9\-\_\.]+$/, msg: "Format: Priority Weight Port Domain", placeholder: "10 5 5060 sip.com." },
  323. 'PTR': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a valid domain name", placeholder: "host.example.com." }
  324. };
  325. // --- SPA & AJAX SMART PARSER ---
  326. async function handleAjaxForm(e, form, containerId, callback) {
  327. e.preventDefault();
  328. const btn = form.querySelector('button[type="submit"]');
  329. if(btn) { btn.innerHTML = spinnerSvg + ' WAIT'; btn.disabled = true; }
  330. try {
  331. const res = await fetch(form.action, { method: form.method || 'POST', body: new FormData(form) });
  332. const html = await res.text();
  333. const doc = new DOMParser().parseFromString(html, 'text/html');
  334. const scriptContent = Array.from(doc.querySelectorAll('script')).map(s => s.innerHTML).join('\n');
  335. const errorMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'error',\s*title:\s*'([^']+)'/);
  336. const successMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'success',\s*title:\s*'([^']+)'/);
  337. const newCont = doc.getElementById(containerId);
  338. if(newCont) {
  339. document.getElementById(containerId).innerHTML = newCont.innerHTML;
  340. if(callback) callback();
  341. if(errorMatch) {
  342. AppToast.fire({ icon: 'error', title: errorMatch[1] });
  343. } else if(successMatch) {
  344. AppToast.fire({ icon: 'success', title: successMatch[1] });
  345. } else {
  346. AppToast.fire({ icon: 'success', title: 'Action Completed!' });
  347. }
  348. } else window.location.reload();
  349. } catch(err) { window.location.reload(); }
  350. }
  351. async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
  352. e.preventDefault();
  353. if(confirmMsg && !confirm(confirmMsg)) return;
  354. try {
  355. const res = await fetch(link.href);
  356. const html = await res.text();
  357. const doc = new DOMParser().parseFromString(html, 'text/html');
  358. const scriptContent = Array.from(doc.querySelectorAll('script')).map(s => s.innerHTML).join('\n');
  359. const errorMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'error',\s*title:\s*'([^']+)'/);
  360. const successMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'success',\s*title:\s*'([^']+)'/);
  361. const newCont = doc.getElementById(containerId);
  362. if(newCont) {
  363. document.getElementById(containerId).innerHTML = newCont.innerHTML;
  364. if(callback) callback();
  365. if(errorMatch) {
  366. AppToast.fire({ icon: 'error', title: errorMatch[1] });
  367. } else if(successMatch) {
  368. AppToast.fire({ icon: 'success', title: successMatch[1] });
  369. } else {
  370. AppToast.fire({ icon: 'success', title: 'Action Executed!' });
  371. }
  372. } else window.location.reload();
  373. } catch(err) { window.location.reload(); }
  374. }
  375. function initTableSort(container) {
  376. const getCellValue = (tr, idx) => tr.children[idx].innerText.trim();
  377. const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
  378. v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.localeCompare(v2, undefined, {numeric: true})
  379. )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
  380. container.querySelectorAll('table').forEach(table => {
  381. if(table.dataset.sortBound) return;
  382. table.dataset.sortBound = "true";
  383. table.querySelectorAll('th').forEach(th => {
  384. if (th.innerText.trim().toLowerCase() === 'actions') return;
  385. th.classList.add('cursor-pointer', 'hover:text-blue-400', 'transition-colors', 'select-none', 'group');
  386. const originalText = th.innerHTML;
  387. if(!originalText.includes('sort-icon')) {
  388. th.innerHTML = `<div class="flex items-center gap-2 justify-${th.classList.contains('text-right') ? 'end' : 'start'}">${originalText}<span class="sort-icon opacity-0 group-hover:opacity-50 transition-opacity text-blue-500 text-lg leading-none">↕</span></div>`;
  389. }
  390. th.addEventListener('click', function() {
  391. const tbody = table.querySelector('tbody');
  392. if(!tbody) return;
  393. table.querySelectorAll('.sort-icon').forEach(icon => {
  394. icon.innerHTML = '↕';
  395. icon.classList.remove('opacity-100', 'text-blue-400');
  396. icon.classList.add('opacity-0', 'text-blue-500');
  397. });
  398. this.asc = !this.asc;
  399. const icon = this.querySelector('.sort-icon');
  400. icon.innerHTML = this.asc ? '↑' : '↓';
  401. icon.classList.remove('opacity-0', 'text-blue-500', 'group-hover:opacity-50');
  402. icon.classList.add('opacity-100', 'text-blue-400');
  403. Array.from(tbody.querySelectorAll('tr'))
  404. .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc))
  405. .forEach(tr => tbody.appendChild(tr));
  406. });
  407. });
  408. });
  409. }
  410. function updateValidation(prefix) {
  411. const typeEl = document.getElementById(prefix + 'Rtype');
  412. const inputEl = document.getElementById(prefix + 'Value');
  413. const helperEl = document.getElementById(prefix + 'Helper');
  414. if(!typeEl || !inputEl || !helperEl) return;
  415. const rule = recordRules[typeEl.value];
  416. inputEl.placeholder = rule.placeholder;
  417. helperEl.innerText = rule.msg;
  418. inputEl.setCustomValidity('');
  419. inputEl.oninput = function() {
  420. let isValid = rule.regex.test(inputEl.value.trim());
  421. inputEl.setCustomValidity(isValid ? '' : rule.msg);
  422. helperEl.className = "text-[9px] font-black tracking-widest uppercase mt-2 h-3 " + (isValid ? "text-gray-500" : "text-red-500");
  423. };
  424. if(inputEl.value) inputEl.oninput();
  425. }
  426. function switchMainTab(tab) {
  427. activeMainTab = tab;
  428. document.querySelectorAll('.main-section').forEach(el => el.classList.add('hidden'));
  429. document.querySelectorAll('[id^="mainTab_"]').forEach(btn => {
  430. btn.classList.remove('border-blue-500', 'text-blue-500');
  431. btn.classList.add('border-transparent', 'text-gray-500');
  432. });
  433. const targetSection = document.getElementById('mainSection_' + tab);
  434. if(targetSection) targetSection.classList.remove('hidden');
  435. const btn = document.getElementById('mainTab_' + tab);
  436. if(btn) {
  437. btn.classList.replace('border-transparent', 'border-blue-500');
  438. btn.classList.replace('text-gray-500', 'text-blue-500');
  439. }
  440. }
  441. function switchTab(selectedType) {
  442. activeRecordTab = selectedType;
  443. document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
  444. document.querySelectorAll('.tab-btn').forEach(btn => {
  445. btn.classList.remove('border-blue-500', 'text-blue-400');
  446. btn.classList.add('border-transparent', 'text-gray-500');
  447. });
  448. const tabEl = document.getElementById('tab-' + selectedType);
  449. if(tabEl) tabEl.classList.remove('hidden');
  450. const activeBtn = document.getElementById('btn-' + selectedType);
  451. if(activeBtn) {
  452. activeBtn.classList.replace('border-transparent', 'border-blue-500');
  453. activeBtn.classList.replace('text-gray-500', 'text-blue-400');
  454. }
  455. }
  456. function restoreRecordTab() {
  457. switchMainTab(activeMainTab);
  458. if(document.getElementById('addRtype')) updateValidation('add');
  459. if(activeRecordTab && document.getElementById('btn-' + activeRecordTab)) {
  460. switchTab(activeRecordTab);
  461. } else {
  462. const firstBtn = document.querySelector('.tab-btn');
  463. if(firstBtn) switchTab(firstBtn.id.replace('btn-', ''));
  464. }
  465. initTableSort(document.getElementById('zone-dynamic-content'));
  466. }
  467. // ΔΙΟΡΘΩΣΗ: Ασφαλής ανάγνωση των data-attributes χωρίς errors από εισαγωγικά!
  468. function triggerEditModal(el) {
  469. const raw = el.getAttribute('data-raw');
  470. const name = el.getAttribute('data-name');
  471. const type = el.getAttribute('data-type');
  472. const val = el.getAttribute('data-val');
  473. document.getElementById('editOldRaw').value = raw;
  474. document.getElementById('editName').value = name;
  475. document.getElementById('editRtype').value = type;
  476. document.getElementById('editValue').value = val;
  477. document.getElementById('editModal').classList.replace('hidden', 'flex');
  478. updateValidation('edit');
  479. setTimeout(() => {
  480. const input = document.getElementById('editValue');
  481. input.focus();
  482. input.setSelectionRange(input.value.length, input.value.length);
  483. }, 50);
  484. }
  485. function closeEditModal() { document.getElementById('editModal').classList.replace('flex', 'hidden'); }
  486. function openEditIpsModal(currentIps) {
  487. const cont = document.getElementById('editIpsContainer');
  488. cont.innerHTML = '';
  489. currentIps.split(' ').forEach(ip => { if(ip.trim()) addEditIpField(ip.trim()); });
  490. document.getElementById('editIpsModal').classList.replace('hidden', 'flex');
  491. }
  492. function closeEditIpsModal() { document.getElementById('editIpsModal').classList.replace('flex', 'hidden'); }
  493. function addEditIpField(val) {
  494. const div = document.createElement('div'); div.className = 'flex gap-2';
  495. div.innerHTML = `<input type="text" name="ips[]" value="${val}" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono text-sm outline-none focus:border-blue-500 transition-colors"><button type="button" onclick="this.parentElement.remove()" class="text-gray-600 hover:text-red-500 px-3 font-black text-xl transition-colors">×</button>`;
  496. document.getElementById('editIpsContainer').appendChild(div);
  497. }
  498. function openConvertToSlaveModal() { document.getElementById('convertToSlaveModal').classList.replace('hidden', 'flex'); }
  499. function closeConvertToSlaveModal() { document.getElementById('convertToSlaveModal').classList.replace('flex', 'hidden'); }
  500. function addSlaveIpField() {
  501. const div = document.createElement('div'); div.className = 'flex gap-2 mt-2';
  502. div.innerHTML = `<input type="text" name="master_ips[]" placeholder="Master IP" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono text-sm outline-none focus:border-purple-500 transition-colors"><button type="button" onclick="this.parentElement.remove()" class="text-gray-600 hover:text-red-500 px-3 font-black text-xl transition-colors">×</button>`;
  503. document.getElementById('slaveIpsContainer').appendChild(div);
  504. }
  505. function runDigTest(domain) {
  506. const modal = document.getElementById('digModal');
  507. const loading = document.getElementById('digLoading');
  508. const content = document.getElementById('digContent');
  509. const analysisCont = document.getElementById('digAnalysis');
  510. const rawCont = document.getElementById('digRaw');
  511. modal.classList.replace('hidden', 'flex');
  512. loading.classList.remove('hidden');
  513. content.classList.add('hidden');
  514. fetch('/api/zones/test_dig', {
  515. method: 'POST',
  516. headers: {'Content-Type': 'application/json'},
  517. body: JSON.stringify({domain: domain, target_ip: '127.0.0.1'})
  518. })
  519. .then(r => r.json())
  520. .then(data => {
  521. loading.classList.add('hidden');
  522. content.classList.remove('hidden');
  523. if(data.success) {
  524. analysisCont.innerHTML = `
  525. <div class="bg-green-900/10 border border-green-500/20 p-4 rounded-2xl">
  526. <p class="text-[9px] text-gray-500 uppercase font-black">Total Records</p>
  527. <p class="text-2xl font-black text-white">${data.total}</p>
  528. </div>
  529. <div class="bg-blue-900/10 border border-blue-500/20 p-4 rounded-2xl flex flex-wrap gap-2 items-center">
  530. ${Object.entries(data.analysis).map(([type, count]) => `
  531. <div class="flex items-center gap-1 bg-black/40 px-2 py-1 rounded border border-gray-800">
  532. <span class="text-blue-400 font-bold text-[10px]">${type}:</span>
  533. <span class="text-white font-black text-[10px]">${count}</span>
  534. </div>
  535. `).join('')}
  536. </div>
  537. `;
  538. } else {
  539. analysisCont.innerHTML = `<div class="col-span-2 bg-red-900/20 border border-red-500/30 p-4 rounded-xl text-red-500 font-bold text-xs">Error: ${data.message}</div>`;
  540. }
  541. rawCont.innerText = data.raw;
  542. })
  543. .catch(err => {
  544. loading.classList.add('hidden');
  545. content.classList.remove('hidden');
  546. analysisCont.innerHTML = `<div class="col-span-2 bg-red-900/20 border border-red-500/30 p-4 rounded-xl text-red-500 font-bold text-xs">API Connection Failed</div>`;
  547. });
  548. }
  549. function closeDigModal() { document.getElementById('digModal').classList.replace('flex', 'hidden'); }
  550. document.addEventListener("DOMContentLoaded", () => { restoreRecordTab(); });
  551. </script>
  552. {% endblock %}