manage_zone.html 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  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 }}/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 }}/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 }}/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' %} ondblclick="openEditModal('{{r.raw|e}}', '{{r.friendly_name|e|default(r.name|e)}}', '{{rtype}}', '{{r.value|e|replace("'", "\\'")|replace("\"", "&quot;")}}')" {% endif %}>
  115. <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>
  116. <td class="px-6 py-4 text-gray-400 text-xs break-all">{{ r.value }}</td>
  117. <td class="px-6 py-4 text-right opacity-0 group-hover:opacity-100 transition whitespace-nowrap">
  118. {% if type == 'master' %}
  119. <button onclick="openEditModal('{{r.raw|e}}', '{{r.friendly_name|e|default(r.name|e)}}', '{{rtype}}', '{{r.value|e|replace("'", "\\'")|replace("\"", "&quot;")}}')" class="text-blue-500 hover:text-blue-400 text-[10px] font-black uppercase mr-4 transition">Edit</button>
  120. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/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 }}"><button type="submit" class="text-red-500/50 hover:text-red-500 text-[10px] font-black uppercase transition">Delete</button></form>
  121. {% else %}
  122. <span class="text-gray-600 text-[9px] uppercase font-black tracking-widest italic">Read Only</span>
  123. {% endif %}
  124. </td>
  125. </tr>
  126. {% endfor %}
  127. </tbody>
  128. </table>
  129. </div>
  130. {% endfor %}
  131. </div>
  132. {% if type == 'master' %}
  133. <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>
  134. {% endif %}
  135. {% else %}
  136. <div class="flex flex-col items-center justify-center p-16 text-center border-2 border-dashed border-gray-800 rounded-2xl">
  137. <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>
  138. <p class="text-gray-500 font-bold uppercase tracking-widest text-[11px]">No records found yet.</p>
  139. </div>
  140. {% endif %}
  141. {% endif %}
  142. </div>
  143. <div id="mainSection_audit" class="main-section hidden">
  144. <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
  145. <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl flex flex-col h-[500px]">
  146. <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>
  147. <div class="flex-1 overflow-y-auto custom-scroll p-6 font-mono text-[11px]">
  148. {% if audit_logs %}
  149. {% for log in audit_logs %}
  150. <div class="mb-2 pb-2 border-b border-gray-800 last:border-0 text-gray-400">
  151. <span class="text-gray-600 block mb-1">{{ log.time }}</span>
  152. <span class="text-blue-400 font-bold">{{ log.user }}</span>: {{ log.action }}
  153. </div>
  154. {% endfor %}
  155. {% else %}
  156. <p class="text-gray-600 text-center italic mt-10">No recent changes found.</p>
  157. {% endif %}
  158. </div>
  159. </div>
  160. <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl flex flex-col h-[500px]">
  161. <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>
  162. <div class="flex-1 overflow-y-auto custom-scroll p-6 font-mono text-[11px]">
  163. {% if backups %}
  164. {% for b in backups %}
  165. <div class="mb-3 pb-3 border-b border-gray-800 last:border-0 flex justify-between items-center group">
  166. <div>
  167. <span class="text-white block font-bold text-sm">{{ b.time }}</span>
  168. <span class="text-gray-600 text-[9px]">{{ b.file }}</span>
  169. </div>
  170. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/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)">
  171. <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>
  172. </form>
  173. </div>
  174. {% endfor %}
  175. {% else %}
  176. <p class="text-gray-600 text-center italic mt-10 text-sm">No backups found. Make a change to create one!</p>
  177. {% endif %}
  178. </div>
  179. </div>
  180. </div>
  181. </div>
  182. <div id="mainSection_security" class="main-section hidden">
  183. <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl p-8 max-w-4xl mx-auto text-center">
  184. <h2 class="text-3xl font-black uppercase tracking-tighter italic mb-4 text-white">DNSSEC Activation</h2>
  185. <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>
  186. <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">
  187. {% if dnssec_enabled %}
  188. <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>
  189. <h3 class="text-2xl font-black text-green-400 uppercase tracking-widest mb-2">Active & Protected</h3>
  190. <p class="text-gray-400 text-xs mb-6">Zone is cryptographically signed via BIND inline-signing.</p>
  191. {% else %}
  192. <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>
  193. <h3 class="text-2xl font-black text-gray-500 uppercase tracking-widest mb-2">Not Protected</h3>
  194. {% endif %}
  195. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/dnssec" method="POST" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  196. {% if dnssec_enabled %}
  197. <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>
  198. {% else %}
  199. <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>
  200. {% endif %}
  201. </form>
  202. </div>
  203. {% if dnssec_enabled and ds_records %}
  204. <div class="text-left bg-black/60 p-6 rounded-xl border border-gray-800">
  205. <h4 class="text-sm font-black text-blue-400 uppercase tracking-widest mb-4">DS Records for Registrar</h4>
  206. <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>
  207. <div class="space-y-2">
  208. {% for ds in ds_records %}
  209. <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!'});">
  210. {% endfor %}
  211. </div>
  212. </div>
  213. {% elif dnssec_enabled %}
  214. <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>
  215. {% endif %}
  216. </div>
  217. </div>
  218. <div id="editModal" class="hidden fixed inset-0 bg-black/95 items-center justify-center z-50 p-6 transition-opacity">
  219. <div class="bg-[#12161f] p-8 rounded-3xl border border-gray-800 shadow-2xl w-full max-w-xl">
  220. <h3 class="font-black text-xl mb-8 text-blue-400 uppercase border-b border-gray-800 pb-4">Modify Record</h3>
  221. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/edit_record" method="POST" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); closeEditModal(); })">
  222. <input type="hidden" name="old_raw" id="editOldRaw">
  223. <div class="grid grid-cols-3 gap-6">
  224. <div class="col-span-2">
  225. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Name / IP</label>
  226. <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">
  227. </div>
  228. <div>
  229. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Type</label>
  230. <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">
  231. </div>
  232. </div>
  233. <div>
  234. <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Value</label>
  235. <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">
  236. <p id="editHelper" class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3 transition-colors"></p>
  237. </div>
  238. <div class="flex gap-4 pt-6">
  239. <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>
  240. <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>
  241. </div>
  242. </form>
  243. </div>
  244. </div>
  245. <div id="editIpsModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-md items-center justify-center z-50 p-6 transition-opacity">
  246. <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
  247. <h3 class="text-xl font-black text-white mb-6 uppercase">Edit DNS Target IPs</h3>
  248. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/edit_ips" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  249. <div id="editIpsContainer" class="space-y-2"></div>
  250. <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>
  251. <div class="flex gap-4 pt-6">
  252. <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>
  253. <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl">Save</button>
  254. </div>
  255. </form>
  256. </div>
  257. </div>
  258. <div id="convertToSlaveModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-md items-center justify-center z-50 p-6 transition-opacity">
  259. <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
  260. <h3 class="text-xl font-black text-purple-400 mb-6 uppercase border-b border-gray-800 pb-4">Convert to Slave</h3>
  261. <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/convert_to_slave" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
  262. <p class="text-xs text-gray-400 font-bold mb-4">Please provide the Master DNS server IPs for this zone.</p>
  263. <div id="slaveIpsContainer" class="space-y-2">
  264. <div class="flex gap-2">
  265. <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">
  266. </div>
  267. </div>
  268. <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>
  269. <div class="flex gap-4 pt-6">
  270. <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>
  271. <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>
  272. </div>
  273. </form>
  274. </div>
  275. </div>
  276. <div id="digModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-sm items-center justify-center z-[100] p-6">
  277. <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]">
  278. <div class="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
  279. <h3 class="text-xl font-black text-green-400 uppercase italic">AXFR Transfer Simulation</h3>
  280. <button onclick="closeDigModal()" class="text-gray-500 hover:text-white text-2xl font-bold">&times;</button>
  281. </div>
  282. <div id="digLoading" class="py-20 text-center">
  283. <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto mb-4"></div>
  284. <p class="text-gray-500 font-black uppercase text-xs tracking-widest">Running Dig AXFR...</p>
  285. </div>
  286. <div id="digContent" class="hidden overflow-y-auto">
  287. <div class="grid grid-cols-2 gap-4 mb-6" id="digAnalysis">
  288. </div>
  289. <h4 class="text-[10px] font-black text-gray-500 uppercase mb-2 tracking-widest">Raw Output</h4>
  290. <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>
  291. </div>
  292. <div class="mt-8">
  293. <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>
  294. </div>
  295. </div>
  296. </div>
  297. </div>
  298. <script>
  299. let activeRecordTab = '';
  300. let activeMainTab = 'records';
  301. const recordRules = {
  302. '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" },
  303. 'AAAA': { regex: /^[a-fA-F0-9:]+$/, msg: "Must be a valid IPv6 address", placeholder: "2001:db8::1" },
  304. 'CNAME': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a domain without spaces", placeholder: "target.example.com." },
  305. 'NS': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a valid Nameserver domain", placeholder: "ns1.example.com." },
  306. 'TXT': { regex: /^.*$/, msg: "Any text value (Usually wrapped in quotes)", placeholder: '"text here"' },
  307. 'MX': { regex: /^\d+\s+[a-zA-Z0-9\-\_\.]+$/, msg: "Format: Priority Domain", placeholder: "10 mail.example.com." },
  308. 'SRV': { regex: /^\d+\s+\d+\s+\d+\s+[a-zA-Z0-9\-\_\.]+$/, msg: "Format: Priority Weight Port Domain", placeholder: "10 5 5060 sip.com." },
  309. 'PTR': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a valid domain name", placeholder: "host.example.com." }
  310. };
  311. // --- SPA & AJAX SMART PARSER ---
  312. async function handleAjaxForm(e, form, containerId, callback) {
  313. e.preventDefault();
  314. const btn = form.querySelector('button[type="submit"]');
  315. if(btn) { btn.innerHTML = spinnerSvg + ' WAIT'; btn.disabled = true; }
  316. try {
  317. const res = await fetch(form.action, { method: form.method || 'POST', body: new FormData(form) });
  318. const html = await res.text();
  319. const doc = new DOMParser().parseFromString(html, 'text/html');
  320. const scriptContent = Array.from(doc.querySelectorAll('script')).map(s => s.innerHTML).join('\n');
  321. const errorMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'error',\s*title:\s*'([^']+)'/);
  322. const successMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'success',\s*title:\s*'([^']+)'/);
  323. const newCont = doc.getElementById(containerId);
  324. if(newCont) {
  325. document.getElementById(containerId).innerHTML = newCont.innerHTML;
  326. if(callback) callback();
  327. if(errorMatch) {
  328. AppToast.fire({ icon: 'error', title: errorMatch[1] });
  329. } else if(successMatch) {
  330. AppToast.fire({ icon: 'success', title: successMatch[1] });
  331. } else {
  332. AppToast.fire({ icon: 'success', title: 'Action Completed!' });
  333. }
  334. } else window.location.reload();
  335. } catch(err) { window.location.reload(); }
  336. }
  337. async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
  338. e.preventDefault();
  339. if(confirmMsg && !confirm(confirmMsg)) return;
  340. try {
  341. const res = await fetch(link.href);
  342. const html = await res.text();
  343. const doc = new DOMParser().parseFromString(html, 'text/html');
  344. const scriptContent = Array.from(doc.querySelectorAll('script')).map(s => s.innerHTML).join('\n');
  345. const errorMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'error',\s*title:\s*'([^']+)'/);
  346. const successMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'success',\s*title:\s*'([^']+)'/);
  347. const newCont = doc.getElementById(containerId);
  348. if(newCont) {
  349. document.getElementById(containerId).innerHTML = newCont.innerHTML;
  350. if(callback) callback();
  351. if(errorMatch) {
  352. AppToast.fire({ icon: 'error', title: errorMatch[1] });
  353. } else if(successMatch) {
  354. AppToast.fire({ icon: 'success', title: successMatch[1] });
  355. } else {
  356. AppToast.fire({ icon: 'success', title: 'Action Executed!' });
  357. }
  358. } else window.location.reload();
  359. } catch(err) { window.location.reload(); }
  360. }
  361. function initTableSort(container) {
  362. const getCellValue = (tr, idx) => tr.children[idx].innerText.trim();
  363. const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
  364. v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.localeCompare(v2, undefined, {numeric: true})
  365. )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
  366. container.querySelectorAll('table').forEach(table => {
  367. if(table.dataset.sortBound) return;
  368. table.dataset.sortBound = "true";
  369. table.querySelectorAll('th').forEach(th => {
  370. if (th.innerText.trim().toLowerCase() === 'actions') return;
  371. th.classList.add('cursor-pointer', 'hover:text-blue-400', 'transition-colors', 'select-none', 'group');
  372. const originalText = th.innerHTML;
  373. if(!originalText.includes('sort-icon')) {
  374. 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>`;
  375. }
  376. th.addEventListener('click', function() {
  377. const tbody = table.querySelector('tbody');
  378. if(!tbody) return;
  379. table.querySelectorAll('.sort-icon').forEach(icon => {
  380. icon.innerHTML = '↕';
  381. icon.classList.remove('opacity-100', 'text-blue-400');
  382. icon.classList.add('opacity-0', 'text-blue-500');
  383. });
  384. this.asc = !this.asc;
  385. const icon = this.querySelector('.sort-icon');
  386. icon.innerHTML = this.asc ? '↑' : '↓';
  387. icon.classList.remove('opacity-0', 'text-blue-500', 'group-hover:opacity-50');
  388. icon.classList.add('opacity-100', 'text-blue-400');
  389. Array.from(tbody.querySelectorAll('tr'))
  390. .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc))
  391. .forEach(tr => tbody.appendChild(tr));
  392. });
  393. });
  394. });
  395. }
  396. function updateValidation(prefix) {
  397. const typeEl = document.getElementById(prefix + 'Rtype');
  398. const inputEl = document.getElementById(prefix + 'Value');
  399. const helperEl = document.getElementById(prefix + 'Helper');
  400. if(!typeEl || !inputEl || !helperEl) return;
  401. const rule = recordRules[typeEl.value];
  402. inputEl.placeholder = rule.placeholder;
  403. helperEl.innerText = rule.msg;
  404. inputEl.setCustomValidity('');
  405. inputEl.oninput = function() {
  406. let isValid = rule.regex.test(inputEl.value.trim());
  407. inputEl.setCustomValidity(isValid ? '' : rule.msg);
  408. helperEl.className = "text-[9px] font-black tracking-widest uppercase mt-2 h-3 " + (isValid ? "text-gray-500" : "text-red-500");
  409. };
  410. if(inputEl.value) inputEl.oninput();
  411. }
  412. function switchMainTab(tab) {
  413. activeMainTab = tab;
  414. document.querySelectorAll('.main-section').forEach(el => el.classList.add('hidden'));
  415. document.querySelectorAll('[id^="mainTab_"]').forEach(btn => {
  416. btn.classList.remove('border-blue-500', 'text-blue-500');
  417. btn.classList.add('border-transparent', 'text-gray-500');
  418. });
  419. document.getElementById('mainSection_' + tab).classList.remove('hidden');
  420. const btn = document.getElementById('mainTab_' + tab);
  421. if(btn) {
  422. btn.classList.replace('border-transparent', 'border-blue-500');
  423. btn.classList.replace('text-gray-500', 'text-blue-500');
  424. }
  425. }
  426. function switchTab(selectedType) {
  427. activeRecordTab = selectedType;
  428. document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
  429. document.querySelectorAll('.tab-btn').forEach(btn => {
  430. btn.classList.remove('border-blue-500', 'text-blue-400');
  431. btn.classList.add('border-transparent', 'text-gray-500');
  432. });
  433. const tabEl = document.getElementById('tab-' + selectedType);
  434. if(tabEl) tabEl.classList.remove('hidden');
  435. const activeBtn = document.getElementById('btn-' + selectedType);
  436. if(activeBtn) {
  437. activeBtn.classList.replace('border-transparent', 'border-blue-500');
  438. activeBtn.classList.replace('text-gray-500', 'text-blue-400');
  439. }
  440. }
  441. function restoreRecordTab() {
  442. switchMainTab(activeMainTab);
  443. if(document.getElementById('addRtype')) updateValidation('add');
  444. if(activeRecordTab && document.getElementById('btn-' + activeRecordTab)) {
  445. switchTab(activeRecordTab);
  446. } else {
  447. const firstBtn = document.querySelector('.tab-btn');
  448. if(firstBtn) switchTab(firstBtn.id.replace('btn-', ''));
  449. }
  450. initTableSort(document.getElementById('zone-dynamic-content'));
  451. }
  452. function openEditModal(raw, name, type, val) {
  453. document.getElementById('editOldRaw').value = raw;
  454. document.getElementById('editName').value = name;
  455. document.getElementById('editRtype').value = type;
  456. document.getElementById('editValue').value = val;
  457. document.getElementById('editModal').classList.replace('hidden', 'flex');
  458. updateValidation('edit');
  459. setTimeout(() => {
  460. const input = document.getElementById('editValue');
  461. input.focus();
  462. input.setSelectionRange(input.value.length, input.value.length);
  463. }, 50);
  464. }
  465. function closeEditModal() { document.getElementById('editModal').classList.replace('flex', 'hidden'); }
  466. function openEditIpsModal(currentIps) {
  467. const cont = document.getElementById('editIpsContainer');
  468. cont.innerHTML = '';
  469. currentIps.split(' ').forEach(ip => { if(ip.trim()) addEditIpField(ip.trim()); });
  470. document.getElementById('editIpsModal').classList.replace('hidden', 'flex');
  471. }
  472. function closeEditIpsModal() { document.getElementById('editIpsModal').classList.replace('flex', 'hidden'); }
  473. function addEditIpField(val) {
  474. const div = document.createElement('div'); div.className = 'flex gap-2';
  475. 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>`;
  476. document.getElementById('editIpsContainer').appendChild(div);
  477. }
  478. function openConvertToSlaveModal() { document.getElementById('convertToSlaveModal').classList.replace('hidden', 'flex'); }
  479. function closeConvertToSlaveModal() { document.getElementById('convertToSlaveModal').classList.replace('flex', 'hidden'); }
  480. function addSlaveIpField() {
  481. const div = document.createElement('div'); div.className = 'flex gap-2 mt-2';
  482. 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>`;
  483. document.getElementById('slaveIpsContainer').appendChild(div);
  484. }
  485. function runDigTest(domain) {
  486. const modal = document.getElementById('digModal');
  487. const loading = document.getElementById('digLoading');
  488. const content = document.getElementById('digContent');
  489. const analysisCont = document.getElementById('digAnalysis');
  490. const rawCont = document.getElementById('digRaw');
  491. modal.classList.replace('hidden', 'flex');
  492. loading.classList.remove('hidden');
  493. content.classList.add('hidden');
  494. fetch('/api/zones/test_dig', {
  495. method: 'POST',
  496. headers: {'Content-Type': 'application/json'},
  497. body: JSON.stringify({domain: domain, target_ip: '127.0.0.1'})
  498. })
  499. .then(r => r.json())
  500. .then(data => {
  501. loading.classList.add('hidden');
  502. content.classList.remove('hidden');
  503. if(data.success) {
  504. analysisCont.innerHTML = `
  505. <div class="bg-green-900/10 border border-green-500/20 p-4 rounded-2xl">
  506. <p class="text-[9px] text-gray-500 uppercase font-black">Total Records</p>
  507. <p class="text-2xl font-black text-white">${data.total}</p>
  508. </div>
  509. <div class="bg-blue-900/10 border border-blue-500/20 p-4 rounded-2xl flex flex-wrap gap-2 items-center">
  510. ${Object.entries(data.analysis).map(([type, count]) => `
  511. <div class="flex items-center gap-1 bg-black/40 px-2 py-1 rounded border border-gray-800">
  512. <span class="text-blue-400 font-bold text-[10px]">${type}:</span>
  513. <span class="text-white font-black text-[10px]">${count}</span>
  514. </div>
  515. `).join('')}
  516. </div>
  517. `;
  518. } else {
  519. 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>`;
  520. }
  521. rawCont.innerText = data.raw;
  522. })
  523. .catch(err => {
  524. loading.classList.add('hidden');
  525. content.classList.remove('hidden');
  526. 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>`;
  527. });
  528. }
  529. function closeDigModal() { document.getElementById('digModal').classList.replace('flex', 'hidden'); }
  530. document.addEventListener("DOMContentLoaded", () => { restoreRecordTab(); });
  531. </script>
  532. {% endblock %}