Skip to content

Commit 5852b08

Browse files
committed
v1.3.2: Custom DNS subdomain, availability check, fixed address UI - Custom subdomain per server with availability validation - Fixed DNS address shown in Dashboard when tunnel active - Editable subdomain with check button - Copy button for DNS address - Worker: added 'check' action endpoint - Backend: new endpoints for subdomain CRUD and availability check
1 parent cc7e584 commit 5852b08

4 files changed

Lines changed: 221 additions & 6 deletions

File tree

backend/api_server.py

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def __init__(self):
197197
# Tunnel management
198198
self.tunnel_process: Optional[subprocess.Popen] = None
199199
self.tunnel_address: Optional[str] = None
200+
self.dns_address: Optional[str] = None
200201

201202
# Install Progress Tracking
202203
self.install_progress: int = 0
@@ -348,6 +349,11 @@ def load_server(self, server_id):
348349
)
349350
self.active_handlers[server_id] = new_handler
350351

352+
# Cargar subdominio DNS personalizado si existe
353+
dns_sub = server_config.get("dns_subdomain", "")
354+
if dns_sub:
355+
new_handler.dns_subdomain = dns_sub
356+
351357
self.config_manager.config["last_selected_id"] = server_id
352358
self.config_manager.save()
353359
return server_config
@@ -1706,11 +1712,18 @@ def run_backup():
17061712

17071713
# --- DNS Proxy Helper ---
17081714
def _get_server_slug(state):
1709-
"""Genera un slug a partir del nombre del servidor para usar como subdominio."""
1715+
"""Genera un slug a partir del subdominio personalizado o nombre del servidor."""
17101716
if not state or not state.server_handler:
17111717
return "minecraft"
1712-
folder_name = os.path.basename(state.server_handler.server_path.rstrip("/\\"))
1713-
# Limpiar: solo letras, números, guiones
1718+
# Primero mirar si el servidor tiene un subdominio personalizado
1719+
handler = state.server_handler
1720+
if hasattr(handler, "dns_subdomain") and handler.dns_subdomain:
1721+
slug = handler.dns_subdomain.strip().lower()
1722+
slug = "".join(c if c.isalnum() or c in "-" else "-" for c in slug).strip("-")
1723+
if slug:
1724+
return slug
1725+
# Fallback: nombre de la carpeta
1726+
folder_name = os.path.basename(handler.server_path.rstrip("/\\"))
17141727
slug = "".join(c if c.isalnum() or c in "-_" else "-" for c in folder_name.lower())
17151728
slug = slug.strip("-")
17161729
return slug if slug else "minecraft"
@@ -1750,9 +1763,15 @@ def _update_dns_record_proxy(state):
17501763
},
17511764
timeout=5,
17521765
)
1766+
state.dns_address = f"{slug}.play.ariser.app"
17531767
state.broadcast_log_sync(
1754-
f"🌐 DNS updated: {slug}{state.tunnel_address}", "info"
1768+
f"🌐 DNS updated: {state.dns_address}{state.tunnel_address}", "info"
17551769
)
1770+
state.broadcast_log_sync({
1771+
"type": "dns_updated",
1772+
"address": state.dns_address,
1773+
"target": state.tunnel_address,
1774+
})
17561775
except Exception as e:
17571776
state.broadcast_log_sync(f"⚠️ DNS update failed: {e}", "warning")
17581777

@@ -1784,12 +1803,13 @@ def _delete_dns_record_proxy(state):
17841803
@app.get("/tunnel/status")
17851804
def get_tunnel_status():
17861805
if not state:
1787-
return {"active": False, "address": None}
1806+
return {"active": False, "address": None, "dns_address": None}
17881807

17891808
return {
17901809
"active": state.tunnel_process is not None
17911810
and state.tunnel_process.poll() is None,
17921811
"address": state.tunnel_address,
1812+
"dns_address": state.dns_address,
17931813
}
17941814

17951815

@@ -2233,6 +2253,7 @@ def run_tunnel():
22332253
state.broadcast_log_sync("🔴 Tunnel closed", "warning")
22342254
state.broadcast_log_sync({"type": "tunnel_disconnected"})
22352255
state.tunnel_address = None
2256+
state.dns_address = None
22362257
# Clean up DNS record
22372258
_delete_dns_record_proxy(state)
22382259

@@ -2294,6 +2315,78 @@ def set_tunnel_address(req: SetTunnelAddressRequest):
22942315
return {"status": "success"}
22952316

22962317

