Files
INSServer/frontend/dashboard.java

389 lines
17 KiB
Java
Raw Normal View History

2026-02-11 23:22:20 +01:00
package ins.frontend;
import ins.frontend.utils.RegistrarDao;
import ins.frontend.utils.WebApp;
2026-02-22 17:21:23 +01:00
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
2026-02-11 23:22:20 +01:00
import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage;
import org.openautonomousconnection.webserver.api.WebPageContext;
2026-02-22 17:21:23 +01:00
import org.openautonomousconnection.webserver.utils.HeaderMaps;
2026-02-11 23:22:20 +01:00
import org.openautonomousconnection.webserver.utils.Html;
import org.openautonomousconnection.webserver.utils.MergedRequestParams;
2026-02-22 17:21:23 +01:00
import org.openautonomousconnection.webserver.utils.WebUrlUtil;
2026-02-11 23:22:20 +01:00
2026-02-22 17:21:23 +01:00
import java.nio.charset.StandardCharsets;
2026-02-11 23:22:20 +01:00
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
2026-02-22 17:21:23 +01:00
* INS registrar dashboard (TLN / InfoName / Records) for protocol v1.0.1 resource packets.
2026-02-11 23:22:20 +01:00
*
* <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>
*/
2026-02-22 17:21:23 +01:00
@Route(path = "/dashboard.html")
2026-02-11 23:22:20 +01:00
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
2026-02-22 17:21:23 +01:00
public WebResourceResponsePacket handle(WebPageContext ctx) throws Exception {
2026-02-11 23:22:20 +01:00
WebApp.init();
2026-02-22 17:21:23 +01:00
if (ctx.session == null || !ctx.session.isValid() || ctx.session.getUser() == null) {
return plain(ctx, 401, "Authentication required (session).");
2026-02-11 23:22:20 +01:00
}
int userId;
try {
2026-02-22 17:21:23 +01:00
userId = Integer.parseInt(ctx.session.getUser());
2026-02-11 23:22:20 +01:00
} catch (Exception e) {
2026-02-22 17:21:23 +01:00
return plain(ctx, 401, "Invalid session user.");
2026-02-11 23:22:20 +01:00
}
RegistrarDao dao = WebApp.get().dao();
2026-02-22 17:21:23 +01:00
// Build target for param merging: "/path?query"
String rawTarget = WebUrlUtil.extractPathAndQuery(ctx.request.getUrl());
if (rawTarget == null) rawTarget = "/dashboard.html";
2026-02-11 23:22:20 +01:00
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);
}
}
2026-02-22 17:21:23 +01:00
return render(ctx, userId, msg, err, dao);
2026-02-11 23:22:20 +01:00
}
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.");
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);
}
2026-02-22 17:21:23 +01:00
private WebResourceResponsePacket render(WebPageContext ctx, int userId, String msg, String err, RegistrarDao dao) throws Exception {
2026-02-11 23:22:20 +01:00
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
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<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>
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<h3>TLNs (public + owned)</h3>
%s
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<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>
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<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>
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<h3>Your InfoNames</h3>
%s
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<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);
2026-02-22 17:21:23 +01:00
return html(ctx, 200, html);
2026-02-11 23:22:20 +01:00
}
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>
2026-02-22 17:21:23 +01:00
2026-02-11 23:22:20 +01:00
<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();
}
2026-02-22 17:21:23 +01:00
private WebResourceResponsePacket plain(WebPageContext ctx, int code, String text) {
byte[] body = (text == null ? "" : text).getBytes(StandardCharsets.UTF_8);
Map<String, String> headers = HeaderMaps.mutable();
headers.put("content-length", String.valueOf(body.length));
return new WebResourceResponsePacket(outHeader(ctx), code, "text/plain; charset=utf-8", headers, body, null);
}
private WebResourceResponsePacket html(WebPageContext ctx, int code, String html) {
byte[] body = Html.utf8(html);
Map<String, String> headers = HeaderMaps.mutable();
headers.put("content-length", String.valueOf(body.length));
return new WebResourceResponsePacket(outHeader(ctx), code, "text/html; charset=utf-8", headers, body, null);
}
private WebPacketHeader outHeader(WebPageContext ctx) {
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
if (in == null) {
return new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis());
}
return new WebPacketHeader(
in.getRequestId(),
in.getTabId(),
in.getPageId(),
in.getFrameId(),
in.getFlags() | WebPacketFlags.RESOURCE,
System.currentTimeMillis()
);
2026-02-11 23:22:20 +01:00
}
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);
}
}
}