This commit is contained in:
UnlegitDqrk
2026-02-11 23:22:20 +01:00
parent 9483c36a66
commit 64ce55ea7b
39 changed files with 2033 additions and 3089 deletions

370
frontend/dashboard.java Normal file
View 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
View 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
View 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
View 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
View 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);
}
}

View 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) {
}
}

View 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
View 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
View 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;
}
}