2318+
# --- DNS Subdomain Endpoints ---
2319+
2320+
@app.get("/server/dns-subdomain")
2321+
def get_dns_subdomain():
2322+
if not state or not state.server_handler:
2323+
return {"subdomain": "", "address": ""}
2324+
slug = _get_server_slug(state)
2325+
settings = _get_dns_settings(state)
2326+
domain = ""
2327+
if settings["enabled"]:
2328+
try:
2329+
app = state.config_manager.config.get("app_settings", {})
2330+
url = settings["url"]
2331+
# Extraer dominio del Worker URL o usar variables
2332+
domain = f"{slug}.play.ariser.app"
2333+
except Exception:
2334+
pass
2335+
return {
2336+
"subdomain": slug,
2337+
"address": domain if settings["enabled"] else "",
2338+
"enabled": settings["enabled"],
2339+
}
2340+
2341+
2342+
@app.post("/server/dns-subdomain")
2343+
async def set_dns_subdomain(request: Request):
2344+
if not state or not state.server_handler:
2345+
raise HTTPException(status_code=400, detail="Server not configured")
2346+
body = await request.json()
2347+
subdomain = (body.get("subdomain") or "").strip().lower()
2348+
subdomain = "".join(c if c.isalnum() or c in "-" else "-" for c in subdomain).strip("-")
2349+
2350+
if not subdomain:
2351+
raise HTTPException(status_code=400, detail="Invalid subdomain")
2352+
2353+
# Guardar en la config del servidor
2354+
server_id = state.selected_server_id
2355+
state.config_manager.update_server(server_id, {"dns_subdomain": subdomain})
2356+
state.server_handler.dns_subdomain = subdomain
2357+
2358+
address = f"{subdomain}.play.ariser.app"
2359+
return {"subdomain": subdomain, "address": address}
2360+
2361+
2362+
@app.post("/server/dns-check")
2363+
async def check_dns_subdomain(request: Request):
2364+
if not state:
2365+
raise HTTPException(status_code=500, detail="App state not initialized")
2366+
body = await request.json()
2367+
subdomain = (body.get("subdomain") or "").strip().lower()
2368+
subdomain = "".join(c if c.isalnum() or c in "-" else "-" for c in subdomain).strip("-")
2369+
2370+
if not subdomain:
2371+
return {"available": False, "error": "Invalid subdomain"}
2372+
2373+
settings = _get_dns_settings(state)
2374+
if not settings["url"]:
2375+
return {"available": True, "note": "No proxy configured"}
2376+
2377+
try:
2378+
import requests as req
2379+
r = req.post(
2380+
settings["url"],
2381+
json={"subdomain": subdomain, "target": "", "action": "check"},
2382+
timeout=5,
2383+
)
2384+
data = r.json()
2385+
return data
2386+
except Exception as e:
2387+
return {"available": True, "note": f"Could not verify: {e}"}
2388+
2389+
22972390
# --- Mods Endpoints ---
22982391
@app.get("/mods/search")
22992392
def search_mods(

electron-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "minecraft-server-gui",
33
"private": true,
4-
"version": "1.3.1",
4+
"version": "1.3.2",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/CalaKuad1/Minecraft-Local-Server-GUI.git"

electron-app/src/api.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,23 @@ export const api = {
271271
body: JSON.stringify({ address })
272272
});
273273
},
274+
getDnsSubdomain: async () => {
275+
return await fetchJson(`${API_URL}/server/dns-subdomain`);
276+
},
277+
setDnsSubdomain: async (subdomain) => {
278+
return await fetchJson(`${API_URL}/server/dns-subdomain`, {
279+
method: 'POST',
280+
headers: { 'Content-Type': 'application/json' },
281+
body: JSON.stringify({ subdomain })
282+
});
283+
},
284+
checkDnsSubdomain: async (subdomain) => {
285+
return await fetchJson(`${API_URL}/server/dns-check`, {
286+
method: 'POST',
287+
headers: { 'Content-Type': 'application/json' },
288+
body: JSON.stringify({ subdomain })
289+
});
290+
},
274291

275292
// --- Mods ---
276293
searchMods: async (query, loader = 'fabric', version = null, projectType = 'mod', sort = 'downloads', category = null) => {

electron-app/src/components/Dashboard.jsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,13 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
370370
setTunnelConnecting(false);
371371
} else if (!tunnelConnecting && tunnelAddress) {
372372
setTunnelAddress(null);
373+
setTunnelConnecting(false);
374+
}
375+
if (serverStatus.tunnel.dns_address) {
376+
setDnsAddress(serverStatus.tunnel.dns_address);
373377
}
374378
}
379+
}
375380
}
376381
}, [serverStatus]);
377382

@@ -404,6 +409,11 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
404409
const [playitClaimLink, setPlayitClaimLink] = useState(null);
405410
const [history, setHistory] = useState({ cpu: [], ram: [] });
406411
const [autoRestart, setAutoRestart] = useState(false);
412+
const [dnsAddress, setDnsAddress] = useState(null);
413+
const [dnsSubdomain, setDnsSubdomain] = useState('');
414+
const [dnsEditing, setDnsEditing] = useState(false);
415+
const [dnsAvailable, setDnsAvailable] = useState(null);
416+
const [dnsChecking, setDnsChecking] = useState(false);
407417

408418
const { isConnected, subscribe, send } = useWebSocket();
409419
const logsEndRef = useRef(null);
@@ -451,6 +461,11 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
451461
return;
452462
}
453463

