2026-02-11 23:22:20 +01:00
|
|
|
package ins.frontend;
|
|
|
|
|
|
|
|
|
|
import ins.frontend.utils.RegistrarDao;
|
|
|
|
|
import ins.frontend.utils.WebApp;
|
|
|
|
|
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
|
|
|
|
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
|
|
|
|
import org.openautonomousconnection.webserver.api.Route;
|
|
|
|
|
import org.openautonomousconnection.webserver.api.SessionContext;
|
|
|
|
|
import org.openautonomousconnection.webserver.api.WebPage;
|
|
|
|
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
|
|
|
|
import org.openautonomousconnection.webserver.utils.Html;
|
|
|
|
|
import org.openautonomousconnection.webserver.utils.MergedRequestParams;
|
|
|
|
|
import org.openautonomousconnection.webserver.utils.QuerySupport;
|
|
|
|
|
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.Locale;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* INS registrar ins.frontend (TLN / InfoName / Records) with proper POST parameter parsing.
|
|
|
|
|
*
|
|
|
|
|
* <p>Supported actions (POST recommended for mutations):</p>
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>create_tln</li>
|
|
|
|
|
* <li>update_tln</li>
|
|
|
|
|
* <li>delete_tln</li>
|
|
|
|
|
* <li>create_infoname</li>
|
|
|
|
|
* <li>delete_infoname</li>
|
|
|
|
|
* <li>add_record</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* <p>Important: Listing/editing/deleting records requires DAO methods that are not part of the provided snippet.
|
|
|
|
|
* This page currently supports adding records only.</p>
|
|
|
|
|
*/
|
|
|
|
|
@Route(path = "dashboard.html")
|
|
|
|
|
public final class dashboard implements WebPage {
|
|
|
|
|
|
|
|
|
|
private static Integer normalizeNullableInt(String s) {
|
|
|
|
|
if (s == null) return null;
|
|
|
|
|
String t = s.trim();
|
|
|
|
|
if (t.isEmpty()) return null;
|
|
|
|
|
try {
|
|
|
|
|
return Integer.parseInt(t);
|
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public WebResponsePacket handle(WebPageContext ctx) throws Exception {
|
|
|
|
|
WebApp.init();
|
|
|
|
|
|
|
|
|
|
SessionContext session = SessionContext.from(
|
|
|
|
|
ctx.client,
|
|
|
|
|
(ProtocolWebServer) ctx.client.getServer(),
|
|
|
|
|
ctx.request.getHeaders()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!session.isValid() || session.getUser() == null) {
|
|
|
|
|
return plain(401, "Authentication required (session).");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int userId;
|
|
|
|
|
try {
|
|
|
|
|
userId = Integer.parseInt(session.getUser());
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return plain(401, "Invalid session user.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RegistrarDao dao = WebApp.get().dao();
|
|
|
|
|
|
|
|
|
|
// Raw target and merged params (GET + POST).
|
|
|
|
|
String rawTarget = org.openautonomousconnection.webserver.utils.QuerySupport.extractRawTarget(ctx.request);
|
|
|
|
|
Map<String, String> headers = ctx.request.getHeaders();
|
|
|
|
|
byte[] body = ctx.request.getBody();
|
|
|
|
|
|
|
|
|
|
MergedRequestParams p = MergedRequestParams.from(rawTarget, headers, body);
|
|
|
|
|
|
|
|
|
|
String msg = null;
|
|
|
|
|
String err = null;
|
|
|
|
|
|
|
|
|
|
String action = p.getOr("action", "").trim();
|
|
|
|
|
if (!action.isBlank()) {
|
|
|
|
|
try {
|
|
|
|
|
ActionResult r = executeAction(action, p, userId, dao);
|
|
|
|
|
msg = r.msg();
|
|
|
|
|
err = r.err();
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
err = "Action failed: " + safeMsg(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return render(userId, msg, err, dao);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ActionResult executeAction(String action, MergedRequestParams p, int userId, RegistrarDao dao) throws Exception {
|
|
|
|
|
String a = action.trim().toLowerCase(Locale.ROOT);
|
|
|
|
|
|
|
|
|
|
if ("create_tln".equals(a)) {
|
|
|
|
|
String name = p.get("name");
|
|
|
|
|
String info = p.getOr("info", "");
|
|
|
|
|
boolean isPublic = p.getBool("is_public");
|
|
|
|
|
boolean allowSubs = p.getBool("allow_subdomains");
|
|
|
|
|
|
|
|
|
|
if (name == null || name.isBlank()) return ActionResult.err("Missing TLN name.");
|
|
|
|
|
|
|
|
|
|
int id = dao.createTln(name.trim(), info, userId, isPublic, allowSubs);
|
|
|
|
|
return ActionResult.ok("Created TLN id=" + id + " (" + name.trim() + ")");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ("update_tln".equals(a)) {
|
|
|
|
|
int id = p.getInt("id", -1);
|
|
|
|
|
String info = p.getOr("info", "");
|
|
|
|
|
boolean isPublic = p.getBool("is_public");
|
|
|
|
|
boolean allowSubs = p.getBool("allow_subdomains");
|
|
|
|
|
|
|
|
|
|
if (id <= 0) return ActionResult.err("Invalid TLN id.");
|
|
|
|
|
|
|
|
|
|
boolean ok = dao.updateTlnOwned(id, userId, info, isPublic, allowSubs);
|
|
|
|
|
return ActionResult.ok(ok ? ("Updated TLN id=" + id) : "Not owner / not found.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ("delete_tln".equals(a)) {
|
|
|
|
|
int id = p.getInt("id", -1);
|
|
|
|
|
if (id <= 0) return ActionResult.err("Invalid TLN id.");
|
|
|
|
|
|
|
|
|
|
boolean ok = dao.deleteTlnOwned(id, userId);
|
|
|
|
|
return ActionResult.ok(ok ? ("Deleted TLN id=" + id) : "Not owner / not found.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ("create_infoname".equals(a)) {
|
|
|
|
|
String tlnName = p.get("tln");
|
|
|
|
|
String info = p.get("info");
|
|
|
|
|
|
|
|
|
|
if (tlnName == null || tlnName.isBlank() || info == null || info.isBlank()) {
|
|
|
|
|
return ActionResult.err("Missing tln / info.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RegistrarDao.TlnRow tln = dao.findTlnByName(tlnName.trim()).orElse(null);
|
|
|
|
|
if (tln == null) return ActionResult.err("Unknown TLN: " + tlnName);
|
|
|
|
|
|
|
|
|
|
if (!RegistrarDao.canUseTln(tln, userId)) {
|
|
|
|
|
return ActionResult.err("TLN not public and not owned by you.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int id = dao.createInfoName(tln, info.trim(), userId);
|
|
|
|
|
return ActionResult.ok("Created infoname id=" + id + " (" + info.trim() + "." + tln.name() + ")");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ("delete_infoname".equals(a)) {
|
|
|
|
|
int id = p.getInt("id", -1);
|
|
|
|
|
if (id <= 0) return ActionResult.err("Invalid infoname id.");
|
|
|
|
|
|
|
|
|
|
boolean ok = dao.deleteInfoNameOwned(id, userId);
|
|
|
|
|
return ActionResult.ok(ok ? ("Deleted infoname id=" + id) : "Not owner / not found.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ("add_record".equals(a)) {
|
|
|
|
|
int infonameId = p.getInt("infoname_id", -1);
|
|
|
|
|
String sub = p.get("sub");
|
|
|
|
|
String type = p.getOr("type", "").trim().toUpperCase(Locale.ROOT);
|
|
|
|
|
String value = p.get("value");
|
|
|
|
|
|
|
|
|
|
int ttl = p.getInt("ttl", 3600);
|
|
|
|
|
|
|
|
|
|
Integer priority = normalizeNullableInt(p.get("priority"));
|
|
|
|
|
Integer port = normalizeNullableInt(p.get("port"));
|
|
|
|
|
Integer weight = normalizeNullableInt(p.get("weight"));
|
|
|
|
|
|
|
|
|
|
if (infonameId <= 0) return ActionResult.err("Invalid infoname_id.");
|
|
|
|
|
if (!dao.isOwnerOfInfoName(infonameId, userId)) return ActionResult.err("Not owner of this infoname.");
|
|
|
|
|
if (type.isBlank() || value == null || value.isBlank()) return ActionResult.err("Missing type/value.");
|
|
|
|
|
|
|
|
|
|
// Validate allow_subdomains against TLN of this infoname (owned list contains TLN metadata).
|
|
|
|
|
RegistrarDao.InfoNameRow[] owned = dao.listOwnedInfoNames(userId);
|
|
|
|
|
RegistrarDao.InfoNameRow row = null;
|
|
|
|
|
for (RegistrarDao.InfoNameRow r : owned) {
|
|
|
|
|
if (r.id() == infonameId) {
|
|
|
|
|
row = r;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (row == null) return ActionResult.err("Infoname not found in your ownership list.");
|
|
|
|
|
|
|
|
|
|
if (!RegistrarDao.canUseSubname(row.tln(), userId, sub)) {
|
|
|
|
|
return ActionResult.err("Subnames are not allowed for this TLN (allow_subdomains=0) unless you own the TLN.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Integer subId = dao.ensureSubname(infonameId, sub);
|
|
|
|
|
int rid = dao.addRecord(infonameId, subId, type, value.trim(), ttl, priority, port, weight);
|
|
|
|
|
return ActionResult.ok("Added record id=" + rid + " type=" + type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ActionResult.err("Unknown action: " + action);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private WebResponsePacket render(int userId, String msg, String err, RegistrarDao dao) throws Exception {
|
|
|
|
|
RegistrarDao.TlnRow[] tlns = dao.listVisibleTlns(userId);
|
|
|
|
|
RegistrarDao.InfoNameRow[] owned = dao.listOwnedInfoNames(userId);
|
|
|
|
|
|
|
|
|
|
String tlnHtml = renderTlnSection(tlns, userId);
|
|
|
|
|
String infoHtml = renderInfoNamesSection(owned);
|
|
|
|
|
|
|
|
|
|
String body = """
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>INS Registrar</h2>
|
|
|
|
|
%s
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
<h3>Create TLN</h3>
|
|
|
|
|
<form method="post" action="dashboard.html" class="form">
|
|
|
|
|
<input type="hidden" name="action" value="create_tln">
|
|
|
|
|
<label><span class="muted">name</span><input type="text" name="name" placeholder="com" required></label>
|
|
|
|
|
<label><span class="muted">info</span><input type="text" name="info" required placeholder="ip[:port]"></label>
|
|
|
|
|
<label><span class="muted">is_public</span><input type="text" name="is_public" value="1" required></label>
|
|
|
|
|
<label><span class="muted">allow_subdomains</span><input type="text" name="allow_subdomains" value="1" required></label>
|
|
|
|
|
<button type="submit">Create TLN</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<h3>TLNs (public + owned)</h3>
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
<h3>Create InfoName</h3>
|
|
|
|
|
<p class="muted">Allowed if TLN is public or owned by you.</p>
|
|
|
|
|
<form method="post" action="dashboard.html" class="form">
|
|
|
|
|
<input type="hidden" name="action" value="create_infoname">
|
|
|
|
|
<label><span class="muted">tln</span><input type="text" name="tln" placeholder="com" required></label>
|
|
|
|
|
<label><span class="muted">info</span><input type="text" name="info" placeholder="example" required></label>
|
|
|
|
|
<button type="submit">Create InfoName</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<h3>Add Record</h3>
|
|
|
|
|
<p class="muted">Subname requires allow_subdomains=1 unless you own the TLN. Root (no sub) always allowed.</p>
|
|
|
|
|
<form method="post" action="dashboard.html" class="form">
|
|
|
|
|
<input type="hidden" name="action" value="add_record">
|
|
|
|
|
<label><span class="muted">infoname_id</span><input type="number" name="infoname_id" min="1" required></label>
|
|
|
|
|
<label><span class="muted">sub (optional)</span><input type="text" name="sub" placeholder="www"></label>
|
|
|
|
|
<label><span class="muted">type</span><input type="text" name="type" placeholder="A/AAAA/TXT/CNAME/MX/SRV/NS" required></label>
|
|
|
|
|
<label><span class="muted">value</span><input type="text" name="value" required></label>
|
|
|
|
|
<label><span class="muted">ttl</span><input type="number" name="ttl" value="3600"></label>
|
|
|
|
|
<label><span class="muted">priority (MX/SRV)</span><input type="number" name="priority"></label>
|
2026-02-11 23:01:13 +00:00
|
|
|
<label><span class="muted">port (SRV)</span><input type="number" placeholder="Default: 1028" value="1028" name="port"></label>
|
2026-02-11 23:22:20 +01:00
|
|
|
<label><span class="muted">weight (SRV)</span><input type="number" name="weight"></label>
|
|
|
|
|
<button type="submit">Add Record</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<h3>Your InfoNames</h3>
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col"><a href="index.html">Home</a></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
""".formatted(
|
|
|
|
|
msg == null ? "" : "<p class='ok'>" + Html.esc(msg) + "</p>",
|
|
|
|
|
err == null ? "" : "<p class='err'>" + Html.esc(err) + "</p>",
|
|
|
|
|
tlnHtml,
|
|
|
|
|
infoHtml
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
String html = Html.page("INS Registrar", body);
|
|
|
|
|
return new WebResponsePacket(200, "text/html", new HashMap<>(), Html.utf8(html));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String renderTlnSection(RegistrarDao.TlnRow[] tlns, int userId) {
|
|
|
|
|
if (tlns == null || tlns.length == 0) {
|
|
|
|
|
return "<p class='muted'>No TLNs available.</p>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
sb.append("<ul>");
|
|
|
|
|
for (RegistrarDao.TlnRow t : tlns) {
|
|
|
|
|
boolean ownedByMe = (t.ownerId() != null && t.ownerId() == userId);
|
|
|
|
|
|
|
|
|
|
sb.append("<li>")
|
|
|
|
|
.append("<code>").append(Html.esc(t.name())).append("</code>")
|
|
|
|
|
.append(" <span class='muted'>(id=").append(t.id()).append(")</span> ")
|
|
|
|
|
.append(ownedByMe ? "<span class='muted'>(owner)</span>" : "<span class='muted'>(public)</span>")
|
|
|
|
|
.append(" <span class='muted'>is_public=").append(t.isPublic() ? "1" : "0")
|
|
|
|
|
.append(", allow_subdomains=").append(t.allowSubdomains() ? "1" : "0")
|
|
|
|
|
.append("</span>");
|
|
|
|
|
|
|
|
|
|
if (ownedByMe) {
|
|
|
|
|
sb.append("""
|
|
|
|
|
<div class="row" style="margin-top:8px; gap:10px; flex-wrap:wrap;">
|
|
|
|
|
<form method="post" action="dashboard.html" class="form" style="margin:0;">
|
|
|
|
|
<input type="hidden" name="action" value="update_tln">
|
|
|
|
|
<input type="hidden" name="id" value="%d">
|
|
|
|
|
<label>
|
|
|
|
|
<span class="muted">info</span>
|
|
|
|
|
<input type="text" name="info" value="%s">
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span class="muted">is_public</span>
|
|
|
|
|
<input type="text" name="is_public" value="%s" style="width:70px;">
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span class="muted">allow_subdomains</span>
|
|
|
|
|
<input type="text" name="allow_subdomains" value="%s" style="width:70px;">
|
|
|
|
|
</label>
|
|
|
|
|
<button type="submit">Update</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<form method="post" action="dashboard.html" style="margin:0;">
|
|
|
|
|
<input type="hidden" name="action" value="delete_tln">
|
|
|
|
|
<input type="hidden" name="id" value="%d">
|
|
|
|
|
<button type="submit" class="muted">Delete</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
""".formatted(
|
|
|
|
|
t.id(),
|
|
|
|
|
Html.esc(t.info() == null ? "" : t.info()),
|
|
|
|
|
t.isPublic() ? "1" : "0",
|
|
|
|
|
t.allowSubdomains() ? "1" : "0",
|
|
|
|
|
t.id()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb.append("</li>");
|
|
|
|
|
}
|
|
|
|
|
sb.append("</ul>");
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String renderInfoNamesSection(RegistrarDao.InfoNameRow[] owned) {
|
|
|
|
|
if (owned == null || owned.length == 0) {
|
|
|
|
|
return "<p class='muted'>No infonames yet.</p>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
sb.append("<ul>");
|
|
|
|
|
for (RegistrarDao.InfoNameRow r : owned) {
|
|
|
|
|
sb.append("<li>")
|
|
|
|
|
.append("<code>")
|
|
|
|
|
.append(Html.esc(r.info())).append(".").append(Html.esc(r.tln().name()))
|
|
|
|
|
.append("</code>")
|
|
|
|
|
.append(" <span class='muted'>(id=").append(r.id()).append(")</span>")
|
|
|
|
|
.append("""
|
|
|
|
|
<form method="post" action="dashboard.html" style="display:inline; margin-left:10px;">
|
|
|
|
|
<input type="hidden" name="action" value="delete_infoname">
|
|
|
|
|
<input type="hidden" name="id" value="%d">
|
|
|
|
|
<button type="submit" class="muted">Delete</button>
|
|
|
|
|
</form>
|
|
|
|
|
""".formatted(r.id()))
|
|
|
|
|
.append("</li>");
|
|
|
|
|
}
|
|
|
|
|
sb.append("</ul>");
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private WebResponsePacket plain(int code, String text) {
|
|
|
|
|
return new WebResponsePacket(code, "text/plain", new HashMap<>(), Html.utf8(text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String safeMsg(Exception e) {
|
|
|
|
|
String m = e.getMessage();
|
|
|
|
|
if (m == null || m.isBlank()) return e.getClass().getSimpleName();
|
|
|
|
|
return m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private record ActionResult(String msg, String err) {
|
|
|
|
|
static ActionResult ok(String msg) {
|
|
|
|
|
return new ActionResult(msg, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static ActionResult err(String err) {
|
|
|
|
|
return new ActionResult(null, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|