Finished
This commit is contained in:
370
frontend/dashboard.java
Normal file
370
frontend/dashboard.java
Normal file
@@ -0,0 +1,370 @@
|
||||
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>
|
||||
<label><span class="muted">port (SRV)</span><input type="number" text="1028" name="port"></label>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
frontend/index.java
Normal file
36
frontend/index.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package ins.frontend;
|
||||
|
||||
import ins.frontend.utils.WebApp;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
||||
import org.openautonomousconnection.webserver.api.Route;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||
import org.openautonomousconnection.webserver.utils.Html;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Landing page for the registrar ins.frontend.
|
||||
*/
|
||||
@Route(path = "index.html")
|
||||
public final class index implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext ctx) {
|
||||
WebApp.init();
|
||||
String html = Html.page("OAC INS Registrar", """
|
||||
<div class="card">
|
||||
<h2>OAC INS Registrar</h2>
|
||||
<p class="muted">What you want to do?</p>
|
||||
<div class="col"><a href="info.html">Info</a></div><br />
|
||||
<div class="row">
|
||||
<div class="col"><a href="login.html">Login</a></div>
|
||||
<div class="col"><a href="register.html">Register</a></div>
|
||||
<div class="col"><a href="dashboard.html">Dashboard</a></div>
|
||||
</div>
|
||||
</div>
|
||||
""");
|
||||
|
||||
return new WebResponsePacket(200, "text/html", new HashMap<>(), Html.utf8(html));
|
||||
}
|
||||
}
|
||||
49
frontend/info.java
Normal file
49
frontend/info.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package ins.frontend;
|
||||
|
||||
import ins.frontend.utils.WebApp;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
||||
import org.openautonomousconnection.webserver.api.Route;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||
import org.openautonomousconnection.webserver.utils.Html;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Landing page for the registrar ins.frontend.
|
||||
*/
|
||||
@Route(path = "info.html")
|
||||
public final class info implements WebPage {
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext ctx) {
|
||||
WebApp.init();
|
||||
String html = Html.page("INS Info", """
|
||||
<section class="card">
|
||||
<h2>OAC Default INS Server</h2>
|
||||
|
||||
<p>
|
||||
The <strong>Default INS Server</strong> is the official, project-operated
|
||||
name service of the Open Autonomous Connection (OAC) network.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It provides a trusted reference point for resolving InfoNames
|
||||
and enables initial client connections
|
||||
to the OAC ecosystem.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This server is maintained by the OAC project and is intended
|
||||
as the recommended entry point for public services and new clients.
|
||||
</p>
|
||||
|
||||
<p class="muted">
|
||||
Note: Alternative or private INS servers may exist, but the default INS
|
||||
server represents the official and stable reference instance.
|
||||
</p>
|
||||
</section> """);
|
||||
|
||||
return new WebResponsePacket(200, "text/html", new HashMap<>(), Html.utf8(html));
|
||||
}
|
||||
}
|
||||
231
frontend/login.java
Normal file
231
frontend/login.java
Normal file
@@ -0,0 +1,231 @@
|
||||
package ins.frontend;
|
||||
|
||||
import ins.frontend.utils.UserDao;
|
||||
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.protocol.side.web.managers.SessionManager;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
|
||||
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.HeaderMaps;
|
||||
import org.openautonomousconnection.webserver.utils.Html;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Login page with existing-session short-circuit.
|
||||
*
|
||||
* <p>Username stored in DB as plain text.</p>
|
||||
*/
|
||||
@Route(path = "/login.html")
|
||||
public final class login implements WebPage {
|
||||
|
||||
private static WebResponsePacket ok(String html) {
|
||||
return new WebResponsePacket(200, "text/html; charset=utf-8", HeaderMaps.mutable(), Html.utf8(html));
|
||||
}
|
||||
|
||||
private static WebResponsePacket text(int code, String msg) {
|
||||
return new WebResponsePacket(code, "text/plain; charset=utf-8", HeaderMaps.mutable(), msg.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static WebResponsePacket redirect302(String location, String session) {
|
||||
Map<String, String> headers = HeaderMaps.mutable();
|
||||
headers.put("location", location);
|
||||
if (session != null && !session.isBlank()) {
|
||||
headers.put("Location", "dashboard.html");
|
||||
headers.put("Set-Cookie", "session=" + session + "; Path=/; HttpOnly; SameSite=Lax");
|
||||
headers.put("session", session);
|
||||
headers.put("cookie", session);
|
||||
}
|
||||
return new WebResponsePacket(302, "text/plain; charset=utf-8", headers, new byte[0]);
|
||||
}
|
||||
|
||||
private static String renderForm(String errOrOk) {
|
||||
String line = "";
|
||||
if (errOrOk != null && !errOrOk.isBlank()) {
|
||||
boolean ok = errOrOk.startsWith("OK:");
|
||||
line = "<p class='" + (ok ? "ok" : "err") + "'>" + errOrOk + "</p>";
|
||||
}
|
||||
|
||||
String body = """
|
||||
<div class="card">
|
||||
<h2>Login</h2>
|
||||
%s
|
||||
<form method="post" action="login.html" class="form">
|
||||
<label><span class="muted">Username</span><input type="text" name="username" autocomplete="username" required></label>
|
||||
<label><span class="muted">Password</span><input type="password" name="password" autocomplete="current-password" required></label>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<div class="col"><a href="register.html">Register</a></div>
|
||||
<div class="col"><a href="index.html">Home</a></div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(line);
|
||||
|
||||
return Html.page("Login", body);
|
||||
}
|
||||
|
||||
private static String resolveIp(WebPageContext ctx) {
|
||||
if (ctx.client == null || ctx.client.getConnection() == null) return "";
|
||||
if (ctx.client.getConnection().getTcpSocket() == null) return "";
|
||||
if (ctx.client.getConnection().getTcpSocket().getInetAddress() == null) return "";
|
||||
return ctx.client.getConnection().getTcpSocket().getInetAddress().getHostAddress();
|
||||
}
|
||||
|
||||
private static String headerIgnoreCase(Map<String, String> headers, String key) {
|
||||
if (headers == null || headers.isEmpty() || key == null) return null;
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseFormUrlEncoded(byte[] body) {
|
||||
if (body == null || body.length == 0) return Map.of();
|
||||
|
||||
String raw = new String(body, StandardCharsets.UTF_8);
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
|
||||
int start = 0;
|
||||
while (start <= raw.length()) {
|
||||
int amp = raw.indexOf('&', start);
|
||||
if (amp < 0) amp = raw.length();
|
||||
|
||||
String pair = raw.substring(start, amp);
|
||||
if (!pair.isEmpty()) {
|
||||
int eq = pair.indexOf('=');
|
||||
String k = (eq < 0) ? pair : pair.substring(0, eq);
|
||||
String v = (eq < 0) ? "" : pair.substring(eq + 1);
|
||||
|
||||
String key = decodeFormToken(k);
|
||||
String val = decodeFormToken(v);
|
||||
|
||||
if (!key.isEmpty()) out.computeIfAbsent(key, __ -> new ArrayList<>(1)).add(val);
|
||||
}
|
||||
|
||||
start = amp + 1;
|
||||
if (amp == raw.length()) break;
|
||||
}
|
||||
|
||||
Map<String, List<String>> frozen = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> e : out.entrySet()) frozen.put(e.getKey(), List.copyOf(e.getValue()));
|
||||
return Map.copyOf(frozen);
|
||||
}
|
||||
|
||||
private static String first(Map<String, List<String>> params, String key) {
|
||||
if (params == null || key == null) return null;
|
||||
List<String> v = params.get(key);
|
||||
if (v == null || v.isEmpty()) return null;
|
||||
String t = v.getFirst();
|
||||
if (t == null) return null;
|
||||
String s = t.trim();
|
||||
return s.isEmpty() ? null : s;
|
||||
}
|
||||
|
||||
private static String decodeFormToken(String s) {
|
||||
if (s == null || s.isEmpty()) return "";
|
||||
byte[] tmp = new byte[s.length()];
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (c == '+') {
|
||||
tmp[n++] = (byte) ' ';
|
||||
continue;
|
||||
}
|
||||
if (c == '%' && i + 2 < s.length()) {
|
||||
int hi = hex(s.charAt(i + 1));
|
||||
int lo = hex(s.charAt(i + 2));
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
tmp[n++] = (byte) ((hi << 4) | lo);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] b = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
|
||||
for (byte bb : b) tmp[n++] = bb;
|
||||
}
|
||||
|
||||
return new String(tmp, 0, n, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static int hex(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
||||
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext ctx) throws Exception {
|
||||
WebApp.init();
|
||||
|
||||
// 1) If a valid session already exists -> go dashboard (keep session)
|
||||
SessionContext existing = SessionContext.from(
|
||||
ctx.client,
|
||||
(ProtocolWebServer) ctx.client.getServer(),
|
||||
ctx.request.getHeaders()
|
||||
);
|
||||
if (existing.isValid() && existing.getUser() != null) {
|
||||
return redirect302("dashboard.html", existing.getSessionId());
|
||||
}
|
||||
|
||||
WebRequestMethod method = ctx.request.getMethod();
|
||||
if (method == null) method = WebRequestMethod.GET;
|
||||
|
||||
if (method == WebRequestMethod.GET) {
|
||||
return ok(renderForm(null));
|
||||
}
|
||||
|
||||
if (method != WebRequestMethod.POST) {
|
||||
return text(405, "Method Not Allowed");
|
||||
}
|
||||
|
||||
String contentType = headerIgnoreCase(ctx.request.getHeaders(), "content-type");
|
||||
String ctLower = (contentType == null) ? "" : contentType.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (!ctLower.startsWith("application/x-www-form-urlencoded")) {
|
||||
return ok(renderForm("Unsupported content-type: " + Html.esc(contentType)));
|
||||
}
|
||||
|
||||
Map<String, List<String>> form = parseFormUrlEncoded(ctx.request.getBody());
|
||||
String username = first(form, "username");
|
||||
String password = first(form, "password");
|
||||
|
||||
if (username == null || password == null) {
|
||||
return ok(renderForm("Missing username/password."));
|
||||
}
|
||||
|
||||
String lookupUsername = username.trim();
|
||||
|
||||
UserDao.UserRow user = WebApp.get().users().findByUsername(lookupUsername).orElse(null);
|
||||
if (user == null) {
|
||||
return ok(renderForm("Invalid credentials."));
|
||||
}
|
||||
|
||||
boolean okPw = WebApp.get().passwordHasher().verify(password, user.passwordEncoded());
|
||||
if (!okPw) {
|
||||
return ok(renderForm("Invalid credentials."));
|
||||
}
|
||||
|
||||
String ip = resolveIp(ctx);
|
||||
String ua = headerIgnoreCase(ctx.request.getHeaders(), "user-agent");
|
||||
if (ua == null) ua = "";
|
||||
|
||||
String session = SessionManager.create(
|
||||
String.valueOf(user.id()),
|
||||
ip,
|
||||
ua,
|
||||
(ProtocolWebServer) ctx.client.getServer()
|
||||
);
|
||||
|
||||
return redirect302("dashboard.html", session);
|
||||
}
|
||||
}
|
||||
221
frontend/register.java
Normal file
221
frontend/register.java
Normal file
@@ -0,0 +1,221 @@
|
||||
package ins.frontend;
|
||||
|
||||
import ins.frontend.utils.RegistrationService;
|
||||
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.protocol.side.web.managers.SessionManager;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
|
||||
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.HeaderMaps;
|
||||
import org.openautonomousconnection.webserver.utils.Html;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Register page with existing-session short-circuit.
|
||||
*/
|
||||
@Route(path = "/register.html")
|
||||
public final class register implements WebPage {
|
||||
|
||||
private static WebResponsePacket ok(String html) {
|
||||
return new WebResponsePacket(200, "text/html; charset=utf-8", HeaderMaps.mutable(), Html.utf8(html));
|
||||
}
|
||||
|
||||
private static WebResponsePacket text(int code, String msg) {
|
||||
return new WebResponsePacket(code, "text/plain; charset=utf-8", HeaderMaps.mutable(), msg.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static WebResponsePacket redirect302(String location, String session) {
|
||||
Map<String, String> headers = HeaderMaps.mutable();
|
||||
headers.put("location", location);
|
||||
if (session != null && !session.isBlank()) {
|
||||
headers.put("Location", "dashboard.html");
|
||||
headers.put("Set-Cookie", "session=" + session + "; Path=/; HttpOnly; SameSite=Lax");
|
||||
headers.put("session", session);
|
||||
headers.put("cookie", session);
|
||||
}
|
||||
return new WebResponsePacket(302, "text/plain; charset=utf-8", headers, new byte[0]);
|
||||
}
|
||||
|
||||
private static String renderForm(String errOrOk) {
|
||||
String line = "";
|
||||
if (errOrOk != null && !errOrOk.isBlank()) {
|
||||
boolean ok = errOrOk.startsWith("OK:");
|
||||
line = "<p class='" + (ok ? "ok" : "err") + "'>" + errOrOk + "</p>";
|
||||
}
|
||||
|
||||
String body = """
|
||||
<div class="card">
|
||||
<h2>Register</h2>
|
||||
%s
|
||||
<form method="post" action="register.html" class="form">
|
||||
<label><span class="muted">Username</span><input type="text" name="username" autocomplete="username" required></label>
|
||||
<label><span class="muted">Password</span><input type="password" name="password" autocomplete="new-password" required></label>
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<div class="col"><a href="login.html">Login</a></div>
|
||||
<div class="col"><a href="index.html">Home</a></div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(line);
|
||||
|
||||
return Html.page("Register", body);
|
||||
}
|
||||
|
||||
private static String resolveIp(WebPageContext ctx) {
|
||||
if (ctx.client == null || ctx.client.getConnection() == null) return "";
|
||||
if (ctx.client.getConnection().getTcpSocket() == null) return "";
|
||||
if (ctx.client.getConnection().getTcpSocket().getInetAddress() == null) return "";
|
||||
return ctx.client.getConnection().getTcpSocket().getInetAddress().getHostAddress();
|
||||
}
|
||||
|
||||
private static String headerIgnoreCase(Map<String, String> headers, String key) {
|
||||
if (headers == null || headers.isEmpty() || key == null) return null;
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseFormUrlEncoded(byte[] body) {
|
||||
if (body == null || body.length == 0) return Map.of();
|
||||
|
||||
String raw = new String(body, StandardCharsets.UTF_8);
|
||||
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||
|
||||
int start = 0;
|
||||
while (start <= raw.length()) {
|
||||
int amp = raw.indexOf('&', start);
|
||||
if (amp < 0) amp = raw.length();
|
||||
|
||||
String pair = raw.substring(start, amp);
|
||||
if (!pair.isEmpty()) {
|
||||
int eq = pair.indexOf('=');
|
||||
String k = (eq < 0) ? pair : pair.substring(0, eq);
|
||||
String v = (eq < 0) ? "" : pair.substring(eq + 1);
|
||||
|
||||
String key = decodeFormToken(k);
|
||||
String val = decodeFormToken(v);
|
||||
|
||||
if (!key.isEmpty()) out.computeIfAbsent(key, __ -> new ArrayList<>(1)).add(val);
|
||||
}
|
||||
|
||||
start = amp + 1;
|
||||
if (amp == raw.length()) break;
|
||||
}
|
||||
|
||||
Map<String, List<String>> frozen = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> e : out.entrySet()) frozen.put(e.getKey(), List.copyOf(e.getValue()));
|
||||
return Map.copyOf(frozen);
|
||||
}
|
||||
|
||||
private static String first(Map<String, List<String>> params, String key) {
|
||||
if (params == null || key == null) return null;
|
||||
List<String> v = params.get(key);
|
||||
if (v == null || v.isEmpty()) return null;
|
||||
String t = v.getFirst();
|
||||
if (t == null) return null;
|
||||
String s = t.trim();
|
||||
return s.isEmpty() ? null : s;
|
||||
}
|
||||
|
||||
private static String decodeFormToken(String s) {
|
||||
if (s == null || s.isEmpty()) return "";
|
||||
byte[] tmp = new byte[s.length()];
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (c == '+') {
|
||||
tmp[n++] = (byte) ' ';
|
||||
continue;
|
||||
}
|
||||
if (c == '%' && i + 2 < s.length()) {
|
||||
int hi = hex(s.charAt(i + 1));
|
||||
int lo = hex(s.charAt(i + 2));
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
tmp[n++] = (byte) ((hi << 4) | lo);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] b = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
|
||||
for (byte bb : b) tmp[n++] = bb;
|
||||
}
|
||||
|
||||
return new String(tmp, 0, n, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static int hex(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
||||
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResponsePacket handle(WebPageContext ctx) throws Exception {
|
||||
WebApp.init();
|
||||
|
||||
// 1) If a valid session already exists -> go dashboard (keep session)
|
||||
SessionContext existing = SessionContext.from(
|
||||
ctx.client,
|
||||
(ProtocolWebServer) ctx.client.getServer(),
|
||||
ctx.request.getHeaders()
|
||||
);
|
||||
if (existing.isValid() && existing.getUser() != null) {
|
||||
return redirect302("dashboard.html", existing.getSessionId());
|
||||
}
|
||||
|
||||
WebRequestMethod method = ctx.request.getMethod();
|
||||
if (method == null) method = WebRequestMethod.GET;
|
||||
|
||||
if (method == WebRequestMethod.GET) {
|
||||
return ok(renderForm(null));
|
||||
}
|
||||
|
||||
if (method != WebRequestMethod.POST) {
|
||||
return text(405, "Method Not Allowed");
|
||||
}
|
||||
|
||||
String contentType = headerIgnoreCase(ctx.request.getHeaders(), "content-type");
|
||||
String ctLower = (contentType == null) ? "" : contentType.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (!ctLower.startsWith("application/x-www-form-urlencoded")) {
|
||||
return ok(renderForm("Unsupported content-type: " + Html.esc(contentType)));
|
||||
}
|
||||
|
||||
Map<String, List<String>> form = parseFormUrlEncoded(ctx.request.getBody());
|
||||
String username = first(form, "username");
|
||||
String password = first(form, "password");
|
||||
|
||||
RegistrationService service = new RegistrationService(WebApp.get().users(), WebApp.get().passwordHasher());
|
||||
RegistrationService.Result r = service.register(username, password);
|
||||
|
||||
if (!r.ok()) {
|
||||
return ok(renderForm(r.error()));
|
||||
}
|
||||
|
||||
// 2) Create new session (user just registered)
|
||||
String ip = resolveIp(ctx);
|
||||
String ua = headerIgnoreCase(ctx.request.getHeaders(), "user-agent");
|
||||
if (ua == null) ua = "";
|
||||
|
||||
String session = SessionManager.create(
|
||||
String.valueOf(r.userId()),
|
||||
ip,
|
||||
ua,
|
||||
(ProtocolWebServer) ctx.client.getServer()
|
||||
);
|
||||
|
||||
return redirect302("dashboard.html", session);
|
||||
}
|
||||
}
|
||||
490
frontend/utils/RegistrarDao.java
Normal file
490
frontend/utils/RegistrarDao.java
Normal file
@@ -0,0 +1,490 @@
|
||||
package ins.frontend.utils;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Registrar DAO for schema:
|
||||
* <ul>
|
||||
* <li>tln(id, name, info, owner_id, is_public, allow_subdomains)</li>
|
||||
* <li>infonames(id, info, tln_id, uid)</li>
|
||||
* <li>subnames(id, name, infoname_id)</li>
|
||||
* <li>records(id, infoname_id, subname_id, type, value, ttl, priority, port, weight)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Rules:</p>
|
||||
* <ul>
|
||||
* <li>Create InfoName under TLN allowed if TLN is public OR owned by user.</li>
|
||||
* <li>Subnames allowed for non-owner only if allow_subdomains=1. Owner always allowed.</li>
|
||||
* <li>TLN name is not editable; only info/is_public/allow_subdomains are editable (owner-only).</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class RegistrarDao {
|
||||
|
||||
private final UserDao.DataSourceProvider dataSource;
|
||||
|
||||
/**
|
||||
* Creates the DAO.
|
||||
*
|
||||
* @param dataSource JDBC connection provider
|
||||
*/
|
||||
public RegistrarDao(UserDao.DataSourceProvider dataSource) {
|
||||
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
|
||||
}
|
||||
|
||||
// ---------- TLN ----------
|
||||
|
||||
/**
|
||||
* TLN usable for creating InfoNames if public OR owned.
|
||||
*
|
||||
* @param tln TLN row
|
||||
* @param userId current user id
|
||||
* @return allowed
|
||||
*/
|
||||
public static boolean canUseTln(TlnRow tln, int userId) {
|
||||
if (tln == null || userId <= 0) return false;
|
||||
if (tln.isPublic) return true;
|
||||
return tln.ownerId != null && tln.ownerId == userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subdomain/subname allowed if:
|
||||
* <ul>
|
||||
* <li>no sub requested (root) OR</li>
|
||||
* <li>TLN allows subdomains OR</li>
|
||||
* <li>user is TLN owner</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param tln TLN row
|
||||
* @param userId current user id
|
||||
* @param sub sub label (nullable)
|
||||
* @return allowed
|
||||
*/
|
||||
public static boolean canUseSubname(TlnRow tln, int userId, String sub) {
|
||||
if (tln == null || userId <= 0) return false;
|
||||
boolean wantsSub = sub != null && !sub.isBlank();
|
||||
if (!wantsSub) return true;
|
||||
if (tln.allowSubdomains) return true;
|
||||
return tln.ownerId != null && tln.ownerId == userId;
|
||||
}
|
||||
|
||||
private static TlnRow mapTln(ResultSet rs) throws SQLException {
|
||||
return new TlnRow(
|
||||
rs.getInt("id"),
|
||||
rs.getString("name"),
|
||||
rs.getString("info"),
|
||||
(Integer) rs.getObject("owner_id"),
|
||||
rs.getInt("is_public") == 1,
|
||||
rs.getInt("allow_subdomains") == 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TLN.
|
||||
*
|
||||
* @param name tln.name (unique)
|
||||
* @param info tln.info (editable, can be null/blank)
|
||||
* @param ownerId owner user id (nullable in DB, pass null to create unowned TLN)
|
||||
* @param isPublic tln.is_public
|
||||
* @param allowSubdomains tln.allow_subdomains
|
||||
* @return new tln.id
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public int createTln(String name, String info, Integer ownerId, boolean isPublic, boolean allowSubdomains) throws SQLException {
|
||||
if (name == null || name.isBlank()) throw new IllegalArgumentException("name must not be blank");
|
||||
|
||||
String sql = "INSERT INTO tln (name, info, owner_id, is_public, allow_subdomains) VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
ps.setString(1, name.trim());
|
||||
ps.setString(2, info);
|
||||
if (ownerId == null) ps.setNull(3, Types.INTEGER);
|
||||
else ps.setInt(3, ownerId);
|
||||
ps.setInt(4, isPublic ? 1 : 0);
|
||||
ps.setInt(5, allowSubdomains ? 1 : 0);
|
||||
|
||||
ps.executeUpdate();
|
||||
|
||||
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||
if (rs.next()) return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new SQLException("No generated key returned for tln.id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds TLN by name.
|
||||
*
|
||||
* @param name tln.name
|
||||
* @return optional row
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public Optional<TlnRow> findTlnByName(String name) throws SQLException {
|
||||
if (name == null || name.isBlank()) return Optional.empty();
|
||||
|
||||
String sql = "SELECT id, name, info, owner_id, is_public, allow_subdomains FROM tln WHERE name = ? LIMIT 1";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setString(1, name.trim());
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return Optional.empty();
|
||||
return Optional.of(mapTln(rs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Permissions ----------
|
||||
|
||||
/**
|
||||
* Lists TLNs visible to a user: owned OR public OR unowned+public.
|
||||
*
|
||||
* @param userId current user id
|
||||
* @return rows
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public TlnRow[] listVisibleTlns(int userId) throws SQLException {
|
||||
String sql = """
|
||||
SELECT id, name, info, owner_id, is_public, allow_subdomains
|
||||
FROM tln
|
||||
WHERE is_public = 1 OR owner_id = ?
|
||||
ORDER BY name ASC
|
||||
""";
|
||||
|
||||
List<TlnRow> out = new ArrayList<>();
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setInt(1, userId);
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) out.add(mapTln(rs));
|
||||
}
|
||||
}
|
||||
|
||||
return out.toArray(new TlnRow[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates TLN fields except name (owner-only).
|
||||
*
|
||||
* @param tlnId tln.id
|
||||
* @param ownerUserId users.id (must match tln.owner_id)
|
||||
* @param newInfo new info text (nullable)
|
||||
* @param isPublic is_public
|
||||
* @param allowSubdomains allow_subdomains
|
||||
* @return true if updated, false if not owned/not found
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public boolean updateTlnOwned(int tlnId, int ownerUserId, String newInfo, boolean isPublic, boolean allowSubdomains) throws SQLException {
|
||||
if (tlnId <= 0) throw new IllegalArgumentException("tlnId must be > 0");
|
||||
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
|
||||
|
||||
String sql = "UPDATE tln SET info = ?, is_public = ?, allow_subdomains = ? WHERE id = ? AND owner_id = ?";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setString(1, newInfo);
|
||||
ps.setInt(2, isPublic ? 1 : 0);
|
||||
ps.setInt(3, allowSubdomains ? 1 : 0);
|
||||
ps.setInt(4, tlnId);
|
||||
ps.setInt(5, ownerUserId);
|
||||
|
||||
return ps.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- InfoNames ----------
|
||||
|
||||
/**
|
||||
* Deletes TLN (owner-only).
|
||||
*
|
||||
* @param tlnId tln.id
|
||||
* @param ownerUserId users.id
|
||||
* @return true if deleted
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public boolean deleteTlnOwned(int tlnId, int ownerUserId) throws SQLException {
|
||||
if (tlnId <= 0) throw new IllegalArgumentException("tlnId must be > 0");
|
||||
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
|
||||
|
||||
String sql = "DELETE FROM tln WHERE id = ? AND owner_id = ?";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setInt(1, tlnId);
|
||||
ps.setInt(2, ownerUserId);
|
||||
|
||||
return ps.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an InfoName under the given TLN.
|
||||
*
|
||||
* @param tln TLN row
|
||||
* @param info infonames.info
|
||||
* @param userId infonames.uid (users.id)
|
||||
* @return new infonames.id
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public int createInfoName(TlnRow tln, String info, int userId) throws SQLException {
|
||||
if (tln == null) throw new IllegalArgumentException("tln must not be null");
|
||||
if (userId <= 0) throw new IllegalArgumentException("userId must be > 0");
|
||||
if (!canUseTln(tln, userId)) throw new SQLException("TLN not public and not owned by user.");
|
||||
if (info == null || info.isBlank()) throw new IllegalArgumentException("info must not be blank");
|
||||
|
||||
String sql = "INSERT INTO infonames (info, tln_id, uid) VALUES (?, ?, ?)";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
ps.setString(1, info.trim());
|
||||
ps.setInt(2, tln.id);
|
||||
ps.setInt(3, userId);
|
||||
|
||||
ps.executeUpdate();
|
||||
|
||||
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||
if (rs.next()) return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new SQLException("No generated key returned for infonames.id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user owns the infoname.
|
||||
*
|
||||
* @param infonameId infonames.id
|
||||
* @param userId users.id
|
||||
* @return true if owned
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public boolean isOwnerOfInfoName(int infonameId, int userId) throws SQLException {
|
||||
if (infonameId <= 0 || userId <= 0) return false;
|
||||
|
||||
String sql = "SELECT 1 FROM infonames WHERE id = ? AND uid = ? LIMIT 1";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setInt(1, infonameId);
|
||||
ps.setInt(2, userId);
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an InfoName (owner-only).
|
||||
*
|
||||
* @param infonameId infonames.id
|
||||
* @param ownerUserId users.id
|
||||
* @return true if deleted
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public boolean deleteInfoNameOwned(int infonameId, int ownerUserId) throws SQLException {
|
||||
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
|
||||
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
|
||||
|
||||
String sql = "DELETE FROM infonames WHERE id = ? AND uid = ?";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setInt(1, infonameId);
|
||||
ps.setInt(2, ownerUserId);
|
||||
|
||||
return ps.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Subnames + Records ----------
|
||||
|
||||
/**
|
||||
* Lists InfoNames owned by user (joined with TLN data).
|
||||
*
|
||||
* @param userId users.id
|
||||
* @return rows
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public InfoNameRow[] listOwnedInfoNames(int userId) throws SQLException {
|
||||
if (userId <= 0) return new InfoNameRow[0];
|
||||
|
||||
String sql = """
|
||||
SELECT i.id AS iid, i.info AS info, i.tln_id AS tln_id,
|
||||
t.name AS tln_name, t.info AS tln_info, t.owner_id AS owner_id,
|
||||
t.is_public AS is_public, t.allow_subdomains AS allow_subdomains
|
||||
FROM infonames i
|
||||
INNER JOIN tln t ON t.id = i.tln_id
|
||||
WHERE i.uid = ?
|
||||
ORDER BY t.name ASC, i.info ASC
|
||||
""";
|
||||
|
||||
List<InfoNameRow> out = new ArrayList<>();
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setInt(1, userId);
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
TlnRow tln = new TlnRow(
|
||||
rs.getInt("tln_id"),
|
||||
rs.getString("tln_name"),
|
||||
rs.getString("tln_info"),
|
||||
(Integer) rs.getObject("owner_id"),
|
||||
rs.getInt("is_public") == 1,
|
||||
rs.getInt("allow_subdomains") == 1
|
||||
);
|
||||
out.add(new InfoNameRow(
|
||||
rs.getInt("iid"),
|
||||
rs.getString("info"),
|
||||
tln
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.toArray(new InfoNameRow[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a subname exists for an infoname; returns subnames.id.
|
||||
*
|
||||
* <p>If sub is null/blank, returns null (root).</p>
|
||||
*
|
||||
* @param infonameId infonames.id
|
||||
* @param sub sub label (nullable)
|
||||
* @return subnames.id or null for root
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public Integer ensureSubname(int infonameId, String sub) throws SQLException {
|
||||
if (sub == null || sub.isBlank()) return null;
|
||||
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
|
||||
|
||||
String name = sub.trim();
|
||||
|
||||
// 1) find
|
||||
String find = "SELECT id FROM subnames WHERE infoname_id = ? AND name = ? LIMIT 1";
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(find)) {
|
||||
|
||||
ps.setInt(1, infonameId);
|
||||
ps.setString(2, name);
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (rs.next()) return rs.getInt("id");
|
||||
}
|
||||
}
|
||||
|
||||
// 2) insert
|
||||
String ins = "INSERT INTO subnames (name, infoname_id) VALUES (?, ?)";
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(ins, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
ps.setString(1, name);
|
||||
ps.setInt(2, infonameId);
|
||||
ps.executeUpdate();
|
||||
|
||||
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||
if (rs.next()) return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new SQLException("No generated key returned for subnames.id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DNS-like record.
|
||||
*
|
||||
* @param infonameId infonames.id
|
||||
* @param subnameId subnames.id or null for root
|
||||
* @param type enum type (A/AAAA/TXT/CNAME/MX/SRV/NS)
|
||||
* @param value record value
|
||||
* @param ttl ttl (default 3600)
|
||||
* @param priority priority (MX/SRV)
|
||||
* @param port port (SRV)
|
||||
* @param weight weight (SRV)
|
||||
* @return new records.id
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public int addRecord(int infonameId,
|
||||
Integer subnameId,
|
||||
String type,
|
||||
String value,
|
||||
int ttl,
|
||||
Integer priority,
|
||||
Integer port,
|
||||
Integer weight) throws SQLException {
|
||||
|
||||
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
|
||||
if (type == null || type.isBlank()) throw new IllegalArgumentException("type must not be blank");
|
||||
if (value == null || value.isBlank()) throw new IllegalArgumentException("value must not be blank");
|
||||
if (ttl <= 0) ttl = 3600;
|
||||
|
||||
String t = type.trim().toUpperCase();
|
||||
|
||||
String sql = """
|
||||
INSERT INTO records (infoname_id, subname_id, type, value, ttl, priority, port, weight)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
ps.setInt(1, infonameId);
|
||||
|
||||
if (subnameId == null) ps.setNull(2, Types.INTEGER);
|
||||
else ps.setInt(2, subnameId);
|
||||
|
||||
ps.setString(3, t);
|
||||
ps.setString(4, value.trim());
|
||||
ps.setInt(5, ttl);
|
||||
|
||||
if (priority == null) ps.setNull(6, Types.INTEGER);
|
||||
else ps.setInt(6, priority);
|
||||
|
||||
if (port == null) ps.setNull(7, Types.INTEGER);
|
||||
else ps.setInt(7, port);
|
||||
|
||||
if (weight == null) ps.setNull(8, Types.INTEGER);
|
||||
else ps.setInt(8, weight);
|
||||
|
||||
ps.executeUpdate();
|
||||
|
||||
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||
if (rs.next()) return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new SQLException("No generated key returned for records.id");
|
||||
}
|
||||
|
||||
/**
|
||||
* TLN row.
|
||||
*/
|
||||
public record TlnRow(int id, String name, String info, Integer ownerId, boolean isPublic, boolean allowSubdomains) {
|
||||
}
|
||||
|
||||
/**
|
||||
* InfoName row (includes TLN).
|
||||
*/
|
||||
public record InfoNameRow(int id, String info, TlnRow tln) {
|
||||
}
|
||||
}
|
||||
122
frontend/utils/RegistrationService.java
Normal file
122
frontend/utils/RegistrationService.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package ins.frontend.utils;
|
||||
|
||||
import org.openautonomousconnection.webserver.utils.PasswordHasher;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Registration service for creating users with secure password hashing and basic validation.
|
||||
*/
|
||||
public final class RegistrationService {
|
||||
|
||||
private static final int USERNAME_MIN = 5;
|
||||
private static final int USERNAME_MAX = 256;
|
||||
private static final int PASSWORD_MIN = 6;
|
||||
private static final int PASSWORD_MAX = 256;
|
||||
|
||||
private final UserDao userDao;
|
||||
private final PasswordHasher hasher;
|
||||
|
||||
/**
|
||||
* Creates a registration service.
|
||||
*
|
||||
* @param userDao user DAO
|
||||
* @param hasher password hasher
|
||||
*/
|
||||
public RegistrationService(UserDao userDao, PasswordHasher hasher) {
|
||||
this.userDao = Objects.requireNonNull(userDao, "userDao");
|
||||
this.hasher = Objects.requireNonNull(hasher, "hasher");
|
||||
}
|
||||
|
||||
private static String normalizeUsername(String u) {
|
||||
if (u == null) return null;
|
||||
String t = u.trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
|
||||
private static String validate(String username, String password) {
|
||||
if (username == null) return "Missing username.";
|
||||
if (password == null) return "Missing password.";
|
||||
|
||||
if (username.length() < USERNAME_MIN) return "Username too short (min " + USERNAME_MIN + ").";
|
||||
if (username.length() > USERNAME_MAX) return "Username too long (max " + USERNAME_MAX + ").";
|
||||
|
||||
// Allow only a safe subset to avoid weird edge cases in UI and future.
|
||||
for (int i = 0; i < username.length(); i++) {
|
||||
char c = username.charAt(i);
|
||||
boolean ok = (c >= 'a' && c <= 'z')
|
||||
|| (c >= 'A' && c <= 'Z')
|
||||
|| (c >= '0' && c <= '9')
|
||||
|| c == '_' || c == '-' || c == '.';
|
||||
if (!ok) return "Username contains invalid characters.";
|
||||
}
|
||||
|
||||
if (password.length() < PASSWORD_MIN) return "Password too short (min " + PASSWORD_MIN + ").";
|
||||
if (password.length() > PASSWORD_MAX) return "Password too long.";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user.
|
||||
*
|
||||
* @param usernameRaw raw username (from form)
|
||||
* @param passwordRaw raw password (from form)
|
||||
* @return result containing either userId or an error message
|
||||
*/
|
||||
public Result register(String usernameRaw, String passwordRaw) {
|
||||
String username = normalizeUsername(usernameRaw);
|
||||
String password = (passwordRaw == null) ? "" : passwordRaw;
|
||||
|
||||
String validationError = validate(username, password);
|
||||
if (validationError != null) {
|
||||
return Result.error(validationError);
|
||||
}
|
||||
|
||||
try {
|
||||
// Choose: store username as-is OR store sha256(username).
|
||||
// Your schema says: username(sha256 hex or plain) -> keep it plain for now.
|
||||
String usernameStored = username;
|
||||
|
||||
if (userDao.findByUsername(usernameStored).isPresent()) {
|
||||
return Result.error("Username already exists.");
|
||||
}
|
||||
|
||||
String passwordEncoded = hasher.hash(password);
|
||||
int userId = userDao.createUserWithNewUuid(usernameStored, passwordEncoded);
|
||||
|
||||
return Result.ok(userId);
|
||||
} catch (Exception e) {
|
||||
return Result.error("Registration failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration result.
|
||||
*
|
||||
* @param ok whether succeeded
|
||||
* @param userId created user id, or -1
|
||||
* @param error error message, or null
|
||||
*/
|
||||
public record Result(boolean ok, int userId, String error) {
|
||||
|
||||
/**
|
||||
* Creates a success result.
|
||||
*
|
||||
* @param userId user id
|
||||
* @return result
|
||||
*/
|
||||
public static Result ok(int userId) {
|
||||
return new Result(true, userId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error result.
|
||||
*
|
||||
* @param error error message
|
||||
* @return result
|
||||
*/
|
||||
public static Result error(String error) {
|
||||
return new Result(false, -1, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
frontend/utils/UserDao.java
Normal file
154
frontend/utils/UserDao.java
Normal file
@@ -0,0 +1,154 @@
|
||||
package ins.frontend.utils;
|
||||
|
||||
import org.openautonomousconnection.webserver.utils.Sha256;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DAO for users table:
|
||||
* users(id, uid(uuid string), username(sha256 hex or plain), password(pbkdf2...))
|
||||
*/
|
||||
public final class UserDao {
|
||||
|
||||
private final DataSourceProvider dataSource;
|
||||
|
||||
/**
|
||||
* Creates a UserDao.
|
||||
*
|
||||
* @param dataSource connection provider
|
||||
*/
|
||||
public UserDao(DataSourceProvider dataSource) {
|
||||
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user by username.
|
||||
*
|
||||
* @param uid exact uid match
|
||||
* @return optional row
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public Optional<UserRow> findByUid(UUID uid) throws SQLException {
|
||||
if (uid == null) return Optional.empty();
|
||||
|
||||
String sql = "SELECT id, uid, username, password FROM users WHERE uid = ? LIMIT 1";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setString(1, uid.toString());
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return Optional.empty();
|
||||
return Optional.of(new UserRow(
|
||||
rs.getInt("id"),
|
||||
rs.getString("uid"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
*
|
||||
* @param uid uuid string (36 chars)
|
||||
* @param username username string (your choice: raw or sha256 hex)
|
||||
* @param passwordEncoded encoded password
|
||||
* @return generated users.id
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public int createUser(String uid, String username, String passwordEncoded) throws SQLException {
|
||||
if (uid == null || uid.isBlank()) throw new IllegalArgumentException("uid must not be blank");
|
||||
if (username == null || username.isBlank()) throw new IllegalArgumentException("username must not be blank");
|
||||
if (passwordEncoded == null || passwordEncoded.isBlank())
|
||||
throw new IllegalArgumentException("passwordEncoded must not be blank");
|
||||
|
||||
String sql = "INSERT INTO users (uid, username, password) VALUES (?, ?, ?)";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
ps.setString(1, uid);
|
||||
ps.setString(2, Sha256.hex(username));
|
||||
ps.setString(3, passwordEncoded);
|
||||
ps.executeUpdate();
|
||||
|
||||
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||
if (rs.next()) return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new SQLException("No generated key returned for users.id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: creates a user with a new random UUID.
|
||||
*
|
||||
* @param username username
|
||||
* @param passwordEncoded encoded password
|
||||
* @return generated users.id
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public int createUserWithNewUuid(String username, String passwordEncoded) throws SQLException {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
while (findByUid(uuid).isPresent()) uuid = UUID.randomUUID();
|
||||
return createUser(uuid.toString(), username, passwordEncoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user by username.
|
||||
*
|
||||
* @param username exact username match
|
||||
* @return optional row
|
||||
* @throws SQLException on SQL errors
|
||||
*/
|
||||
public Optional<UserRow> findByUsername(String username) throws SQLException {
|
||||
if (username == null || username.isBlank()) return Optional.empty();
|
||||
username = Sha256.hex(username);
|
||||
|
||||
String sql = "SELECT id, uid, username, password FROM users WHERE username = ? LIMIT 1";
|
||||
|
||||
try (Connection c = dataSource.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
ps.setString(1, username);
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return Optional.empty();
|
||||
return Optional.of(new UserRow(
|
||||
rs.getInt("id"),
|
||||
rs.getString("uid"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection provider abstraction.
|
||||
*/
|
||||
public interface DataSourceProvider {
|
||||
/**
|
||||
* @return open SQL connection
|
||||
* @throws SQLException on errors
|
||||
*/
|
||||
Connection getConnection() throws SQLException;
|
||||
}
|
||||
|
||||
/**
|
||||
* User row.
|
||||
*
|
||||
* @param id users.id
|
||||
* @param uid users.uid (uuid)
|
||||
* @param username users.username
|
||||
* @param passwordEncoded users.password
|
||||
*/
|
||||
public record UserRow(int id, String uid, String username, String passwordEncoded) {
|
||||
}
|
||||
}
|
||||
113
frontend/utils/WebApp.java
Normal file
113
frontend/utils/WebApp.java
Normal file
@@ -0,0 +1,113 @@
|
||||
package ins.frontend.utils;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
|
||||
import org.openautonomousconnection.webserver.utils.PasswordHasher;
|
||||
import org.openautonomousconnection.webserver.utils.Pbkdf2Sha256Hasher;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Application singleton holding DAO + password hasher.
|
||||
*/
|
||||
public final class WebApp {
|
||||
|
||||
private static volatile WebApp INSTANCE;
|
||||
private static boolean isInit;
|
||||
private static UserDao.DataSourceProvider dsp = null;
|
||||
private final UserDao userDao;
|
||||
private final RegistrarDao registrarDao;
|
||||
private final PasswordHasher passwordHasher;
|
||||
|
||||
private WebApp(UserDao userDao, RegistrarDao registrarDao, PasswordHasher passwordHasher) {
|
||||
this.userDao = Objects.requireNonNull(userDao, "userDao");
|
||||
this.registrarDao = Objects.requireNonNull(registrarDao, "registrarDao");
|
||||
this.passwordHasher = Objects.requireNonNull(passwordHasher, "passwordHasher");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the singleton (call once at server startup).
|
||||
*/
|
||||
public static void init() {
|
||||
if (isInit) return;
|
||||
|
||||
try {
|
||||
if (!new File(new File(".").getParent(), "db.properties").exists()) {
|
||||
new File(new File(".").getParent(), "db.properties").createNewFile();
|
||||
}
|
||||
|
||||
ConfigurationManager config = new ConfigurationManager(new File(new File(".").getParent(), "db.properties"));
|
||||
config.loadProperties();
|
||||
|
||||
if (!config.isSet("db.url")) {
|
||||
config.set(
|
||||
"db.url",
|
||||
"jdbc:mariadb://localhost:3306/ins?useUnicode=true&characterEncoding=utf8"
|
||||
);
|
||||
config.saveProperties();
|
||||
}
|
||||
|
||||
if (!config.isSet("db.user")) {
|
||||
config.set("db.user", "username");
|
||||
config.saveProperties();
|
||||
}
|
||||
|
||||
if (!config.isSet("db.password")) {
|
||||
config.set("db.password", "password");
|
||||
config.saveProperties();
|
||||
}
|
||||
|
||||
dsp = () -> {
|
||||
try {
|
||||
isInit = true;
|
||||
|
||||
return DriverManager.getConnection(config.getString("db.url"), config.getString("db.user"), config.getString("db.password"));
|
||||
} catch (SQLException e) {
|
||||
throw new SQLException("Failed to open DB connection", e);
|
||||
}
|
||||
};
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to create config", e);
|
||||
}
|
||||
UserDao udao = new UserDao(dsp);
|
||||
RegistrarDao rdao = new RegistrarDao(dsp);
|
||||
PasswordHasher hasher = new Pbkdf2Sha256Hasher(150_000, 16, 32);
|
||||
|
||||
INSTANCE = new WebApp(udao, rdao, hasher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initialized instance.
|
||||
*
|
||||
* @return app
|
||||
*/
|
||||
public static WebApp get() {
|
||||
WebApp v = INSTANCE;
|
||||
if (v == null) throw new IllegalStateException("Oac2WebApp not initialized. Call Oac2WebApp.init(...) first.");
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return user DAO
|
||||
*/
|
||||
public UserDao users() {
|
||||
return userDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return registrar DAO
|
||||
*/
|
||||
public RegistrarDao dao() {
|
||||
return registrarDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return password hasher
|
||||
*/
|
||||
public PasswordHasher passwordHasher() {
|
||||
return passwordHasher;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user