464+
if (item.type === 'dns_updated') {
465+
setDnsAddress(item.address);
466+
return;
467+
}
468+
454469
if (item.message !== undefined || item.level) {
455470
const msgText = typeof item.message === 'string' ? item.message : JSON.stringify(item.message || '');
456471

@@ -477,6 +492,18 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
477492
return subscribe('dashboard', handleWsMessage);
478493
}, [subscribe, handleWsMessage]);
479494

495+
// Cargar subdominio DNS al montar
496+
useEffect(() => {
497+
const load = async () => {
498+
try {
499+
const data = await api.getDnsSubdomain();
500+
if (data?.subdomain) setDnsSubdomain(data.subdomain);
501+
if (data?.address) setDnsAddress(data.address);
502+
} catch (e) {}
503+
};
504+
load();
505+
}, [serverStatus?.server_id]);
506+
480507
// Robust Auto-scroll logs
481508
useEffect(() => {
482509
if (logsEndRef.current && !userScrolledUpRef.current) {
@@ -781,6 +808,84 @@ export default function Dashboard({ status: serverStatus, onRefresh }) {
781808
</div>
782809
</div>
783810

811+
{/* Fixed DNS Address - appears when tunnel is active */}
812+
{tunnelAddress && (
813+
<div className="flex items-center gap-4 p-4 bg-emerald-500/[0.03] border border-emerald-500/10 rounded-sm relative z-50 backdrop-blur-2xl">
814+
<div className="p-2 rounded-sm border border-emerald-500/20 bg-emerald-500/5 text-emerald-400">
815+
<Globe size={16} />
816+
</div>
817+
<div className="flex-1 min-w-0">
818+
<div className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest leading-none mb-1">
819+
Fixed Address
820+
</div>
821+
{dnsAddress ? (
822+
<div className="flex items-center gap-2">
823+
<span className="text-sm font-mono text-emerald-400 font-bold select-all">{dnsAddress}</span>
824+
<button
825+
onClick={() => { navigator.clipboard.writeText(dnsAddress); }}
826+
className="text-[10px] text-zinc-500 hover:text-white px-2 py-0.5 rounded-sm border border-white/5 hover:border-white/20 transition-colors uppercase tracking-wider"
827+
>
828+
Copy
829+
</button>
830+
</div>
831+
) : (
832+
<div className="text-sm font-mono text-zinc-500">
833+
Enable "Fixed Address" in Settings to get a permanent domain
834+
</div>
835+
)}
836+
</div>
837+
<div className="flex items-center gap-2 shrink-0">
838+
{dnsEditing ? (
839+
<form onSubmit={async (e) => {
840+
e.preventDefault();
841+
const val = e.target.elements.subdomain.value.trim();
842+
if (!val) return;
843+
setDnsChecking(true);
844+
try {
845+
const check = await api.checkDnsSubdomain(val);
846+
if (!check.available) {
847+
setDnsAvailable(false);
848+
setDnsChecking(false);
849+
return;
850+
}
851+
const result = await api.setDnsSubdomain(val);
852+
setDnsSubdomain(result.subdomain);
853+
setDnsAddress(result.address);
854+
setDnsEditing(false);
855+
setDnsAvailable(null);
856+
} catch (err) {
857+
console.error(err);
858+
}
859+
setDnsChecking(false);
860+
}} className="flex items-center gap-1">
861+
<span className="text-xs text-zinc-500">.play.ariser.app</span>
862+
<input
863+
name="subdomain"
864+
defaultValue={dnsSubdomain}
865+
placeholder="survival"
866+
className="w-24 bg-black/40 border border-white/10 rounded-sm px-2 py-1 text-xs text-white placeholder-zinc-700 font-mono outline-none focus:border-emerald-500/40"
867+
autoFocus
868+
/>
869+
<button type="submit" disabled={dnsChecking} className="text-[10px] px-2 py-1 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 rounded-sm hover:bg-emerald-500/20 disabled:opacity-50 uppercase tracking-wider">
870+
{dnsChecking ? '...' : 'Set'}
871+
</button>
872+
<button type="button" onClick={() => { setDnsEditing(false); setDnsAvailable(null); }} className="text-[10px] px-1 py-1 text-zinc-500 hover:text-white"></button>
873+
</form>
874+
) : (
875+
<button
876+
onClick={() => setDnsEditing(true)}
877+
className="text-[10px] px-2 py-1 text-zinc-500 hover:text-white rounded-sm border border-white/5 hover:border-white/20 transition-colors uppercase tracking-wider"
878+
>
879+
{dnsSubdomain ? dnsSubdomain : 'Customize'}
880+
</button>
881+
)}
882+
</div>
883+
{dnsAvailable === false && (
884+
<div className="text-[10px] text-red-400 absolute -bottom-5 left-0">That name is already taken</div>
885+
)}
886+
</div>
887+
)}
888+
784889
{/* Stats Grid */}
785890
<div className="grid grid-cols-1 md:grid-cols-4 gap-5 relative z-10">
786891
<StatCard icon={Users} label={t('dashboard.players_online')} value={onlineCount !== undefined ? `${onlineCount}` : '-'} sublabel={`/ ${status.max_players || 20} ${t('status.online')}`} />

0 commit comments

Comments
 (0)